Browse Source

Merge pull request #7491 from nocodb/nc-feat/copy-paste-attachment

feat: add attachments from clipboard
pull/7529/head
Raju Udava 8 months ago committed by GitHub
parent
commit
e88fcb0334
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 67
      packages/nc-gui/composables/useMultiSelect/convertCellData.ts
  2. 104
      packages/nc-gui/composables/useMultiSelect/index.ts
  3. 16
      packages/nocodb-sdk/src/lib/helperFunctions.ts

67
packages/nc-gui/composables/useMultiSelect/convertCellData.ts

@ -5,16 +5,16 @@ import type { AppInfo } from '~/composables/useGlobal'
import { parseProp } from '#imports' import { parseProp } from '#imports'
export default function convertCellData( export default function convertCellData(
args: { to: UITypes; value: string; column: ColumnType; appInfo: AppInfo }, args: { to: UITypes; value: string; column: ColumnType; appInfo: AppInfo; files?: FileList | File[]; oldValue?: unknown },
isMysql = false, isMysql = false,
isMultiple = false, isMultiple = false,
) { ) {
const { to, value, column } = args const { to, value, column, files = [], oldValue } = args
const dateFormat = isMysql ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ' const dateFormat = isMysql ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ'
// return null if value is empty // return null if value is empty
if (value === '') return null if (value === '' && to !== UITypes.Attachment) return null
switch (to) { switch (to) {
case UITypes.SingleLineText: case UITypes.SingleLineText:
@ -113,22 +113,36 @@ export default function convertCellData(
} }
} }
case UITypes.Attachment: { case UITypes.Attachment: {
let parsedVal const parsedOldValue = parseProp(oldValue)
try { const oldAttachments = parsedOldValue && Array.isArray(parsedOldValue) ? parsedOldValue : []
parsedVal = parseProp(value)
parsedVal = Array.isArray(parsedVal) ? parsedVal : [parsedVal]
} catch (e) {
if (isMultiple) {
return null
} else {
throw new Error('Invalid attachment data')
}
}
if (parsedVal.some((v: any) => v && !(v.url || v.data || v.path))) { if (!value && !files.length) {
if (oldAttachments.length) return undefined
return null return null
} }
let parsedVal = []
if (value) {
try {
parsedVal = parseProp(value)
parsedVal = Array.isArray(parsedVal)
? parsedVal
: typeof parsedVal === 'object' && Object.keys(parsedVal).length
? [parsedVal]
: []
} catch (e) {
if (isMultiple) {
return null
} else {
throw new Error('Invalid attachment data')
}
}
if (parsedVal.some((v: any) => v && !(v.url || v.data || v.path))) {
return null
}
}
// TODO(refactor): duplicate logic in attachment/utils.ts // TODO(refactor): duplicate logic in attachment/utils.ts
const defaultAttachmentMeta = { const defaultAttachmentMeta = {
...(args.appInfo.ee && { ...(args.appInfo.ee && {
@ -147,7 +161,7 @@ export default function convertCellData(
const attachments = [] const attachments = []
for (const attachment of parsedVal) { for (const attachment of value ? parsedVal : files) {
if (args.appInfo.ee) { if (args.appInfo.ee) {
// verify number of files // verify number of files
if (parsedVal.length > attachmentMeta.maxNumberOfAttachments) { if (parsedVal.length > attachmentMeta.maxNumberOfAttachments) {
@ -164,7 +178,6 @@ export default function convertCellData(
message.error(`The size of ${attachment.name} exceeds the maximum file size ${attachmentMeta.maxAttachmentSize} MB.`) message.error(`The size of ${attachment.name} exceeds the maximum file size ${attachmentMeta.maxAttachmentSize} MB.`)
continue continue
} }
// verify mime type // verify mime type
if ( if (
!attachmentMeta.supportedAttachmentMimeTypes.includes('*') && !attachmentMeta.supportedAttachmentMimeTypes.includes('*') &&
@ -175,11 +188,29 @@ export default function convertCellData(
continue continue
} }
} }
// this prevent file with same names
const isFileNameAlreadyExist = oldAttachments.some((el) => el.title === (attachment?.title || attachment?.name))
if (isFileNameAlreadyExist) {
if (isMultiple) {
message.error(`File with name ${attachment?.title || attachment?.name} already attached`)
continue
} else {
throw new Error(`File with name ${attachment?.title || attachment?.name} already attached`)
}
}
attachments.push(attachment) attachments.push(attachment)
} }
return JSON.stringify(attachments) if (oldAttachments.length && !attachments.length) {
return undefined
} else if (value && attachments.length) {
return JSON.stringify([...oldAttachments, ...attachments])
} else if (files.length && attachments.length) {
return attachments
} else {
return null
}
} }
case UITypes.SingleSelect: case UITypes.SingleSelect:
case UITypes.MultiSelect: { case UITypes.MultiSelect: {

104
packages/nc-gui/composables/useMultiSelect/index.ts

@ -20,6 +20,7 @@ import {
reactive, reactive,
ref, ref,
unref, unref,
useApi,
useBase, useBase,
useCopy, useCopy,
useEventListener, useEventListener,
@ -61,6 +62,10 @@ export function useMultiSelect(
const { isMysql, isPg } = useBase() const { isMysql, isPg } = useBase()
const { base } = storeToRefs(useBase())
const { api } = useApi()
const editEnabled = ref(_editEnabled) const editEnabled = ref(_editEnabled)
const isMouseDown = ref(false) const isMouseDown = ref(false)
@ -772,7 +777,6 @@ export function useMultiSelect(
// Replace \" with " in clipboard data // Replace \" with " in clipboard data
const clipboardData = e.clipboardData?.getData('text/plain') || '' const clipboardData = e.clipboardData?.getData('text/plain') || ''
try { try {
if (clipboardData?.includes('\n') || clipboardData?.includes('\t')) { if (clipboardData?.includes('\n') || clipboardData?.includes('\t')) {
// if the clipboard data contains new line or tab, then it is a matrix or LongText // if the clipboard data contains new line or tab, then it is a matrix or LongText
@ -819,6 +823,7 @@ export function useMultiSelect(
to: pasteCol.uidt as UITypes, to: pasteCol.uidt as UITypes,
column: pasteCol, column: pasteCol,
appInfo: unref(appInfo), appInfo: unref(appInfo),
oldValue: pasteCol.uidt === UITypes.Attachment ? pasteRow.row[pasteCol.title!] : undefined,
}, },
isMysql(meta.value?.source_id), isMysql(meta.value?.source_id),
true, true,
@ -881,11 +886,20 @@ export function useMultiSelect(
to: columnObj.uidt as UITypes, to: columnObj.uidt as UITypes,
column: columnObj, column: columnObj,
appInfo: unref(appInfo), appInfo: unref(appInfo),
files: columnObj.uidt === UITypes.Attachment && e.clipboardData?.files?.length ? e.clipboardData?.files : undefined,
oldValue: rowObj.row[columnObj.title!],
}, },
isMysql(meta.value?.source_id), isMysql(meta.value?.source_id),
) )
if (pasteValue !== undefined) { if (columnObj.uidt === UITypes.Attachment && e.clipboardData?.files?.length && pasteValue?.length) {
const uploadedFiles = await handleFileUpload(pasteValue, columnObj.id!)
rowObj.row[columnObj.title!] =
Array.isArray(uploadedFiles) && uploadedFiles.length
? JSON.stringify([...handleParseAttachmentCellData(rowObj.row[columnObj.title!]), ...uploadedFiles])
: null
} else if (pasteValue !== undefined) {
rowObj.row[columnObj.title!] = pasteValue rowObj.row[columnObj.title!] = pasteValue
} }
@ -903,29 +917,60 @@ export function useMultiSelect(
const rows = unref(data).slice(startRow, endRow + 1) const rows = unref(data).slice(startRow, endRow + 1)
const props = [] const props = []
let pasteValue
const files = e.clipboardData?.files
for (const row of rows) { for (const row of rows) {
// TODO handle insert new row // TODO handle insert new row
if (!row || row.rowMeta.new) continue if (!row || row.rowMeta.new) continue
for (const col of cols) { for (const col of cols) {
if (!col.title) continue if (!col.title || !isPasteable(row, col)) {
if (!isPasteable(row, col)) {
continue continue
} }
props.push(col.title) if (files?.length) {
if (col.uidt !== UITypes.Attachment) {
continue
}
if (pasteValue === undefined) {
const fileUploadPayload = convertCellData(
{
value: '',
to: col.uidt as UITypes,
column: col,
appInfo: unref(appInfo),
files,
oldValue: row.row[col.title],
},
isMysql(meta.value?.source_id),
true,
)
if (fileUploadPayload?.length) {
const uploadedFiles = await handleFileUpload(fileUploadPayload, col.id!)
pasteValue =
Array.isArray(uploadedFiles) && uploadedFiles.length
? JSON.stringify([...handleParseAttachmentCellData(row.row[col.title]), ...uploadedFiles])
: null
}
}
} else {
pasteValue = convertCellData(
{
value: clipboardData,
to: col.uidt as UITypes,
column: col,
appInfo: unref(appInfo),
oldValue: row.row[col.title],
},
isMysql(meta.value?.source_id),
true,
)
}
const pasteValue = convertCellData( props.push(col.title)
{
value: clipboardData,
to: col.uidt as UITypes,
column: col,
appInfo: unref(appInfo),
},
isMysql(meta.value?.source_id),
true,
)
if (pasteValue !== undefined) { if (pasteValue !== undefined) {
row.row[col.title] = pasteValue row.row[col.title] = pasteValue
@ -933,6 +978,7 @@ export function useMultiSelect(
} }
} }
if (!props.length) return
await bulkUpdateRows?.(rows, props) await bulkUpdateRows?.(rows, props)
} }
} }
@ -957,6 +1003,32 @@ export function useMultiSelect(
event.preventDefault() event.preventDefault()
} }
async function handleFileUpload(files: File[], columnId: string) {
try {
const data = await api.storage.upload(
{
path: [NOCO, base.value.id, meta.value?.id, columnId].join('/'),
},
{
files,
},
)
return data
} catch (e: any) {
message.error(e.message || t('msg.error.internalError'))
}
}
function handleParseAttachmentCellData(value: string | null) {
const parsedVal = parseProp(value)
if (parsedVal && Array.isArray(parsedVal)) {
return parsedVal
} else {
return []
}
}
useEventListener(document, 'keydown', handleKeyDown) useEventListener(document, 'keydown', handleKeyDown)
useEventListener(document, 'mouseup', handleMouseUp) useEventListener(document, 'mouseup', handleMouseUp)
useEventListener(document, 'paste', handlePaste) useEventListener(document, 'paste', handlePaste)

16
packages/nocodb-sdk/src/lib/helperFunctions.ts

@ -14,13 +14,15 @@ const getSystemColumnsIds = (columns) => {
const getSystemColumns = (columns) => columns.filter(isSystemColumn) || []; const getSystemColumns = (columns) => columns.filter(isSystemColumn) || [];
const isSystemColumn = (col): boolean => const isSystemColumn = (col): boolean =>
col && !!(
(col.uidt === UITypes.ForeignKey || col &&
((col.column_name === 'created_at' || col.column_name === 'updated_at') && (col.uidt === UITypes.ForeignKey ||
col.uidt === UITypes.DateTime) || ((col.column_name === 'created_at' || col.column_name === 'updated_at') &&
(col.pk && (col.ai || col.cdf)) || col.uidt === UITypes.DateTime) ||
(col.pk && col.meta && col.meta.ag) || (col.pk && (col.ai || col.cdf)) ||
col.system); (col.pk && col.meta && col.meta.ag) ||
col.system)
);
const isSelfReferencingTableColumn = (col): boolean => { const isSelfReferencingTableColumn = (col): boolean => {
return ( return (

Loading…
Cancel
Save