feat: use blacklist approach for file editability in Monaco Editor

Switch from whitelist to blacklist pattern for determining editable files.
Files are now editable unless they are known binary types (audio, archives,
executables, Office documents, fonts, etc.), enabling support for any
runtime-generated text files without needing to add extensions one by one.
This commit is contained in:
yyh
2026-01-21 16:53:01 +08:00
parent b5e31c0f25
commit d444a8eadc
3 changed files with 86 additions and 15 deletions

View File

@ -2,9 +2,9 @@ import type { AppAssetTreeView } from '@/types/app-asset'
import { useMemo } from 'react' import { useMemo } from 'react'
import { import {
getFileExtension, getFileExtension,
isCodeOrTextFile,
isImageFile, isImageFile,
isMarkdownFile, isMarkdownFile,
isTextLikeFile,
isVideoFile, isVideoFile,
} from '../utils/file-utils' } from '../utils/file-utils'
@ -17,24 +17,21 @@ export type FileTypeInfo = {
isMediaFile: boolean isMediaFile: boolean
} }
/**
* Hook to determine file type information based on file node.
* Returns flags for markdown, code/text, image, video files.
*/
export function useFileTypeInfo(fileNode: AppAssetTreeView | undefined): FileTypeInfo { export function useFileTypeInfo(fileNode: AppAssetTreeView | undefined): FileTypeInfo {
return useMemo(() => { return useMemo(() => {
const ext = getFileExtension(fileNode?.name, fileNode?.extension) const ext = getFileExtension(fileNode?.name, fileNode?.extension)
const markdown = isMarkdownFile(ext) const markdown = isMarkdownFile(ext)
const codeOrText = isCodeOrTextFile(ext)
const image = isImageFile(ext) const image = isImageFile(ext)
const video = isVideoFile(ext) const video = isVideoFile(ext)
const editable = isTextLikeFile(ext)
const codeOrText = editable && !markdown
return { return {
isMarkdown: markdown, isMarkdown: markdown,
isCodeOrText: codeOrText, isCodeOrText: codeOrText,
isImage: image, isImage: image,
isVideo: video, isVideo: video,
isEditable: markdown || codeOrText, isEditable: editable,
isMediaFile: image || video, isMediaFile: image || video,
} }
}, [fileNode?.name, fileNode?.extension]) }, [fileNode?.name, fileNode?.extension])

View File

@ -12,7 +12,7 @@ import {
useCreateAppAssetFolder, useCreateAppAssetFolder,
useRenameAppAssetNode, useRenameAppAssetNode,
} from '@/service/use-app-asset' } from '@/service/use-app-asset'
import { getFileExtension, isCodeOrTextFile, isMarkdownFile } from '../utils/file-utils' import { getFileExtension, isTextLikeFile } from '../utils/file-utils'
import { createDraftTreeNode, insertDraftTreeNode } from '../utils/tree-utils' import { createDraftTreeNode, insertDraftTreeNode } from '../utils/tree-utils'
type UseInlineCreateNodeOptions = { type UseInlineCreateNodeOptions = {
@ -86,7 +86,7 @@ export function useInlineCreateNode({
parentId: pendingCreateParentId, parentId: pendingCreateParentId,
}) })
const extension = getFileExtension(trimmedName, createdFile.extension) const extension = getFileExtension(trimmedName, createdFile.extension)
if (isMarkdownFile(extension) || isCodeOrTextFile(extension)) if (isTextLikeFile(extension))
storeApi.getState().openTab(createdFile.id, { pinned: true }) storeApi.getState().openTab(createdFile.id, { pinned: true })
Toast.notify({ Toast.notify({
type: 'success', type: 'success',

View File

@ -2,10 +2,80 @@ import { FileAppearanceTypeEnum } from '@/app/components/base/file-uploader/type
const MARKDOWN_EXTENSIONS = ['md', 'markdown', 'mdx'] const MARKDOWN_EXTENSIONS = ['md', 'markdown', 'mdx']
const CODE_EXTENSIONS = ['json', 'yaml', 'yml', 'toml', 'js', 'jsx', 'ts', 'tsx', 'py', 'schema'] const CODE_EXTENSIONS = ['json', 'yaml', 'yml', 'toml', 'js', 'jsx', 'ts', 'tsx', 'py', 'schema']
const TEXT_EXTENSIONS = ['txt', 'log', 'ini', 'env'] const IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp', 'ico', 'tiff', 'psd', 'heic', 'heif', 'avif']
const IGNORE_EXTENSIONS = ['gitignore', 'dockerignore', 'prettierignore', 'eslintignore', 'npmignore', 'hgignore'] const VIDEO_EXTENSIONS = ['mp4', 'mov', 'webm', 'mpeg', 'mpg', 'm4v', 'avi', 'mkv', 'flv', 'wmv', '3gp']
const IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp', 'ico']
const VIDEO_EXTENSIONS = ['mp4', 'mov', 'webm', 'mpeg', 'mpg', 'm4v', 'avi'] const BINARY_EXTENSIONS = [
'mp3',
'wav',
'ogg',
'flac',
'm4a',
'aac',
'wma',
'aiff',
'opus',
'zip',
'tar',
'gz',
'rar',
'7z',
'bz2',
'xz',
'tgz',
'tbz2',
'lz',
'lzma',
'cab',
'iso',
'dmg',
'exe',
'dll',
'so',
'dylib',
'bin',
'o',
'obj',
'class',
'pyc',
'pyo',
'pyd',
'wasm',
'app',
'msi',
'deb',
'rpm',
'pdf',
'doc',
'docx',
'xls',
'xlsx',
'ppt',
'pptx',
'odt',
'ods',
'odp',
'rtf',
'epub',
'mobi',
'ttf',
'otf',
'woff',
'woff2',
'eot',
'db',
'sqlite',
'sqlite3',
'mdb',
'accdb',
'jar',
'war',
'ear',
'apk',
'ipa',
'aab',
'lock',
]
export function getFileExtension(name?: string, extension?: string): string { export function getFileExtension(name?: string, extension?: string): string {
if (extension) if (extension)
@ -31,8 +101,12 @@ export function isMarkdownFile(extension: string): boolean {
return MARKDOWN_EXTENSIONS.includes(extension) return MARKDOWN_EXTENSIONS.includes(extension)
} }
export function isCodeOrTextFile(extension: string): boolean { export function isBinaryFile(extension: string): boolean {
return CODE_EXTENSIONS.includes(extension) || TEXT_EXTENSIONS.includes(extension) || IGNORE_EXTENSIONS.includes(extension) return BINARY_EXTENSIONS.includes(extension)
}
export function isTextLikeFile(extension: string): boolean {
return !isBinaryFile(extension) && !isImageFile(extension) && !isVideoFile(extension)
} }
export function isImageFile(extension: string): boolean { export function isImageFile(extension: string): boolean {