mirror of https://github.com/nocodb/nocodb
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.
336 lines
8.1 KiB
336 lines
8.1 KiB
<script setup lang="ts"> |
|
import { inject, watchEffect } from '@vue/runtime-core' |
|
import type { ColumnType, TableType } from 'nocodb-sdk' |
|
import type { Ref } from 'vue' |
|
import { useToast } from 'vue-toastification' |
|
import { useNuxtApp } from '#app' |
|
import { ColumnInj, MetaInj } from '~/components' |
|
import useProject from '~/composables/useProject' |
|
// import FileSaver from "file-saver"; |
|
import { isImage } from '~/utils/fileUtils' |
|
import MaterialPlusIcon from '~icons/mdi/plus' |
|
import MaterialArrowExpandIcon from '~icons/mdi/arrow-expand' |
|
|
|
const { modelValue } = defineProps<{ modelValue: string | Array<any> | null }>() |
|
const emit = defineEmits(['update:modelValue']) |
|
|
|
const isPublicForm = inject<boolean>('isPublicForm', false) |
|
const isForm = inject<boolean>('isForm', false) |
|
const meta = inject(MetaInj) |
|
const column = inject<ColumnType>(ColumnInj) |
|
const editEnabled = inject<boolean>('editEnabled') |
|
|
|
const localFilesState = reactive([]) |
|
const attachments = ref([]) |
|
const uploading = ref(false) |
|
const fileInput = ref<HTMLInputElement>() |
|
|
|
const { $api } = useNuxtApp() |
|
const { project } = useProject() |
|
const toast = useToast() |
|
|
|
watchEffect(() => { |
|
if (modelValue) { |
|
attachments.value = ((typeof modelValue === 'string' ? JSON.parse(modelValue) : modelValue) || []).filter(Boolean) |
|
} |
|
}) |
|
|
|
const selectImage = (file: any, i) => { |
|
// todo: implement |
|
} |
|
|
|
const openUrl = (url: string, target = '_blank') => { |
|
window.open(url, target) |
|
} |
|
|
|
const addFile = () => { |
|
fileInput.value?.click() |
|
} |
|
|
|
const onFileSelection = async (e) => { |
|
// 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 |
|
// } |
|
|
|
// todo : move to com |
|
uploading.value = true |
|
const newAttachments = [] |
|
for (const file of fileInput.value?.files ?? []) { |
|
try { |
|
const data = await $api.storage.upload( |
|
{ |
|
path: ['noco', project.value.title, meta?.value?.title, column?.title].join('/'), |
|
}, |
|
{ |
|
files: file, |
|
json: '{}', |
|
}, |
|
) |
|
newAttachments.push(...data) |
|
} catch (e: any) { |
|
toast.error(e.message || 'Some internal error occurred') |
|
uploading.value = false |
|
return |
|
} |
|
} |
|
uploading.value = false |
|
emit('update:modelValue', JSON.stringify([...attachments.value, ...newAttachments])) |
|
|
|
// this.$emit('input', JSON.stringify(this.localState)) |
|
// this.$emit('update') |
|
} |
|
</script> |
|
|
|
<template> |
|
<div class="main h-100"> |
|
<div class="d-flex align-center img-container"> |
|
<div class="d-flex no-overflow"> |
|
<div |
|
v-for="(item, i) in isPublicForm ? localFilesState : attachments" |
|
:key="item.url || item.title" |
|
class="thumbnail align-center justify-center d-flex" |
|
> |
|
<!-- <v-tooltip bottom> --> |
|
<!-- <template #activator="{ on }"> --> |
|
<!-- <v-img |
|
v-if="isImage(item.title, item.mimetype)" |
|
lazy-src="https://via.placeholder.com/60.png?text=Loading..." |
|
alt="#" |
|
max-height="99px" |
|
contain |
|
:src="item.url || item.data" |
|
v-on="on" |
|
@click="selectImage(item.url || item.data, i)" |
|
> --> |
|
<img |
|
v-if="isImage(item.title, item.mimetype)" |
|
alt="#" |
|
style="max-height: 30px; max-width: 30px" |
|
:src="item.url || item.data" |
|
@click="selectImage(item.url || item.data, i)" |
|
/> |
|
<!-- <template #placeholder> --> |
|
<!-- <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> |
|
<!-- 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> |
|
|
|
<v-spacer /> |
|
|
|
<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="d-none" @change="onFileSelection" /> |
|
</div> |
|
</template> |
|
|
|
<style scoped lang="scss"> |
|
.thumbnail { |
|
height: 30px; |
|
width: 30px; |
|
margin: 2px; |
|
border-radius: 4px; |
|
|
|
img { |
|
max-height: 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); |
|
} |
|
|
|
/*.img-container { |
|
margin: 0 -2px; |
|
} |
|
|
|
.no-overflow { |
|
overflow: hidden; |
|
} |
|
|
|
.add { |
|
transition: 0.2s background-color; |
|
!*background-color: #666666ee;*! |
|
border-radius: 4px; |
|
height: 33px; |
|
margin: 5px 2px; |
|
} |
|
|
|
.add:hover { |
|
!*background-color: #66666699;*! |
|
} |
|
|
|
.thumbnail { |
|
height: 99px; |
|
width: 99px; |
|
margin: 2px; |
|
border-radius: 4px; |
|
} |
|
|
|
.thumbnail img { |
|
!*max-height: 33px;*! |
|
max-width: 99px; |
|
} |
|
|
|
.main { |
|
min-height: 20px; |
|
position: relative; |
|
height: auto; |
|
} |
|
|
|
.expand-icon { |
|
margin-left: 8px; |
|
border-radius: 2px; |
|
!*opacity: 0;*! |
|
transition: 0.3s background-color; |
|
} |
|
|
|
.expand-icon:hover { |
|
!*opacity: 1;*! |
|
background-color: var(--v-primary-lighten4); |
|
} |
|
|
|
.modal-thumbnail img { |
|
height: 50px; |
|
max-width: 100%; |
|
border-radius: 4px; |
|
} |
|
|
|
.modal-thumbnail { |
|
position: relative; |
|
margin: 10px 10px; |
|
} |
|
|
|
.remove-icon { |
|
position: absolute; |
|
top: 5px; |
|
right: 5px; |
|
} |
|
|
|
.modal-thumbnail-card { |
|
.download-icon { |
|
position: absolute; |
|
bottom: 5px; |
|
right: 5px; |
|
opacity: 0; |
|
transition: 0.4s opacity; |
|
} |
|
|
|
&:hover .download-icon { |
|
opacity: 1; |
|
} |
|
} |
|
|
|
.image-overlay-container { |
|
max-height: 100vh; |
|
overflow-y: auto; |
|
position: relative; |
|
} |
|
|
|
.image-overlay-container .close-icon { |
|
position: fixed; |
|
top: 15px; |
|
right: 15px; |
|
} |
|
|
|
.overlay-thumbnail { |
|
transition: 0.4s transform, 0.4s opacity; |
|
opacity: 0.5; |
|
} |
|
|
|
.overlay-thumbnail.active { |
|
transform: scale(1.4); |
|
opacity: 1; |
|
} |
|
|
|
.overlay-thumbnail:hover { |
|
opacity: 1; |
|
} |
|
|
|
.modal-title { |
|
text-overflow: ellipsis; |
|
white-space: nowrap; |
|
width: 100%; |
|
overflow: hidden; |
|
} |
|
|
|
.modal-thumbnail-card { |
|
transition: 0.4s transform; |
|
} |
|
|
|
.modal-thumbnail-card:hover { |
|
transform: scale(1.05); |
|
} |
|
|
|
.drop-overlay { |
|
z-index: 5; |
|
position: absolute; |
|
width: 100%; |
|
height: 100%; |
|
left: 0; |
|
right: 0; |
|
top: 0; |
|
bottom: 5px; |
|
background: #aaaaaa44; |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
pointer-events: none; |
|
} |
|
|
|
.expand-icon { |
|
opacity: 0; |
|
transition: 0.4s opacity; |
|
} |
|
|
|
.main:hover .expand-icon { |
|
opacity: 1; |
|
}*/ |
|
</style>
|
|
|