Compare commits

..
Author SHA1 Message Date
Etienne 3516703144 fix 2026-02-19 10:28:26 +01:00
Etienne 31111faeb9 fix 2026-02-19 09:41:12 +01:00
db9636bd9e i18n - docs translations (#18067)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-19 02:07:09 +01:00
5 changed files with 68 additions and 38 deletions
@@ -497,7 +497,7 @@ export default defineLogicFunction({
安装后函数是在你的应用安装到工作区后自动运行的逻辑函数。 这对于一次性设置任务很有用,例如填充默认数据、创建初始记录或配置工作区设置。
When you scaffold a new app with `create-twenty-app`, a post-install function is generated for you at `src/logic-functions/post-install.ts`:
当你使用 `create-twenty-app` 脚手架创建一个新应用时,会在 `src/logic-functions/post-install.ts` 为你生成一个安装后函数:
```typescript
// src/logic-functions/post-install.ts
@@ -518,7 +518,7 @@ export default defineLogicFunction({
});
```
The function is wired into your app by referencing its universal identifier in `application-config.ts`:
通过在 `application-config.ts` 中引用其通用标识符,可将该函数接入你的应用:
```typescript
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
@@ -17,12 +17,12 @@ import { ActivityList } from '@/activities/components/ActivityList';
import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { assertIsDefinedOrThrow } from 'twenty-shared/utils';
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
import { IconDownload, IconX } from 'twenty-ui/display';
import { IconButton } from 'twenty-ui/input';
import {
PermissionFlagType,
FeatureFlagKey,
PermissionFlagType,
} from '~/generated-metadata/graphql';
import { AttachmentRow } from './AttachmentRow';
@@ -212,15 +212,20 @@ export const AttachmentList = ({
/>
) : (
<ActivityList>
{attachments.map((attachment) => (
<AttachmentRow
key={attachment.id}
attachment={attachment}
onPreview={
isAttachmentPreviewEnabled ? handlePreview : undefined
}
/>
))}
{attachments
.filter(
(attachment) =>
!isFilesFieldMigrated || isDefined(attachment.file?.[0]),
)
.map((attachment) => (
<AttachmentRow
key={attachment.id}
attachment={attachment}
onPreview={
isAttachmentPreviewEnabled ? handlePreview : undefined
}
/>
))}
</ActivityList>
)}
</StyledDropZoneContainer>
@@ -22,8 +22,8 @@ import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFla
import { IconCalendar, OverflowingTextWithTooltip } from 'twenty-ui/display';
import { isNavigationModifierPressed } from 'twenty-ui/utilities';
import {
PermissionFlagType,
FeatureFlagKey,
PermissionFlagType,
} from '~/generated-metadata/graphql';
import { formatToHumanReadableDate } from '~/utils/date-utils';
import { getFileNameAndExtension } from '~/utils/file/getFileNameAndExtension';
@@ -96,7 +96,11 @@ export const AttachmentRow = ({
);
const { name: originalFileName, extension: attachmentFileExtension } =
getFileNameAndExtension(attachment.name);
getFileNameAndExtension(
isFilesFieldMigrated
? (attachment.file?.[0]?.label as string)
: attachment.name,
);
const [attachmentFileName, setAttachmentFileName] =
useState(originalFileName);
@@ -206,7 +210,9 @@ export const AttachmentRow = ({
target="_blank"
rel="noopener noreferrer"
>
<OverflowingTextWithTooltip text={attachment.name} />
<OverflowingTextWithTooltip
text={`${attachmentFileName}${attachmentFileExtension}`}
/>
</StyledLink>
</StyledLinkContainer>
)}
@@ -122,28 +122,26 @@ describe('extractFileInfo', () => {
});
});
it('should fallback to extension mime type when content cannot be detected for PNG extension', async () => {
const result = await extractFileInfo({
file: textBuffer,
filename: 'fake-image.png',
});
expect(result).toEqual({
mimeType: 'image/png',
ext: 'png',
});
it('should throw error when PNG extension is used with non-PNG buffer', async () => {
await expect(
extractFileInfo({
file: textBuffer,
filename: 'fake-image.png',
}),
).rejects.toThrow(
"File content does not match its extension. The file has extension 'png' (expected mime type: image/png), but the file content could not be detected as this type. The file may be corrupted, have the wrong extension, or be a security risk.",
);
});
it('should fallback to extension mime type when content cannot be detected for PDF extension', async () => {
const result = await extractFileInfo({
file: textBuffer,
filename: 'fake-document.pdf',
});
expect(result).toEqual({
mimeType: 'application/pdf',
ext: 'pdf',
});
it('should throw error when PDF extension is used with non-PDF buffer', async () => {
await expect(
extractFileInfo({
file: textBuffer,
filename: 'fake-document.pdf',
}),
).rejects.toThrow(
"File content does not match its extension. The file has extension 'pdf' (expected mime type: application/pdf), but the file content could not be detected as this type. The file may be corrupted, have the wrong extension, or be a security risk.",
);
});
it('should handle markdown files using extension', async () => {
@@ -1,8 +1,14 @@
import { msg } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import FileType from 'file-type';
import FileType, { type MimeType } from 'file-type';
import { lookup } from 'mrmime';
import { isDefined } from 'twenty-shared/utils';
import {
FileStorageException,
FileStorageExceptionCode,
} from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
import { buildFileInfo } from 'src/engine/core-modules/file/utils/build-file-info.utils';
export const extractFileInfo = async ({
@@ -29,7 +35,22 @@ export const extractFileInfo = async ({
let mimeType: string = 'application/octet-stream';
if (isNonEmptyString(ext)) {
mimeType = lookup(ext) ?? 'application/octet-stream';
const mimeTypeFromExtension = lookup(ext);
if (
mimeTypeFromExtension &&
FileType.mimeTypes.has(mimeTypeFromExtension as MimeType)
) {
throw new FileStorageException(
`File content does not match its extension. The file has extension '${ext}' (expected mime type: ${mimeTypeFromExtension}), but the file content could not be detected as this type. The file may be corrupted, have the wrong extension, or be a security risk.`,
FileStorageExceptionCode.INVALID_EXTENSION,
{
userFriendlyMessage: msg`The file extension doesn't match the file content. Please check that your file is not corrupted and has the correct extension.`,
},
);
}
mimeType = mimeTypeFromExtension ?? 'application/octet-stream';
}
return {