|
|
@ -1,15 +1,15 @@ |
|
|
|
<script setup lang="ts"> |
|
|
|
<script setup lang="ts"> |
|
|
|
import { useToast } from 'vue-toastification' |
|
|
|
import { notification } from 'ant-design-vue' |
|
|
|
import { inject, reactive, ref, useProject, watchEffect } from '#imports' |
|
|
|
import { computed, inject, ref, useApi, useDropZone, useFileDialog, useProject, watch } from '#imports' |
|
|
|
import { useNuxtApp } from '#app' |
|
|
|
|
|
|
|
import { ColumnInj, EditModeInj, MetaInj } from '~/context' |
|
|
|
import { ColumnInj, EditModeInj, MetaInj } from '~/context' |
|
|
|
import { NOCO } from '~/lib' |
|
|
|
import { NOCO } from '~/lib' |
|
|
|
import { isImage } from '~/utils' |
|
|
|
import { isImage } from '~/utils' |
|
|
|
import MaterialPlusIcon from '~icons/mdi/plus' |
|
|
|
import MaterialSymbolsAttachFile from '~icons/material-symbols/attach-file' |
|
|
|
import MaterialArrowExpandIcon from '~icons/mdi/arrow-expand' |
|
|
|
import MaterialArrowExpandIcon from '~icons/mdi/arrow-expand' |
|
|
|
|
|
|
|
import MaterialSymbolsFileCopyOutline from '~icons/material-symbols/file-copy-outline' |
|
|
|
|
|
|
|
|
|
|
|
interface Props { |
|
|
|
interface Props { |
|
|
|
modelValue: string | any[] | null |
|
|
|
modelValue: string | Record<string, any>[] | null |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const { modelValue } = defineProps<Props>() |
|
|
|
const { modelValue } = defineProps<Props>() |
|
|
@ -17,27 +17,44 @@ const { modelValue } = defineProps<Props>() |
|
|
|
const emit = defineEmits(['update:modelValue']) |
|
|
|
const emit = defineEmits(['update:modelValue']) |
|
|
|
|
|
|
|
|
|
|
|
const isPublicForm = inject<boolean>('isPublicForm', false) |
|
|
|
const isPublicForm = inject<boolean>('isPublicForm', false) |
|
|
|
|
|
|
|
|
|
|
|
const isForm = inject<boolean>('isForm', false) |
|
|
|
const isForm = inject<boolean>('isForm', false) |
|
|
|
const meta = inject(MetaInj) |
|
|
|
|
|
|
|
const column = inject(ColumnInj) |
|
|
|
const meta = inject(MetaInj)! |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const column = inject(ColumnInj)! |
|
|
|
|
|
|
|
|
|
|
|
const editEnabled = inject(EditModeInj, ref(false)) |
|
|
|
const editEnabled = inject(EditModeInj, ref(false)) |
|
|
|
|
|
|
|
|
|
|
|
const localFilesState = reactive([]) |
|
|
|
|
|
|
|
const attachments = ref([]) |
|
|
|
const attachments = ref([]) |
|
|
|
|
|
|
|
|
|
|
|
const uploading = ref(false) |
|
|
|
const uploading = ref(false) |
|
|
|
const fileInput = ref<HTMLInputElement>() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const { $api } = useNuxtApp() |
|
|
|
const dropZoneRef = ref<HTMLDivElement>() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const { api } = useApi() |
|
|
|
|
|
|
|
|
|
|
|
const { project } = useProject() |
|
|
|
const { project } = useProject() |
|
|
|
|
|
|
|
|
|
|
|
const toast = useToast() |
|
|
|
const { files, open, reset } = useFileDialog() |
|
|
|
|
|
|
|
|
|
|
|
watchEffect(() => { |
|
|
|
const { isOverDropZone } = useDropZone(dropZoneRef, onDrop) |
|
|
|
if (modelValue) { |
|
|
|
|
|
|
|
attachments.value = ((typeof modelValue === 'string' ? JSON.parse(modelValue) : modelValue) || []).filter(Boolean) |
|
|
|
watch( |
|
|
|
|
|
|
|
() => modelValue, |
|
|
|
|
|
|
|
(nextModel) => { |
|
|
|
|
|
|
|
if (nextModel) { |
|
|
|
|
|
|
|
attachments.value = ((typeof nextModel === 'string' ? JSON.parse(nextModel) : nextModel) || []).filter(Boolean) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function onDrop(droppedFiles: File[] | null) { |
|
|
|
|
|
|
|
if (droppedFiles) { |
|
|
|
|
|
|
|
// set files |
|
|
|
|
|
|
|
console.log(droppedFiles) |
|
|
|
} |
|
|
|
} |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const selectImage = (file: any, i: unknown) => { |
|
|
|
const selectImage = (file: any, i: unknown) => { |
|
|
|
// todo: implement |
|
|
|
// todo: implement |
|
|
@ -47,11 +64,9 @@ const openUrl = (url: string, target = '_blank') => { |
|
|
|
window.open(url, target) |
|
|
|
window.open(url, target) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const addFile = () => { |
|
|
|
|
|
|
|
fileInput.value?.click() |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const onFileSelection = async (e: unknown) => { |
|
|
|
const onFileSelection = async (e: unknown) => { |
|
|
|
|
|
|
|
if (!files.value) return |
|
|
|
|
|
|
|
|
|
|
|
// if (this.isPublicGrid) { |
|
|
|
// if (this.isPublicGrid) { |
|
|
|
// return |
|
|
|
// return |
|
|
|
// } |
|
|
|
// } |
|
|
@ -76,90 +91,85 @@ const onFileSelection = async (e: unknown) => { |
|
|
|
// return |
|
|
|
// return |
|
|
|
// } |
|
|
|
// } |
|
|
|
|
|
|
|
|
|
|
|
// todo : move to com |
|
|
|
|
|
|
|
uploading.value = true |
|
|
|
uploading.value = true |
|
|
|
|
|
|
|
|
|
|
|
const newAttachments = [] |
|
|
|
const newAttachments = [] |
|
|
|
for (const file of fileInput.value?.files ?? []) { |
|
|
|
|
|
|
|
|
|
|
|
for (const file of files.value) { |
|
|
|
try { |
|
|
|
try { |
|
|
|
const data = await $api.storage.upload( |
|
|
|
const data = await api.storage.upload( |
|
|
|
{ |
|
|
|
{ |
|
|
|
path: [NOCO, project.value.title, meta?.value?.title, column?.title].join('/'), |
|
|
|
path: [NOCO, project.value.title, meta.value.title, column.title].join('/'), |
|
|
|
}, |
|
|
|
}, |
|
|
|
{ |
|
|
|
{ |
|
|
|
files: file, |
|
|
|
files: file, |
|
|
|
json: '{}', |
|
|
|
json: '{}', |
|
|
|
}, |
|
|
|
}, |
|
|
|
) |
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
newAttachments.push(...data) |
|
|
|
newAttachments.push(...data) |
|
|
|
} catch (e: any) { |
|
|
|
} catch (e: any) { |
|
|
|
toast.error(e.message || 'Some internal error occurred') |
|
|
|
notification.error({ |
|
|
|
uploading.value = false |
|
|
|
message: e.message || 'Some internal error occurred', |
|
|
|
return |
|
|
|
}) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
uploading.value = false |
|
|
|
uploading.value = false |
|
|
|
|
|
|
|
|
|
|
|
emit('update:modelValue', JSON.stringify([...attachments.value, ...newAttachments])) |
|
|
|
emit('update:modelValue', JSON.stringify([...attachments.value, ...newAttachments])) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
watch(files, console.log) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const items = computed(() => (isPublicForm ? files.value : attachments.value) || []) |
|
|
|
</script> |
|
|
|
</script> |
|
|
|
|
|
|
|
|
|
|
|
<template> |
|
|
|
<template> |
|
|
|
<div class="h-full w-full"> |
|
|
|
<div class="flex items-center"> |
|
|
|
<div class="flex items-center img-container"> |
|
|
|
<div ref="dropZoneRef" class="flex-1 group color-transition flex items-center p-1 hover:text-primary"> |
|
|
|
<div class="d-flex no-overflow"> |
|
|
|
<template v-if="isOverDropZone"> |
|
|
|
<div |
|
|
|
<div |
|
|
|
v-for="(item, i) in isPublicForm ? localFilesState : attachments" |
|
|
|
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" |
|
|
|
:key="item.url || item.title" |
|
|
|
|
|
|
|
class="thumbnail align-center justify-center d-flex" |
|
|
|
|
|
|
|
> |
|
|
|
> |
|
|
|
<!-- <v-tooltip bottom> --> |
|
|
|
<MaterialSymbolsFileCopyOutline class="text-pink-500" /> Drop here |
|
|
|
<!-- <template #activator="{ on }"> --> |
|
|
|
</div> |
|
|
|
<!-- <v-img |
|
|
|
</template> |
|
|
|
v-if="isImage(item.title, item.mimetype)" |
|
|
|
<template v-else> |
|
|
|
lazy-src="https://via.placeholder.com/60.png?text=Loading..." |
|
|
|
<div class="flex overflow-hidden"> |
|
|
|
alt="#" |
|
|
|
<div v-for="(item, i) of items" :key="item.url || item.title" class="thumbnail align-center justify-center d-flex"> |
|
|
|
max-height="99px" |
|
|
|
<img |
|
|
|
contain |
|
|
|
v-if="isImage(item.title, item.mimetype)" |
|
|
|
:src="item.url || item.data" |
|
|
|
alt="#" |
|
|
|
v-on="on" |
|
|
|
style="max-height: 30px; max-width: 30px" |
|
|
|
@click="selectImage(item.url || item.data, i)" |
|
|
|
:src="item.url || item.data" |
|
|
|
> --> |
|
|
|
@click="selectImage(item.url || item.data, i)" |
|
|
|
<img |
|
|
|
/> |
|
|
|
v-if="isImage(item.title, item.mimetype)" |
|
|
|
<v-icon v-else-if="item.icon" :size="active ? 33 : 22" v-on="on" @click="openUrl(item.url || item.data, '_blank')"> |
|
|
|
alt="#" |
|
|
|
{{ item.icon }} |
|
|
|
style="max-height: 30px; max-width: 30px" |
|
|
|
</v-icon> |
|
|
|
:src="item.url || item.data" |
|
|
|
<v-icon v-else :size="active ? 33 : 22" v-on="on" @click="openUrl(item.url || item.data, '_blank')"> |
|
|
|
@click="selectImage(item.url || item.data, i)" |
|
|
|
mdi-file |
|
|
|
/> |
|
|
|
</v-icon> |
|
|
|
<!-- <template #placeholder> --> |
|
|
|
</div> |
|
|
|
<!-- <v-skeleton-loader type="image" :height="active ? 33 : 22" :width="active ? 33 : 22" /> --> |
|
|
|
|
|
|
|
<!-- </template> --> |
|
|
|
|
|
|
|
<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> |
|
|
|
|
|
|
|
<!-- </template> --> |
|
|
|
|
|
|
|
<!-- <span>{{ item.title }}</span> --> |
|
|
|
|
|
|
|
<!-- </v-tooltip> --> |
|
|
|
|
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- todo: hide or toggle based on ancestor --> |
|
|
|
|
|
|
|
<div class="add d-flex align-center justify-center px-1 nc-attachment-add" @click="addFile"> |
|
|
|
|
|
|
|
<v-icon v-if="uploading" small color="primary" class="nc-attachment-add-spinner"> mdi-loading mdi-spin</v-icon> |
|
|
|
|
|
|
|
<!-- <v-btn v-else-if="isForm" outlined x-small color="" text class="nc-attachment-add-btn"> |
|
|
|
|
|
|
|
<v-icon x-small color="" icon="MaterialPlusIcon"> mdi-plus </v-icon> |
|
|
|
|
|
|
|
Attachment |
|
|
|
|
|
|
|
</v-btn> |
|
|
|
|
|
|
|
<v-icon small color="primary nc-attachment-add-icon"> |
|
|
|
|
|
|
|
mdi-plus |
|
|
|
|
|
|
|
</v-icon> --> |
|
|
|
|
|
|
|
<MaterialPlusIcon /> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<MaterialArrowExpandIcon @click.stop="dialog = true" /> |
|
|
|
|
|
|
|
<!-- <v-icon class="expand-icon mr-1" x-small color="primary" @click.stop="dialog = true"> mdi-arrow-expand </v-icon> --> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<input ref="fileInput" type="file" multiple class="hidden" @change="onFileSelection" /> |
|
|
|
<!-- 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> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<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> |
|
|
|
|
|
|
|
</a-tooltip> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<MaterialArrowExpandIcon v-if="items.length" @click.stop /> |
|
|
|
|
|
|
|
</template> |
|
|
|
|
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</template> |
|
|
|
</template> |
|
|
|
|
|
|
|
|
|
|
@ -175,14 +185,4 @@ const onFileSelection = async (e: unknown) => { |
|
|
|
max-width: 33px; |
|
|
|
max-width: 33px; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.expand-icon { |
|
|
|
|
|
|
|
margin-left: 8px; |
|
|
|
|
|
|
|
border-radius: 2px; |
|
|
|
|
|
|
|
transition: 0.3s background-color; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.expand-icon:hover { |
|
|
|
|
|
|
|
background-color: var(--v-primary-lighten4); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
</style> |
|
|
|
</style> |
|
|
|