|
|
|
@ -3,22 +3,35 @@ import { notification } from 'ant-design-vue'
|
|
|
|
|
import { computed, inject, ref, useApi, useDropZone, useFileDialog, useProject, watch } from '#imports' |
|
|
|
|
import { ColumnInj, EditModeInj, MetaInj } from '~/context' |
|
|
|
|
import { NOCO } from '~/lib' |
|
|
|
|
import { isImage } from '~/utils' |
|
|
|
|
import { isImage, openLink } from '~/utils' |
|
|
|
|
import MaterialSymbolsAttachFile from '~icons/material-symbols/attach-file' |
|
|
|
|
import MaterialArrowExpandIcon from '~icons/mdi/arrow-expand' |
|
|
|
|
import MaterialSymbolsFileCopyOutline from '~icons/material-symbols/file-copy-outline' |
|
|
|
|
import MdiReload from '~icons/mdi/reload' |
|
|
|
|
import IcOutlineInsertDriveFile from '~icons/ic/outline-insert-drive-file' |
|
|
|
|
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' |
|
|
|
|
|
|
|
|
|
interface Props { |
|
|
|
|
modelValue: string | Record<string, any>[] | null |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
interface Emits { |
|
|
|
|
(event: 'update:modelValue', value: string | Record<string, any>): void |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const { modelValue } = defineProps<Props>() |
|
|
|
|
|
|
|
|
|
const emit = defineEmits(['update:modelValue']) |
|
|
|
|
const emits = defineEmits<Emits>() |
|
|
|
|
|
|
|
|
|
const isPublicForm = inject('isPublicForm', false) |
|
|
|
|
|
|
|
|
|
const isPublicForm = inject<boolean>('isPublicForm', false) |
|
|
|
|
const isForm = inject('isForm', false) |
|
|
|
|
|
|
|
|
|
const isForm = inject<boolean>('isForm', false) |
|
|
|
|
// todo: replace placeholder var |
|
|
|
|
const isPublicGrid = $ref(false) |
|
|
|
|
|
|
|
|
|
const meta = inject(MetaInj)! |
|
|
|
|
|
|
|
|
@ -26,13 +39,13 @@ const column = inject(ColumnInj)!
|
|
|
|
|
|
|
|
|
|
const editEnabled = inject(EditModeInj, ref(false)) |
|
|
|
|
|
|
|
|
|
const attachments = ref([]) |
|
|
|
|
const storedFiles = ref<{ title: string; file: File }[]>([]) |
|
|
|
|
|
|
|
|
|
const uploading = ref(false) |
|
|
|
|
const attachments = ref<File[]>([]) |
|
|
|
|
|
|
|
|
|
const dropZoneRef = ref<HTMLDivElement>() |
|
|
|
|
|
|
|
|
|
const { api } = useApi() |
|
|
|
|
const { api, isLoading } = useApi() |
|
|
|
|
|
|
|
|
|
const { project } = useProject() |
|
|
|
|
|
|
|
|
@ -47,12 +60,13 @@ watch(
|
|
|
|
|
attachments.value = ((typeof nextModel === 'string' ? JSON.parse(nextModel) : nextModel) || []).filter(Boolean) |
|
|
|
|
} |
|
|
|
|
}, |
|
|
|
|
{ immediate: true }, |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
function onDrop(droppedFiles: File[] | null) { |
|
|
|
|
if (droppedFiles) { |
|
|
|
|
// set files |
|
|
|
|
console.log(droppedFiles) |
|
|
|
|
onFileSelection(droppedFiles) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -60,42 +74,30 @@ const selectImage = (file: any, i: unknown) => {
|
|
|
|
|
// todo: implement |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const openUrl = (url: string, target = '_blank') => { |
|
|
|
|
window.open(url, target) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const onFileSelection = async (e: unknown) => { |
|
|
|
|
if (!files.value) return |
|
|
|
|
|
|
|
|
|
// if (this.isPublicGrid) { |
|
|
|
|
// return |
|
|
|
|
// } |
|
|
|
|
// if (!this.$refs.file.files || !this.$refs.file.files.length) { |
|
|
|
|
// return |
|
|
|
|
// } |
|
|
|
|
|
|
|
|
|
// if (this.isPublicForm) { |
|
|
|
|
// this.localFilesState.push(...Array.from(this.$refs.file.files).map((file) => { |
|
|
|
|
// const res = { file, title: file.name } |
|
|
|
|
// if (isImage(file.name, file.mimetype)) { |
|
|
|
|
// const reader = new FileReader() |
|
|
|
|
// reader.onload = (e) => { |
|
|
|
|
// this.$set(res, 'data', e.target.result) |
|
|
|
|
// } |
|
|
|
|
// reader.readAsDataURL(file) |
|
|
|
|
// } |
|
|
|
|
// return res |
|
|
|
|
// })) |
|
|
|
|
// |
|
|
|
|
// this.$emit('input', this.localFilesState.map(f => f.file)) |
|
|
|
|
// return |
|
|
|
|
// } |
|
|
|
|
|
|
|
|
|
uploading.value = true |
|
|
|
|
async function onFileSelection(selectedFiles: FileList | File[]) { |
|
|
|
|
if (!selectedFiles.length || isPublicGrid) return |
|
|
|
|
|
|
|
|
|
if (isPublicForm) { |
|
|
|
|
storedFiles.value.push( |
|
|
|
|
...Array.from(selectedFiles).map((file) => { |
|
|
|
|
const res = { file, title: file.name } |
|
|
|
|
if (isImage(file.name, (file as any).mimetype)) { |
|
|
|
|
const reader = new FileReader() |
|
|
|
|
reader.readAsDataURL(file) |
|
|
|
|
} |
|
|
|
|
return res |
|
|
|
|
}), |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
emits( |
|
|
|
|
'update:modelValue', |
|
|
|
|
storedFiles.value.map((f) => f.file), |
|
|
|
|
) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const newAttachments = [] |
|
|
|
|
|
|
|
|
|
for (const file of files.value) { |
|
|
|
|
for (const file of selectedFiles) { |
|
|
|
|
try { |
|
|
|
|
const data = await api.storage.upload( |
|
|
|
|
{ |
|
|
|
@ -115,74 +117,91 @@ const onFileSelection = async (e: unknown) => {
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
uploading.value = false |
|
|
|
|
|
|
|
|
|
emit('update:modelValue', JSON.stringify([...attachments.value, ...newAttachments])) |
|
|
|
|
emits('update:modelValue', JSON.stringify([...attachments.value, ...newAttachments])) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
watch(files, console.log) |
|
|
|
|
|
|
|
|
|
const items = computed(() => (isPublicForm ? files.value : attachments.value) || []) |
|
|
|
|
watch(files, (nextFiles) => nextFiles && onFileSelection(nextFiles)) |
|
|
|
|
|
|
|
|
|
const items = computed(() => (isPublicForm ? storedFiles.value : attachments.value) || []) |
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
</script> |
|
|
|
|
|
|
|
|
|
<template> |
|
|
|
|
<div class="flex items-center"> |
|
|
|
|
<div ref="dropZoneRef" class="flex-1 group color-transition flex items-center p-1 hover:text-primary"> |
|
|
|
|
<template v-if="isOverDropZone"> |
|
|
|
|
<div ref="dropZoneRef" class="flex-1 color-transition flex items-center justify-between gap-1"> |
|
|
|
|
<template v-if="isOverDropZone"> |
|
|
|
|
<div |
|
|
|
|
class="w-full h-full flex items-center justify-center p-1 rounded gap-1 bg-gradient-to-t from-primary/10 via-primary/25 to-primary/10 !text-primary" |
|
|
|
|
> |
|
|
|
|
<MaterialSymbolsFileCopyOutline class="text-pink-500" /> Drop here |
|
|
|
|
</div> |
|
|
|
|
</template> |
|
|
|
|
<template v-else> |
|
|
|
|
<div |
|
|
|
|
:class="{ 'mx-auto px-4': !items.length }" |
|
|
|
|
class="group flex gap-1 items-center active:ring rounded border-1 p-1 hover:bg-primary/10" |
|
|
|
|
@click.stop="open" |
|
|
|
|
> |
|
|
|
|
<MdiReload v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" /> |
|
|
|
|
|
|
|
|
|
<a-tooltip v-else placement="bottom"> |
|
|
|
|
<template #title> Click or drop a file into cell </template> |
|
|
|
|
|
|
|
|
|
<div class="flex items-center gap-2"> |
|
|
|
|
<MaterialSymbolsAttachFile class="transform group-hover:(text-pink-500 scale-120)" /> |
|
|
|
|
|
|
|
|
|
<div v-if="!items.length" class="group-hover:text-primary">Add file(s)</div> |
|
|
|
|
</div> |
|
|
|
|
</a-tooltip> |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
<div class="h-full w-full flex flex-wrap flex-col overflow-x-scroll overflow-y-hidden gap-2 scrollbar-thin-primary py-1"> |
|
|
|
|
<div |
|
|
|
|
class="w-full h-full flex items-center justify-center p-1 rounded gap-1 bg-gradient-to-t from-primary/10 via-primary/25 to-primary/10 !text-primary" |
|
|
|
|
v-for="(item, i) of items" |
|
|
|
|
:key="item.url || item.title" |
|
|
|
|
class="flex-auto flex items-center justify-center w-[45px] border-1" |
|
|
|
|
> |
|
|
|
|
<MaterialSymbolsFileCopyOutline class="text-pink-500" /> Drop here |
|
|
|
|
</div> |
|
|
|
|
</template> |
|
|
|
|
<template v-else> |
|
|
|
|
<div class="flex overflow-hidden"> |
|
|
|
|
<div v-for="(item, i) of items" :key="item.url || item.title" class="thumbnail align-center justify-center d-flex"> |
|
|
|
|
<a-tooltip placement="bottom"> |
|
|
|
|
<template #title> |
|
|
|
|
<div class="text-center w-full">{{ item.title }}</div> |
|
|
|
|
</template> |
|
|
|
|
|
|
|
|
|
<img |
|
|
|
|
v-if="isImage(item.title, item.mimetype)" |
|
|
|
|
alt="#" |
|
|
|
|
style="max-height: 30px; max-width: 30px" |
|
|
|
|
:alt="item.title || `#${i}`" |
|
|
|
|
:src="item.url || item.data" |
|
|
|
|
@click="selectImage(item.url || item.data, i)" |
|
|
|
|
/> |
|
|
|
|
<v-icon v-else-if="item.icon" :size="active ? 33 : 22" v-on="on" @click="openUrl(item.url || item.data, '_blank')"> |
|
|
|
|
{{ item.icon }} |
|
|
|
|
</v-icon> |
|
|
|
|
<v-icon v-else :size="active ? 33 : 22" v-on="on" @click="openUrl(item.url || item.data, '_blank')"> |
|
|
|
|
mdi-file |
|
|
|
|
</v-icon> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
<!-- todo: hide or toggle based on ancestor --> |
|
|
|
|
<div class="mx-auto flex gap-1 items-center active:ring rounded border-1 py-1 px-4" @click.stop="open"> |
|
|
|
|
<v-icon v-if="uploading" small color="primary" class="nc-attachment-add-spinner"> mdi-loading mdi-spin</v-icon> |
|
|
|
|
<component :is="FileIcon(item.icon)" v-else-if="item.icon" @click="openLink(item.url || item.data)" /> |
|
|
|
|
|
|
|
|
|
<a-tooltip placement="bottom"> |
|
|
|
|
<template #title> Click or drop a file into cell </template> |
|
|
|
|
<div class="flex items-center gap-2"> |
|
|
|
|
<MaterialSymbolsAttachFile class="transform group-hover:(text-pink-500 scale-120)" /> |
|
|
|
|
<div v-if="!items.length">Add file(s)</div> |
|
|
|
|
</div> |
|
|
|
|
<IcOutlineInsertDriveFile v-else @click.stop="openLink(item.url || item.data)" /> |
|
|
|
|
</a-tooltip> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
<MaterialArrowExpandIcon v-if="items.length" @click.stop /> |
|
|
|
|
</template> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
</template> |
|
|
|
|
<div v-if="items.length" class="group flex gap-1 items-center active:ring rounded border-1 p-1 hover:bg-primary/10"> |
|
|
|
|
<MdiReload v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" /> |
|
|
|
|
|
|
|
|
|
<style scoped lang="scss"> |
|
|
|
|
.thumbnail { |
|
|
|
|
height: 30px; |
|
|
|
|
width: 30px; |
|
|
|
|
margin: 2px; |
|
|
|
|
border-radius: 4px; |
|
|
|
|
<a-tooltip v-else placement="bottom"> |
|
|
|
|
<template #title> View attachments </template> |
|
|
|
|
|
|
|
|
|
img { |
|
|
|
|
max-height: 33px; |
|
|
|
|
max-width: 33px; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
</style> |
|
|
|
|
<MaterialArrowExpandIcon class="transform group-hover:(text-pink-500 scale-120)" @click.stop /> |
|
|
|
|
</a-tooltip> |
|
|
|
|
</div> |
|
|
|
|
</template> |
|
|
|
|
</div> |
|
|
|
|
</template> |
|
|
|
|