diff --git a/packages/nc-gui/components.d.ts b/packages/nc-gui/components.d.ts
index 7809d0a39b..05d859d72c 100644
--- a/packages/nc-gui/components.d.ts
+++ b/packages/nc-gui/components.d.ts
@@ -69,6 +69,7 @@ declare module '@vue/runtime-core' {
ATextarea: typeof import('ant-design-vue/es')['Textarea']
ATimePicker: typeof import('ant-design-vue/es')['TimePicker']
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
+ ATree: typeof import('ant-design-vue/es')['Tree']
ATypographyTitle: typeof import('ant-design-vue/es')['TypographyTitle']
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
BiFiletypeJson: typeof import('~icons/bi/filetype-json')['default']
diff --git a/packages/nc-gui/components/cell/attachment/Carousel.vue b/packages/nc-gui/components/cell/attachment/Carousel.vue
index 63a7c1bb0f..bf0bf3983b 100644
--- a/packages/nc-gui/components/cell/attachment/Carousel.vue
+++ b/packages/nc-gui/components/cell/attachment/Carousel.vue
@@ -70,13 +70,13 @@ onClickOutside(carouselRef, () => {
>
-
+
-
+
@@ -105,6 +105,9 @@ onClickOutside(carouselRef, () => {
diff --git a/packages/nc-gui/components/cell/attachment/index.vue b/packages/nc-gui/components/cell/attachment/index.vue
index 7a4e99a315..7cf0364f86 100644
--- a/packages/nc-gui/components/cell/attachment/index.vue
+++ b/packages/nc-gui/components/cell/attachment/index.vue
@@ -60,6 +60,7 @@ const {
selectedImage,
isReadonly,
storedFiles,
+ getAttachmentUrl,
} = useProvideAttachmentCell(updateModelValue)
watch(
@@ -97,10 +98,19 @@ const { isOverDropZone } = useDropZone(currentCellRef as any, onDrop)
/** on new value, reparse our stored attachments */
watch(
() => modelValue,
- (nextModel) => {
+ async (nextModel) => {
if (nextModel) {
try {
- const nextAttachments = ((typeof nextModel === 'string' ? JSON.parse(nextModel) : nextModel) || []).filter(Boolean)
+ let nextAttachments = ((typeof nextModel === 'string' ? JSON.parse(nextModel) : nextModel) || []).filter(Boolean)
+
+ // reconstruct the url
+ // See /packages/nocodb/src/lib/version-upgrader/ncAttachmentUpgrader.ts for the details
+ nextAttachments = await Promise.all(
+ nextAttachments.map(async (attachment: any) => ({
+ ...attachment,
+ url: await getAttachmentUrl(attachment),
+ })),
+ )
if (isPublic.value && isForm.value) {
storedFiles.value = nextAttachments
diff --git a/packages/nc-gui/components/cell/attachment/utils.ts b/packages/nc-gui/components/cell/attachment/utils.ts
index 637d967476..6b202c519c 100644
--- a/packages/nc-gui/components/cell/attachment/utils.ts
+++ b/packages/nc-gui/components/cell/attachment/utils.ts
@@ -1,3 +1,5 @@
+import type { AttachmentType } from 'nocodb-sdk'
+import RenameFile from './RenameFile.vue'
import {
ColumnInj,
EditModeInj,
@@ -24,13 +26,6 @@ import MdiFilePowerpointBox from '~icons/mdi/file-powerpoint-box'
import MdiFileExcelOutline from '~icons/mdi/file-excel-outline'
import IcOutlineInsertDriveFile from '~icons/ic/outline-insert-drive-file'
-interface AttachmentProps extends File {
- data?: any
- file: File
- title: string
- mimetype: string
-}
-
export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
(updateModelValue: (data: string | Record[]) => void) => {
const isReadonly = inject(ReadonlyInj, ref(false))
@@ -46,12 +41,13 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
const editEnabled = inject(EditModeInj, ref(false))
/** keep user selected File object */
- const storedFiles = ref([])
+ const storedFiles = ref([])
- const attachments = ref([])
+ const attachments = ref([])
const modalVisible = ref(false)
+ /** for image carousel */
const selectedImage = ref()
const { project } = useProject()
@@ -60,17 +56,37 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
const { files, open } = useFileDialog()
+ const { appInfo } = useGlobal()
+
const { t } = useI18n()
+ const defaultAttachmentMeta = {
+ ...(appInfo.value.ee && {
+ // Maximum Number of Attachments per cell
+ maxNumberOfAttachments: Math.max(1, +appInfo.value.ncMaxAttachmentsAllowed || 50) || 50,
+ // Maximum File Size per file
+ maxAttachmentSize: Math.max(1, +appInfo.value.ncMaxAttachmentsAllowed || 20) || 20,
+ supportedAttachmentMimeTypes: ['*'],
+ }),
+ }
+
+ /** our currently visible items, either the locally stored or the ones from db, depending on isPublic & isForm status */
+ const visibleItems = computed(() => (isPublic.value && isForm.value ? storedFiles.value : attachments.value))
+
+ /** for bulk download */
+ const selectedVisibleItems = ref(Array.from({ length: visibleItems.value.length }, () => false))
+
/** remove a file from our stored attachments (either locally stored or saved ones) */
function removeFile(i: number) {
if (isPublic.value) {
storedFiles.value.splice(i, 1)
attachments.value.splice(i, 1)
+ selectedVisibleItems.value.splice(i, 1)
updateModelValue(storedFiles.value)
} else {
attachments.value.splice(i, 1)
+ selectedVisibleItems.value.splice(i, 1)
updateModelValue(JSON.stringify(attachments.value))
}
@@ -80,12 +96,58 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
async function onFileSelect(selectedFiles: FileList | File[]) {
if (!selectedFiles.length) return
+ const attachmentMeta = {
+ ...defaultAttachmentMeta,
+ ...(typeof column.value?.meta === 'string' ? JSON.parse(column.value.meta) : column.value?.meta),
+ }
+
+ const newAttachments = []
+
+ const files: File[] = []
+
+ for (const file of selectedFiles) {
+ if (appInfo.value.ee) {
+ // verify number of files
+ if (visibleItems.value.length + selectedFiles.length > attachmentMeta.maxNumberOfAttachments) {
+ message.error(
+ `You can only upload at most ${attachmentMeta.maxNumberOfAttachments} file${
+ attachmentMeta.maxNumberOfAttachments > 1 ? 's' : ''
+ } to this cell.`,
+ )
+ return
+ }
+
+ // verify file size
+ if (file.size > attachmentMeta.maxAttachmentSize * 1024 * 1024) {
+ message.error(`The size of ${file.name} exceeds the maximum file size ${attachmentMeta.maxAttachmentSize} MB.`)
+ continue
+ }
+
+ // verify mime type
+ if (
+ !attachmentMeta.supportedAttachmentMimeTypes.includes('*') &&
+ !attachmentMeta.supportedAttachmentMimeTypes.includes(file.type) &&
+ !attachmentMeta.supportedAttachmentMimeTypes.includes(file.type.split('/')[0])
+ ) {
+ message.error(`${file.name} has the mime type ${file.type} which is not allowed in this column.`)
+ continue
+ }
+ }
+
+ files.push(file)
+ }
+
if (isPublic.value && isForm.value) {
- const newFiles = await Promise.all(
- Array.from(selectedFiles).map(
+ const newFiles = await Promise.all(
+ Array.from(files).map(
(file) =>
- new Promise((resolve) => {
- const res: AttachmentProps = { ...file, file, title: file.name, mimetype: file.type }
+ new Promise((resolve) => {
+ const res: { file: File; title: string; mimetype: string; data?: any } = {
+ ...file,
+ file,
+ title: file.name,
+ mimetype: file.type,
+ }
if (isImage(file.name, (file).mimetype ?? file.type)) {
const reader = new FileReader()
@@ -107,35 +169,47 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
}),
),
)
-
attachments.value = [...attachments.value, ...newFiles]
return updateModelValue(attachments.value)
}
- const newAttachments = []
-
- for (const file of selectedFiles) {
- try {
- const data = await api.storage.upload(
- {
- path: [NOCO, project.value.title, meta.value?.title, column.value?.title].join('/'),
- },
- {
- files: file,
- json: '{}',
- },
- )
-
- newAttachments.push(...data)
- } catch (e: any) {
- message.error(e.message || t('msg.error.internalError'))
- }
+ try {
+ const data = await api.storage.upload(
+ {
+ path: [NOCO, project.value.title, meta.value?.title, column.value?.title].join('/'),
+ },
+ {
+ files,
+ json: '{}',
+ },
+ )
+ newAttachments.push(...data)
+ } catch (e: any) {
+ message.error(e.message || t('msg.error.internalError'))
}
updateModelValue(JSON.stringify([...attachments.value, ...newAttachments]))
}
+ async function renameFile(attachment: AttachmentType, idx: number) {
+ return new Promise((resolve) => {
+ const { close } = useDialog(RenameFile, {
+ title: attachment.title,
+ onRename: (newTitle: string) => {
+ attachments.value[idx].title = newTitle
+ updateModelValue(JSON.stringify(attachments.value))
+ close()
+ resolve(true)
+ },
+ onCancel: () => {
+ close()
+ resolve(true)
+ },
+ })
+ })
+ }
+
/** save files on drop */
async function onDrop(droppedFiles: File[] | null) {
if (droppedFiles) {
@@ -144,11 +218,41 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
}
}
+ /** bulk download selected files */
+ async function bulkDownloadFiles() {
+ await Promise.all(selectedVisibleItems.value.map(async (v, i) => v && (await downloadFile(visibleItems.value[i]))))
+ selectedVisibleItems.value = Array.from({ length: visibleItems.value.length }, () => false)
+ }
+
/** download a file */
- async function downloadFile(item: Record) {
+ async function downloadFile(item: AttachmentType) {
;(await import('file-saver')).saveAs(item.url || item.data, item.title)
}
+ /** construct the attachment url
+ * See /packages/nocodb/src/lib/version-upgrader/ncAttachmentUpgrader.ts for the details
+ * */
+ async function getAttachmentUrl(item: AttachmentType) {
+ const path = item?.path
+ // if path doesn't exist, use `item.url`
+ if (path) {
+ // try ${appInfo.value.ncSiteUrl}/${item.path} first
+ const url = `${appInfo.value.ncSiteUrl}/${item.path}`
+ try {
+ const res = await fetch(url)
+ if (res.ok) {
+ // use `url` if it is accessible
+ return Promise.resolve(url)
+ }
+ } catch {
+ // for some cases, `url` is not accessible as expected
+ // do nothing here
+ }
+ }
+ // if it fails, use the original url
+ return Promise.resolve(item.url)
+ }
+
const FileIcon = (icon: string) => {
switch (icon) {
case 'mdi-pdf-box':
@@ -164,9 +268,6 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
}
}
- /** our currently visible items, either the locally stored or the ones from db, depending on isPublic & isForm status */
- const visibleItems = computed(() => (isPublic.value && isForm.value ? storedFiles.value : attachments.value))
-
watch(files, (nextFiles) => nextFiles && onFileSelect(nextFiles))
return {
@@ -185,10 +286,15 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
modalVisible,
FileIcon,
removeFile,
+ renameFile,
downloadFile,
updateModelValue,
selectedImage,
+ selectedVisibleItems,
storedFiles,
+ bulkDownloadFiles,
+ defaultAttachmentMeta,
+ getAttachmentUrl,
}
},
'useAttachmentCell',
diff --git a/packages/nc-gui/components/smartsheet/column/AttachmentOptions.vue b/packages/nc-gui/components/smartsheet/column/AttachmentOptions.vue
new file mode 100644
index 0000000000..7dd3884cb7
--- /dev/null
+++ b/packages/nc-gui/components/smartsheet/column/AttachmentOptions.vue
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Allow All Mime Types
+
+
+
+
+
+
+ {{ title.substr(0, title.indexOf(searchValue)) }}
+ {{ searchValue }}
+ {{ title.substr(title.indexOf(searchValue) + searchValue.length) }}
+
+ {{ title }}
+
+
+
+
+
+
+
diff --git a/packages/nc-gui/components/smartsheet/column/EditOrAdd.vue b/packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
index 8211e94759..603468b06b 100644
--- a/packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
+++ b/packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
@@ -14,6 +14,7 @@ import {
uiTypes,
useColumnCreateStoreOrThrow,
useEventListener,
+ useGlobal,
useI18n,
useMetas,
useNuxtApp,
@@ -38,6 +39,8 @@ const { t } = useI18n()
const { $e } = useNuxtApp()
+const { appInfo } = useGlobal()
+
const meta = inject(MetaInj, ref())
const isForm = inject(IsFormInj, ref(false))
@@ -133,7 +136,7 @@ useEventListener('keydown', (e: KeyboardEvent) => {
@@ -212,6 +215,11 @@ useEventListener('keydown', (e: KeyboardEvent) => {
+
+
diff --git a/packages/nc-gui/components/smartsheet/column/utils.ts b/packages/nc-gui/components/smartsheet/column/utils.ts
index 6a6a253c8b..c12bf237b2 100644
--- a/packages/nc-gui/components/smartsheet/column/utils.ts
+++ b/packages/nc-gui/components/smartsheet/column/utils.ts
@@ -7,3 +7,193 @@ const relationNames = {
export function getRelationName(type: string) {
return relationNames[type as keyof typeof relationNames]
}
+
+// supported mime types
+// retrieved from https://github.com/sindresorhus/file-type/blob/main/supported.js#L146
+export const fileMimeTypes = [
+ {
+ title: 'Application',
+ key: 'application',
+ children: [
+ { title: 'application/dicom', key: 'application/dicom' },
+ { title: 'application/eps', key: 'application/eps' },
+ { title: 'application/epub+zip', key: 'application/epub+zip' },
+ { title: 'application/gzip', key: 'application/gzip' },
+ { title: 'application/mxf', key: 'application/mxf' },
+ { title: 'application/ogg', key: 'application/ogg' },
+ { title: 'application/pdf', key: 'application/pdf' },
+ { title: 'application/pgp-encrypted', key: 'application/pgp-encrypted' },
+ { title: 'application/postscript', key: 'application/postscript' },
+ { title: 'application/rtf', key: 'application/rtf' },
+ { title: 'application/vnd.ms-asf', key: 'application/vnd.ms-asf' },
+ { title: 'application/vnd.ms-cab-compressed', key: 'application/vnd.ms-cab-compressed' },
+ { title: 'application/vnd.ms-fontobject', key: 'application/vnd.ms-fontobject' },
+ { title: 'application/vnd.ms-htmlhelp', key: 'application/vnd.ms-htmlhelp' },
+ { title: 'application/vnd.ms-outlook', key: 'application/vnd.ms-outlook' },
+ { title: 'application/vnd.oasis.opendocument.presentation', key: 'application/vnd.oasis.opendocument.presentation' },
+ { title: 'application/vnd.oasis.opendocument.spreadsheet', key: 'application/vnd.oasis.opendocument.spreadsheet' },
+ { title: 'application/vnd.oasis.opendocument.text', key: 'application/vnd.oasis.opendocument.text' },
+ {
+ title: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ key: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ },
+ {
+ title: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ key: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ },
+ {
+ title: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ key: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ },
+ { title: 'application/vnd.sketchup.skp', key: 'application/vnd.sketchup.skp' },
+ { title: 'application/vnd.tcpdump.pcap', key: 'application/vnd.tcpdump.pcap' },
+ { title: 'application/wasm', key: 'application/wasm' },
+ { title: 'application/x-7z-compressed', key: 'application/x-7z-compressed' },
+ { title: 'application/x-apache-arrow', key: 'application/x-apache-arrow' },
+ { title: 'application/x-apple-diskimage', key: 'application/x-apple-diskimage' },
+ { title: 'application/x-asar', key: 'application/x-asar' },
+ { title: 'application/x-blender', key: 'application/x-blender' },
+ { title: 'application/x-bzip2', key: 'application/x-bzip2' },
+ { title: 'application/x-cfb', key: 'application/x-cfb' },
+ { title: 'application/x-compress', key: 'application/x-compress' },
+ { title: 'application/x-deb', key: 'application/x-deb' },
+ { title: 'application/x-elf', key: 'application/x-elf' },
+ { title: 'application/x-esri-shape', key: 'application/x-esri-shape' },
+ { title: 'application/x-google-chrome-extension', key: 'application/x-google-chrome-extension' },
+ { title: 'application/x-indesign', key: 'application/x-indesign' },
+ { title: 'application/x-lzh-compressed', key: 'application/x-lzh-compressed' },
+ { title: 'application/x-lzip', key: 'application/x-lzip' },
+ { title: 'application/x-mie', key: 'application/x-mie' },
+ { title: 'application/x-mobipocket-ebook', key: 'application/x-mobipocket-ebook' },
+ { title: 'application/x-msdownload', key: 'application/x-msdownload' },
+ { title: 'application/x-nintendo-nes-rom', key: 'application/x-nintendo-nes-rom' },
+ { title: 'application/x-rar-compressed', key: 'application/x-rar-compressed' },
+ { title: 'application/x-rpm', key: 'application/x-rpm' },
+ { title: 'application/x-shockwave-flash', key: 'application/x-shockwave-flash' },
+ { title: 'application/x-sqlite3', key: 'application/x-sqlite3' },
+ { title: 'application/x-tar', key: 'application/x-tar' },
+ { title: 'application/x-unix-archive', key: 'application/x-unix-archive' },
+ { title: 'application/x-xpinstall', key: 'application/x-xpinstall' },
+ { title: 'application/x-xz', key: 'application/x-xz' },
+ { title: 'application/x.apple.alias', key: 'application/x.apple.alias' },
+ { title: 'application/x.ms.shortcut', key: 'application/x.ms.shortcut' },
+ { title: 'application/xml', key: 'application/xml' },
+ { title: 'application/zip', key: 'application/zip' },
+ { title: 'application/zstd', key: 'application/zstd' },
+ ],
+ },
+ {
+ title: 'Audio',
+ key: 'audio',
+ children: [
+ { title: 'audio/aac', key: 'audio/aac' },
+ { title: 'audio/aiff', key: 'audio/aiff' },
+ { title: 'audio/amr', key: 'audio/amr' },
+ { title: 'audio/ape', key: 'audio/ape' },
+ { title: 'audio/midi', key: 'audio/midi' },
+ { title: 'audio/mp4', key: 'audio/mp4' },
+ { title: 'audio/mpeg', key: 'audio/mpeg' },
+ { title: 'audio/ogg', key: 'audio/ogg' },
+ { title: 'audio/opus', key: 'audio/opus' },
+ { title: 'audio/qcelp', key: 'audio/qcelp' },
+ { title: 'audio/vnd.dolby.dd-raw', key: 'audio/vnd.dolby.dd-raw' },
+ { title: 'audio/vnd.wave', key: 'audio/vnd.wave' },
+ { title: 'audio/wavpack', key: 'audio/wavpack' },
+ { title: 'audio/x-dsf', key: 'audio/x-dsf' },
+ { title: 'audio/x-flac', key: 'audio/x-flac' },
+ { title: 'audio/x-it', key: 'audio/x-it' },
+ { title: 'audio/x-m4a', key: 'audio/x-m4a' },
+ { title: 'audio/x-ms-asf', key: 'audio/x-ms-asf' },
+ { title: 'audio/x-musepack', key: 'audio/x-musepack' },
+ { title: 'audio/x-s3m', key: 'audio/x-s3m' },
+ { title: 'audio/x-voc', key: 'audio/x-voc' },
+ { title: 'audio/x-xm', key: 'audio/x-xm' },
+ ],
+ },
+ {
+ title: 'Image',
+ key: 'image',
+ children: [
+ { title: 'image/apng', key: 'image/apng' },
+ { title: 'image/avif', key: 'image/avif' },
+ { title: 'image/bmp', key: 'image/bmp' },
+ { title: 'image/bpg', key: 'image/bpg' },
+ { title: 'image/flif', key: 'image/flif' },
+ { title: 'image/gif', key: 'image/gif' },
+ { title: 'image/heic', key: 'image/heic' },
+ { title: 'image/heic-sequence', key: 'image/heic-sequence' },
+ { title: 'image/heif', key: 'image/heif' },
+ { title: 'image/heif-sequence', key: 'image/heif-sequence' },
+ { title: 'image/icns', key: 'image/icns' },
+ { title: 'image/jls', key: 'image/jls' },
+ { title: 'image/jp2', key: 'image/jp2' },
+ { title: 'image/jpeg', key: 'image/jpeg' },
+ { title: 'image/jpm', key: 'image/jpm' },
+ { title: 'image/jpx', key: 'image/jpx' },
+ { title: 'image/jxl', key: 'image/jxl' },
+ { title: 'image/ktx', key: 'image/ktx' },
+ { title: 'image/mj2', key: 'image/mj2' },
+ { title: 'image/png', key: 'image/png' },
+ { title: 'image/tiff', key: 'image/tiff' },
+ { title: 'image/vnd.adobe.photoshop', key: 'image/vnd.adobe.photoshop' },
+ { title: 'image/vnd.dwg', key: 'image/vnd.dwg' },
+ { title: 'image/vnd.ms-photo', key: 'image/vnd.ms-photo' },
+ { title: 'image/webp', key: 'image/webp' },
+ { title: 'image/x-adobe-dng', key: 'image/x-adobe-dng' },
+ { title: 'image/x-canon-cr2', key: 'image/x-canon-cr2' },
+ { title: 'image/x-canon-cr3', key: 'image/x-canon-cr3' },
+ { title: 'image/x-fujifilm-raf', key: 'image/x-fujifilm-raf' },
+ { title: 'image/x-icon', key: 'image/x-icon' },
+ { title: 'image/x-nikon-nef', key: 'image/x-nikon-nef' },
+ { title: 'image/x-olympus-orf', key: 'image/x-olympus-orf' },
+ { title: 'image/x-panasonic-rw2', key: 'image/x-panasonic-rw2' },
+ { title: 'image/x-sony-arw', key: 'image/x-sony-arw' },
+ { title: 'image/x-xcf', key: 'image/x-xcf' },
+ ],
+ },
+ {
+ title: 'Video',
+ key: 'video',
+ children: [
+ { title: 'video/3gpp', key: 'video/3gpp' },
+ { title: 'video/3gpp2', key: 'video/3gpp2' },
+ { title: 'video/MP1S', key: 'video/MP1S' },
+ { title: 'video/MP2P', key: 'video/MP2P' },
+ { title: 'video/mp2t', key: 'video/mp2t' },
+ { title: 'video/mp4', key: 'video/mp4' },
+ { title: 'video/mpeg', key: 'video/mpeg' },
+ { title: 'video/ogg', key: 'video/ogg' },
+ { title: 'video/quicktime', key: 'video/quicktime' },
+ { title: 'video/vnd.avi', key: 'video/vnd.avi' },
+ { title: 'video/webm', key: 'video/webm' },
+ { title: 'video/x-flv', key: 'video/x-flv' },
+ { title: 'video/x-m4v', key: 'video/x-m4v' },
+ { title: 'video/x-matroska', key: 'video/x-matroska' },
+ { title: 'video/x-ms-asf', key: 'video/x-ms-asf' },
+ ],
+ },
+ {
+ title: 'Misc',
+ key: 'misc',
+ children: [
+ { title: 'model/3mf', key: 'model/3mf' },
+ { title: 'model/gltf-binary', key: 'model/gltf-binary' },
+ { title: 'model/stl', key: 'model/stl' },
+ { title: 'text/calendar', key: 'text/calendar' },
+ { title: 'text/vcard', key: 'text/vcard' },
+ { title: 'text/plain', key: 'text/plain' },
+ { title: 'text/html', key: 'text/html' },
+ { title: 'text/xml', key: 'text/xml' },
+ { title: 'text/calendar', key: 'text/calendar' },
+ { title: 'text/javascript', key: 'text/javascript' },
+ { title: 'text/css', key: 'text/css' },
+ { title: 'text/csv', key: 'text/csv' },
+ { title: 'font/otf', key: 'font/otf' },
+ { title: 'font/ttf', key: 'font/ttf' },
+ { title: 'font/woff', key: 'font/woff' },
+ { title: 'font/woff2', key: 'font/woff2' },
+ ],
+ },
+]
+
+export const fileMimeTypeList = fileMimeTypes.map((o) => o.children).flat(1)
diff --git a/packages/nc-gui/components/virtual-cell/QrCode.vue b/packages/nc-gui/components/virtual-cell/QrCode.vue
index 14b772bc74..4ba96f3ab7 100644
--- a/packages/nc-gui/components/virtual-cell/QrCode.vue
+++ b/packages/nc-gui/components/virtual-cell/QrCode.vue
@@ -1,6 +1,6 @@