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.
600 lines
16 KiB
600 lines
16 KiB
2 years ago
|
<template>
|
||
|
<div
|
||
|
class="main h-100"
|
||
|
@dragover.prevent="dragOver = true"
|
||
|
@dragenter.prevent="dragOver = true"
|
||
|
@dragexit="dragOver = false"
|
||
|
@dragleave="dragOver = false"
|
||
|
@dragend="dragOver = false"
|
||
|
@drop.prevent.stop="onFileDrop"
|
||
|
>
|
||
|
<div v-show="(isForm || _isUIAllowed('tableAttachment')) && dragOver" class="drop-overlay">
|
||
|
<div>
|
||
|
<v-icon small>
|
||
|
mdi-cloud-upload-outline
|
||
|
</v-icon>
|
||
|
<span class="caption font-weight-bold">Drop here</span>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<div class="d-flex align-center img-container">
|
||
|
<div class="d-flex no-overflow">
|
||
|
<div
|
||
|
v-for="(item,i) in (isPublicForm ? localFilesState : localState)"
|
||
|
: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)"
|
||
|
>
|
||
|
<template #placeholder>
|
||
|
<v-skeleton-loader
|
||
|
type="image"
|
||
|
:height="active ? 33 : 22"
|
||
|
:width="active ? 33 : 22"
|
||
|
/>
|
||
|
</template>
|
||
|
</v-img>
|
||
|
<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
|
||
|
v-if="isForm || active && !isPublicGrid && !isLocked"
|
||
|
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="">
|
||
|
mdi-plus
|
||
|
</v-icon>
|
||
|
Attachment
|
||
|
</v-btn>
|
||
|
<v-icon
|
||
|
v-else-if="_isUIAllowed('tableAttachment')"
|
||
|
v-show="active"
|
||
|
small
|
||
|
color="primary nc-attachment-add-icon"
|
||
|
>
|
||
|
mdi-plus
|
||
|
</v-icon>
|
||
|
</div>
|
||
|
|
||
|
<v-spacer />
|
||
|
|
||
|
<v-icon class="expand-icon mr-1" x-small color="primary" @click.stop="dialog = true">
|
||
|
mdi-arrow-expand
|
||
|
</v-icon>
|
||
|
<input ref="file" type="file" multiple class="d-none" @change="onFileSelection">
|
||
|
</div>
|
||
|
<v-dialog
|
||
|
v-if="dialog"
|
||
|
v-model="dialog"
|
||
|
width="800"
|
||
|
>
|
||
|
<v-card class="h-100 images-modal">
|
||
|
<v-card-text class="h-100 backgroundColor">
|
||
|
<div class="d-flex mx-2">
|
||
|
<v-btn
|
||
|
v-if="(isForm || _isUIAllowed('tableAttachment')) && !isPublicGrid && !isLocked"
|
||
|
small
|
||
|
class="my-4 "
|
||
|
:loading="uploading"
|
||
|
@click="addFile"
|
||
|
>
|
||
|
<v-icon small class="mr-2">
|
||
|
mdi-link-variant
|
||
|
</v-icon>
|
||
|
<span class="caption">Attach File</span>
|
||
|
</v-btn>
|
||
|
|
||
|
<!-- <v-text-field v-model="urlString" @keypress.enter="uploadByUrl" />-->
|
||
|
</div>
|
||
|
|
||
|
<div class="d-flex flex-wrap h-100">
|
||
|
<v-container fluid style="max-height:calc(90vh - 80px);overflow-y: auto">
|
||
|
<draggable
|
||
|
v-model="localState"
|
||
|
class="row"
|
||
|
@update="onOrderUpdate"
|
||
|
>
|
||
|
<v-col v-for="(item,i) in (isPublicForm ? localFilesState : localState)" :key="i" cols="4">
|
||
|
<v-card
|
||
|
class="modal-thumbnail-card align-center justify-center d-flex"
|
||
|
height="200px"
|
||
|
style="position: relative"
|
||
|
>
|
||
|
<v-icon
|
||
|
v-if="_isUIAllowed('tableAttachment') && !isPublicGrid && !isLocked"
|
||
|
small
|
||
|
class="remove-icon"
|
||
|
@click="removeItem(i)"
|
||
|
>
|
||
|
mdi-close-circle
|
||
|
</v-icon>
|
||
|
<v-icon color="grey" class="download-icon" @click.stop="downloadItem(item,i)">
|
||
|
mdi-download
|
||
|
</v-icon>
|
||
|
<div class="pa-2 d-flex align-center" style="height:200px">
|
||
|
<img
|
||
|
v-if="isImage(item.title, item.mimetype)"
|
||
|
style="max-height: 100%;max-width: 100%"
|
||
|
alt="#"
|
||
|
:src="item.url || item.data"
|
||
|
@click="selectImage(item.url,i)"
|
||
|
>
|
||
|
|
||
|
<v-icon v-else-if="item.icon" size="33" @click="openUrl(item.url || item.data,'_blank')">
|
||
|
{{
|
||
|
item.icon
|
||
|
}}
|
||
|
</v-icon>
|
||
|
<v-icon v-else size="33" @click="openUrl(item.url || item.data,'_blank')">
|
||
|
mdi-file
|
||
|
</v-icon>
|
||
|
</div>
|
||
|
</v-card>
|
||
|
<p class="caption mt-2 modal-title" :title="item.title">
|
||
|
{{ item.title }}
|
||
|
</p>
|
||
|
</v-col>
|
||
|
</draggable>
|
||
|
</v-container>
|
||
|
</div>
|
||
|
</v-card-text>
|
||
|
</v-card>
|
||
|
</v-dialog>
|
||
|
|
||
|
<v-overlay v-if="showImage" v-model="showImage" z-index="99999" opacity=".93">
|
||
|
<div v-click-outside="hideIfVisible" class="image-overlay-container">
|
||
|
<template v-if="showImage && selectedImage">
|
||
|
<v-carousel v-model="carousel" height="calc(100vh - 100px)" hide-delimiters>
|
||
|
<v-carousel-item
|
||
|
v-for="(item,i) in (isPublicForm ? localFilesState : localState)"
|
||
|
:key="i"
|
||
|
>
|
||
|
<div class="mx-auto d-flex flex-column justify-center align-center" style="min-height:100px">
|
||
|
<p class="title text-center">
|
||
|
{{ item.title }}
|
||
|
<v-icon class="ml-3" color="grey" @click.stop="downloadItem(item,i)">
|
||
|
mdi-download
|
||
|
</v-icon>
|
||
|
</p>
|
||
|
<div style="width:90vh;height:calc(100vh - 150px)" class="d-flex align-center justify-center">
|
||
|
<img
|
||
|
v-if="isImage(item.title, item.mimetype)"
|
||
|
style="max-width:90vh;max-height:calc(100vh - 100px)"
|
||
|
:src="item.url || item.data"
|
||
|
>
|
||
|
<v-icon v-else-if="item.icon" size="55">
|
||
|
{{ item.icon }}
|
||
|
</v-icon>
|
||
|
<v-icon v-else size="55">
|
||
|
mdi-file
|
||
|
</v-icon>
|
||
|
</div>
|
||
|
</div>
|
||
|
</v-carousel-item>
|
||
|
</v-carousel>
|
||
|
</template>
|
||
|
<v-sheet
|
||
|
v-if="showImage"
|
||
|
class="mx-auto align-center justify-center"
|
||
|
max-width="90vw"
|
||
|
height="80px"
|
||
|
style="background: transparent"
|
||
|
>
|
||
|
<v-slide-group
|
||
|
multiple
|
||
|
show-arrows
|
||
|
>
|
||
|
<v-slide-item
|
||
|
v-for="(item,i) in (isPublicForm ? localFilesState : localState)"
|
||
|
:key="i"
|
||
|
>
|
||
|
<v-card
|
||
|
:key="i"
|
||
|
class="ma-2 pa-2 d-flex align-center justify-center overlay-thumbnail"
|
||
|
:class="{active: carousel === i}"
|
||
|
width="48"
|
||
|
height="48"
|
||
|
@click="carousel = i"
|
||
|
>
|
||
|
<img
|
||
|
v-if="isImage(item.title, item.mimetype)"
|
||
|
style="max-width:100%;max-height:100%"
|
||
|
:src="item.url || item.data"
|
||
|
>
|
||
|
<v-icon v-else-if="item.icon" size="48">
|
||
|
{{ item.icon }}
|
||
|
</v-icon>
|
||
|
<v-icon v-else size="48">
|
||
|
mdi-file
|
||
|
</v-icon>
|
||
|
</v-card>
|
||
|
</v-slide-item>
|
||
|
</v-slide-group>
|
||
|
</v-sheet>
|
||
|
<v-icon x-large class="close-icon" @click="showImage=false">
|
||
|
mdi-close-circle
|
||
|
</v-icon>
|
||
|
</div>
|
||
|
</v-overlay>
|
||
|
</div>
|
||
|
</template>
|
||
|
|
||
|
<script>
|
||
|
import FileSaver from 'file-saver'
|
||
|
import draggable from 'vuedraggable'
|
||
|
import { isImage } from '@/components/project/spreadsheet/helpers/imageExt'
|
||
|
|
||
|
export default {
|
||
|
name: 'EditableAttachmentCell',
|
||
|
components: { draggable },
|
||
|
props: ['dbAlias', 'value', 'active', 'isLocked', 'meta', 'column', 'isPublicGrid', 'isForm', 'isPublicForm', 'viewId'],
|
||
|
data: () => ({
|
||
|
carousel: null,
|
||
|
uploading: false,
|
||
|
localState: '',
|
||
|
dialog: false,
|
||
|
showImage: false,
|
||
|
selectedImage: null,
|
||
|
dragOver: false,
|
||
|
localFilesState: [],
|
||
|
urlString: ''
|
||
|
}),
|
||
|
watch: {
|
||
|
value(val, prev) {
|
||
|
try {
|
||
|
this.localState = ((typeof val === 'string' && val !== prev ? JSON.parse(val) : val) || []).filter(Boolean)
|
||
|
} catch (e) {
|
||
|
this.localState = []
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
created() {
|
||
|
try {
|
||
|
this.localState = ((typeof this.value === 'string' ? JSON.parse(this.value) : this.value) || []).filter(Boolean)
|
||
|
} catch (e) {
|
||
|
this.localState = []
|
||
|
}
|
||
|
document.addEventListener('keydown', this.onArrowDown)
|
||
|
},
|
||
|
beforeDestroy() {
|
||
|
document.removeEventListener('keydown', this.onArrowDown)
|
||
|
},
|
||
|
mounted() {
|
||
|
},
|
||
|
methods: {
|
||
|
async uploadByUrl() {
|
||
|
const data = await this.$api.storage.uploadByUrl(
|
||
|
{
|
||
|
path: ['noco', this.projectName, this.meta.title, this.column.title].join('/')
|
||
|
},
|
||
|
[{
|
||
|
url: this.urlString
|
||
|
}]
|
||
|
)
|
||
|
|
||
|
this.localState.push(...data)
|
||
|
},
|
||
|
openUrl(url, target) {
|
||
|
window.open(url, target)
|
||
|
},
|
||
|
isImage,
|
||
|
hideIfVisible() {
|
||
|
if (this.showImage) {
|
||
|
this.showImage = false
|
||
|
}
|
||
|
},
|
||
|
selectImage(selectedImage, i) {
|
||
|
this.carousel = i
|
||
|
this.selectedImage = selectedImage
|
||
|
this.showImage = true
|
||
|
},
|
||
|
addFile() {
|
||
|
if (!this.isLocked) {
|
||
|
this.$refs.file.click()
|
||
|
}
|
||
|
},
|
||
|
async onFileSelection() {
|
||
|
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
|
||
|
}
|
||
|
|
||
|
this.uploading = true
|
||
|
for (const file of this.$refs.file.files) {
|
||
|
try {
|
||
|
const data = await this.$api.storage.upload(
|
||
|
{
|
||
|
path: ['noco', this.projectName, this.meta.title, this.column.title].join('/')
|
||
|
}, {
|
||
|
files: file,
|
||
|
json: '{}'
|
||
|
}
|
||
|
)
|
||
|
|
||
|
this.localState.push(...data)
|
||
|
} catch (e) {
|
||
|
this.$toast.error((e.message) || 'Some internal error occurred').goAway(3000)
|
||
|
this.uploading = false
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
|
||
|
this.uploading = false
|
||
|
this.$emit('input', JSON.stringify(this.localState))
|
||
|
this.$emit('update')
|
||
|
},
|
||
|
onOrderUpdate() {
|
||
|
this.$emit('input', JSON.stringify(this.localState))
|
||
|
this.$emit('update')
|
||
|
},
|
||
|
removeItem(i) {
|
||
|
if (this.isPublicForm) {
|
||
|
this.localFilesState.splice(i, 1)
|
||
|
this.$emit('input', this.localFilesState.map(f => f.file))
|
||
|
} else {
|
||
|
this.localState.splice(i, 1)
|
||
|
this.$emit('input', JSON.stringify(this.localState))
|
||
|
}
|
||
|
this.$emit('update')
|
||
|
},
|
||
|
downloadItem(item) {
|
||
|
FileSaver.saveAs(item.url || item.data, item.title)
|
||
|
},
|
||
|
onArrowDown(e) {
|
||
|
if (!this.showImage) {
|
||
|
return
|
||
|
}
|
||
|
e = e || window.event
|
||
|
// eslint-disable-next-line eqeqeq
|
||
|
if (e.keyCode == '37') {
|
||
|
this.carousel = (this.carousel || this.localState.length) - 1
|
||
|
// eslint-disable-next-line eqeqeq
|
||
|
} else if (e.keyCode == '39') {
|
||
|
this.carousel = ++this.carousel % this.localState.length
|
||
|
// eslint-disable-next-line eqeqeq
|
||
|
} else if (e.keyCode == '27') {
|
||
|
this.hideIfVisible()
|
||
|
}
|
||
|
},
|
||
|
async onFileDrop(e) {
|
||
|
this.dragOver = false
|
||
|
this.$refs.file.files = e.dataTransfer.files
|
||
|
await this.onFileSelection()
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
</script>
|
||
|
|
||
|
<style scoped lang="scss">
|
||
|
.img-container {
|
||
|
margin: 0 -2px;
|
||
|
}
|
||
|
|
||
|
.no-overflow {
|
||
|
overflow: hidden;
|
||
|
}
|
||
|
|
||
|
.add {
|
||
|
transition: .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: .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: .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: .4s transform, .4s opacity;
|
||
|
opacity: .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: .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: .4s opacity;
|
||
|
}
|
||
|
|
||
|
.main:hover .expand-icon {
|
||
|
opacity: 1;
|
||
|
}
|
||
|
|
||
|
</style>
|
||
|
<!--
|
||
|
/**
|
||
|
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
|
||
|
*
|
||
|
* @author Naveen MR <oof1lab@gmail.com>
|
||
|
* @author Pranav C Balan <pranavxc@gmail.com>
|
||
|
* @author Wing-Kam Wong <wingkwong.code@gmail.com>
|
||
|
*
|
||
|
* @license GNU AGPL version 3 or any later version
|
||
|
*
|
||
|
* This program is free software: you can redistribute it and/or modify
|
||
|
* it under the terms of the GNU Affero General Public License as
|
||
|
* published by the Free Software Foundation, either version 3 of the
|
||
|
* License, or (at your option) any later version.
|
||
|
*
|
||
|
* This program is distributed in the hope that it will be useful,
|
||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
* GNU Affero General Public License for more details.
|
||
|
*
|
||
|
* You should have received a copy of the GNU Affero General Public License
|
||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||
|
*
|
||
|
*/
|
||
|
-->
|