多维表格
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

300 lines
8.9 KiB

import type { AttachmentType } from 'nocodb-sdk'
import RenameFile from './RenameFile.vue'
import {
ColumnInj,
EditModeInj,
IsFormInj,
IsPublicInj,
MetaInj,
NOCO,
ReadonlyInj,
computed,
inject,
isImage,
message,
ref,
useApi,
useFileDialog,
useI18n,
useInjectionState,
useProject,
watch,
} from '#imports'
import MdiPdfBox from '~icons/mdi/pdf-box'
import MdiFileWordOutline from '~icons/mdi/file-word-outline'
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'
export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
(updateModelValue: (data: string | Record<string, any>[]) => void) => {
const isReadonly = inject(ReadonlyInj, ref(false))
const isPublic = inject(IsPublicInj, ref(false))
const isForm = inject(IsFormInj, ref(false))
const meta = inject(MetaInj, ref())
const column = inject(ColumnInj, ref())
const editEnabled = inject(EditModeInj, ref(false))
/** keep user selected File object */
const storedFiles = ref<AttachmentType[]>([])
const attachments = ref<AttachmentType[]>([])
const modalVisible = ref(false)
/** for image carousel */
const selectedImage = ref()
const { project } = useProject()
const { api, isLoading } = useApi()
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: ['application', 'audio', 'image', 'video', 'misc'],
}),
}
/** our currently visible items, either the locally stored or the ones from db, depending on isPublic & isForm status */
const visibleItems = computed<any[]>(() => (isPublic.value && isForm.value ? storedFiles.value : attachments.value))
/** for bulk download */
const selectedVisibleItems = ref<boolean[]>(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))
}
}
/** save a file on select / drop, either locally (in-memory) or in the db */
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(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<AttachmentType>(
Array.from(files).map(
(file) =>
new Promise<AttachmentType>((resolve) => {
const res: { file: File; title: string; mimetype: string; data?: any } = {
...file,
file,
title: file.name,
mimetype: file.type,
}
if (isImage(file.name, (<any>file).mimetype ?? file.type)) {
const reader = new FileReader()
reader.onload = (e) => {
res.data = e.target?.result
resolve(res)
}
reader.onerror = () => {
resolve(res)
}
reader.readAsDataURL(file)
} else {
resolve(res)
}
}),
),
)
attachments.value = [...attachments.value, ...newFiles]
return updateModelValue(attachments.value)
}
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<boolean>((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) {
// set files
await onFileSelect(droppedFiles)
}
}
/** 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: 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':
return MdiPdfBox
case 'mdi-file-word-outline':
return MdiFileWordOutline
case 'mdi-file-powerpoint-box':
return MdiFilePowerpointBox
case 'mdi-file-excel-outline':
return MdiFileExcelOutline
default:
return IcOutlineInsertDriveFile
}
}
watch(files, (nextFiles) => nextFiles && onFileSelect(nextFiles))
return {
attachments,
visibleItems,
isPublic,
isForm,
isReadonly,
meta,
column,
editEnabled,
isLoading,
api,
open: () => open(),
onDrop,
modalVisible,
FileIcon,
removeFile,
renameFile,
downloadFile,
updateModelValue,
selectedImage,
selectedVisibleItems,
storedFiles,
bulkDownloadFiles,
defaultAttachmentMeta,
getAttachmentUrl,
}
},
'useAttachmentCell',
)