feat: File Uploader (wip)
This commit is contained in:
parent
742860f56e
commit
847be08a9b
4 changed files with 247 additions and 108 deletions
|
|
@ -1,51 +1,57 @@
|
|||
<template>
|
||||
<div class="file-preview">
|
||||
<div class="file-icon border rounded">
|
||||
<div class="file-icon">
|
||||
<img
|
||||
v-if="is_image"
|
||||
:src="src"
|
||||
:alt="file.name"
|
||||
style="object-fit: cover; height: 100%;"
|
||||
>
|
||||
<div class="flex align-center justify-center" style="height: 100%;" v-else>
|
||||
<i class="octicon octicon-file-text text-extra-muted" style="font-size: 5rem;"></i>
|
||||
<div class="fallback" v-else v-html="frappe.utils.icon('file', 'md')">
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-info">
|
||||
<div class="text-medium flex justify-between">
|
||||
<span :title="file.name">
|
||||
<a :href="file.doc.file_url" v-if="file.doc" target="_blank">
|
||||
<i v-if="file.doc.is_private" class="fa fa-lock fa-fw text-warning"></i>
|
||||
<i v-else class="fa fa-unlock-alt fa-fw text-warning"></i>
|
||||
{{ file.name | file_name }}
|
||||
</a>
|
||||
<span v-else>
|
||||
<span class="cursor-pointer" @click="$emit('toggle_private')" :title="__('Toggle Public/Private')">
|
||||
<i v-if="file.private" class="fa fa-lock fa-fw text-warning"></i>
|
||||
<i v-else class="fa fa-unlock-alt fa-fw text-warning"></i>
|
||||
</span>
|
||||
{{ file.name | file_name }}
|
||||
<div>
|
||||
<div>
|
||||
<a :href="file.doc.file_url" v-if="file.doc" target="_blank">
|
||||
<span class="file-name">{{ file.name | file_name }}</span>
|
||||
<i v-html="frappe.utils.icon(file.doc.is_private ? 'lock' : 'unlock')"></i>
|
||||
</a>
|
||||
<span class="flex" v-else>
|
||||
<span class="file-name">{{ file.name | file_name }}</span>
|
||||
<span class="cursor-pointer" @click="$emit('toggle_private')" :title="__('Toggle Public/Private')">
|
||||
<i v-html="frappe.utils.icon(file.is_private ? 'lock' : 'unlock')"></i>
|
||||
</span>
|
||||
</span>
|
||||
<i v-if="uploaded" class="octicon octicon-check text-success" :title="__('Uploaded successfully')"></i>
|
||||
<i v-if="file.failed" class="octicon octicon-x text-danger" :title="__('Upload failed')"></i>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-small text-muted">
|
||||
<span class="file-size">
|
||||
{{ file.file_obj.size | file_size }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-remove" @click="$emit('remove')" v-if="!uploaded">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||
<div class="file-actions">
|
||||
<ProgressRing
|
||||
v-show="file.uploading"
|
||||
primary="var(--primary-color)"
|
||||
secondary="var(--gray-200)"
|
||||
radius="24"
|
||||
:progress="progress"
|
||||
stroke="3"
|
||||
/>
|
||||
<div v-if="uploaded" v-html="frappe.utils.icon('solid-success', 'lg')"></div>
|
||||
<div v-if="file.failed" v-html="frappe.utils.icon('solid-red', 'lg')"></div>
|
||||
<button v-if="!uploaded && !file.uploading" class="btn" @click="$emit('remove')" v-html="frappe.utils.icon('delete', 'md')"></button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ProgressRing from './ProgressRing.vue';
|
||||
export default {
|
||||
name: 'FilePreview',
|
||||
props: ['file'],
|
||||
components: {
|
||||
ProgressRing
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
src: null
|
||||
|
|
@ -65,7 +71,8 @@ export default {
|
|||
return frappe.form.formatters.FileSize(value);
|
||||
},
|
||||
file_name(value) {
|
||||
return frappe.utils.file_name_ellipsis(value, 9);
|
||||
return value;
|
||||
// return frappe.utils.file_name_ellipsis(value, 9);
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -74,39 +81,88 @@ export default {
|
|||
},
|
||||
is_image() {
|
||||
return this.file.file_obj.type.startsWith('image');
|
||||
},
|
||||
progress() {
|
||||
let value = Math.round((this.file.progress * 100) / this.file.total);
|
||||
if (isNaN(value)) {
|
||||
value = 0;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
@import "frappe/public/less/variables.less";
|
||||
|
||||
<style>
|
||||
.file-preview {
|
||||
width: 25%;
|
||||
padding-right: 15px;
|
||||
padding-bottom: 15px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.file-preview + .file-preview {
|
||||
border-top-color: var(--border-color);
|
||||
}
|
||||
|
||||
.file-preview:hover {
|
||||
background-color: var(--bg-color);
|
||||
border-color: var(--dark-border-color);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.file-preview:hover + .file-preview {
|
||||
border-top-color: transparent;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
height: 10rem;
|
||||
border-radius: var(--border-radius);
|
||||
width: 2.625rem;
|
||||
height: 2.625rem;
|
||||
overflow: hidden;
|
||||
margin-right: var(--margin-md);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.file-icon img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.file-icon .fallback {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: var(--text-base);
|
||||
font-weight: var(--text-bold);
|
||||
color: var(--text-color);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
margin-top: 5px;
|
||||
.file-size {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.file-remove {
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
right: 7px;
|
||||
background: @text-dark;
|
||||
color: white;
|
||||
padding: 3px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
.file-actions {
|
||||
width: 2.5rem;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.file-actions .btn {
|
||||
padding: var(--padding-xs);
|
||||
box-shadow: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -5,38 +5,56 @@
|
|||
@drop.prevent="dropfiles"
|
||||
>
|
||||
<div
|
||||
class="file-upload-area padding border rounded text-center cursor-pointer flex align-center justify-center"
|
||||
@click="browse_files"
|
||||
class="file-upload-area"
|
||||
v-show="files.length === 0 && !show_file_browser && !show_web_link"
|
||||
>
|
||||
<div v-if="!is_dragging">
|
||||
<div>
|
||||
{{ __('Drag and drop files, ') }}
|
||||
<label style="margin: 0">
|
||||
<a href="#" class="text-primary" @click.prevent>{{ __('browse,') }}</a>
|
||||
<input
|
||||
type="file"
|
||||
class="hidden"
|
||||
ref="file_input"
|
||||
@change="on_file_input"
|
||||
:multiple="allow_multiple"
|
||||
:accept="restrictions.allowed_file_types.join(', ')"
|
||||
>
|
||||
</label>
|
||||
<span v-if="!disable_file_browser">
|
||||
{{ __('choose an') }}
|
||||
<a href="#" class="text-primary bold"
|
||||
@click.stop.prevent="show_file_browser = true"
|
||||
>
|
||||
{{ __('uploaded file') }}
|
||||
</a>
|
||||
</span>
|
||||
{{ __('or attach a') }}
|
||||
<a class="text-primary bold" href
|
||||
@click.stop.prevent="show_web_link = true"
|
||||
<div class="text-center">
|
||||
{{ __('Drag and drop files here or') }}
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<button class="btn btn-file-upload" @click="browse_files">
|
||||
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="15" cy="15" r="15" fill="url(#paint0_linear)"/>
|
||||
<path d="M13.5 22V19" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M16.5 22V19" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.5 22H19.5" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.5 16H22.5" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M21 8H9C8.17157 8 7.5 8.67157 7.5 9.5V17.5C7.5 18.3284 8.17157 19 9 19H21C21.8284 19 22.5 18.3284 22.5 17.5V9.5C22.5 8.67157 21.8284 8 21 8Z" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="0" y1="0" x2="0" y2="30" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#2C9AF1"/>
|
||||
<stop offset="1" stop-color="#2490EF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<div class="mt-1">{{ __('Select File') }}</div>
|
||||
</button>
|
||||
<input
|
||||
type="file"
|
||||
class="hidden"
|
||||
ref="file_input"
|
||||
@change="on_file_input"
|
||||
:multiple="allow_multiple"
|
||||
:accept="restrictions.allowed_file_types.join(', ')"
|
||||
>
|
||||
{{ __('web link') }}
|
||||
</a>
|
||||
<button class="btn btn-file-upload" v-if="!disable_file_browser" @click="show_file_browser = true">
|
||||
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="15" cy="15" r="15" fill="#48BB74"/>
|
||||
<path d="M13.0245 11.5H8C7.72386 11.5 7.5 11.7239 7.5 12V20C7.5 21.1046 8.39543 22 9.5 22H20.5C21.6046 22 22.5 21.1046 22.5 20V14.5C22.5 14.2239 22.2761 14 22 14H15.2169C15.0492 14 14.8926 13.9159 14.8 13.776L13.4414 11.724C13.3488 11.5841 13.1922 11.5 13.0245 11.5Z" stroke="white" stroke-miterlimit="10" stroke-linecap="square"/>
|
||||
<path d="M8.87939 9.5V8.5C8.87939 8.22386 9.10325 8 9.37939 8H20.6208C20.8969 8 21.1208 8.22386 21.1208 8.5V12" stroke="white" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<div class="mt-1">{{ __('Library') }}</div>
|
||||
</button>
|
||||
<button class="btn btn-file-upload" @click="show_web_link = true">
|
||||
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="15" cy="15" r="15" fill="#ECAC4B"/>
|
||||
<path d="M12.0469 17.9543L17.9558 12.0454" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13.8184 11.4547L15.7943 9.47873C16.4212 8.85205 17.2714 8.5 18.1578 8.5C19.0443 8.5 19.8945 8.85205 20.5214 9.47873V9.47873C21.1481 10.1057 21.5001 10.9558 21.5001 11.8423C21.5001 12.7287 21.1481 13.5789 20.5214 14.2058L18.5455 16.1818" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11.4547 13.8184L9.47873 15.7943C8.85205 16.4212 8.5 17.2714 8.5 18.1578C8.5 19.0443 8.85205 19.8945 9.47873 20.5214V20.5214C10.1057 21.1481 10.9558 21.5001 11.8423 21.5001C12.7287 21.5001 13.5789 21.1481 14.2058 20.5214L16.1818 18.5455" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<div class="mt-1">{{ __('Link') }}</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-muted text-medium">
|
||||
{{ upload_notes }}
|
||||
|
|
@ -47,15 +65,15 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="file-preview-area" v-show="files.length && !show_file_browser && !show_web_link">
|
||||
<div class="margin-bottom" v-if="!upload_complete">
|
||||
<!-- <div class="margin-bottom" v-if="!upload_complete">
|
||||
<label>
|
||||
<input type="checkbox" class="input-with-feedback" @change="e => toggle_all_private(e.target.checked)">
|
||||
<span class="text-medium" style="font-weight: normal;">
|
||||
{{ __('Make all attachments private') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex flex-wrap">
|
||||
</div> -->
|
||||
<div class="file-preview-container">
|
||||
<FilePreview
|
||||
v-for="(file, i) in files"
|
||||
:key="file.name"
|
||||
|
|
@ -81,30 +99,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="upload-progress" v-if="currently_uploading !== -1 && !upload_complete && !show_file_browser && !show_web_link">
|
||||
<span
|
||||
class="text-medium"
|
||||
v-html="__('Uploading {0} of {1}', [String(currently_uploading + 1).bold(), String(files.length).bold()])"
|
||||
>
|
||||
</span>
|
||||
<div
|
||||
class="progress"
|
||||
:key="i"
|
||||
v-for="(file, i) in files"
|
||||
v-show="currently_uploading===i"
|
||||
>
|
||||
<div
|
||||
class="progress-bar"
|
||||
:class="[file.total - file.progress < 20 ? 'progress-bar-success' : 'progress-bar-warning']"
|
||||
role="progressbar"
|
||||
:aria-valuenow="(file.progress * 100 / file.total)"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
:style="{'width': (file.progress * 100 / file.total) + '%' }"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FileBrowser
|
||||
ref="file_browser"
|
||||
v-if="show_file_browser && !disable_file_browser"
|
||||
|
|
@ -219,6 +213,9 @@ export default {
|
|||
this.files[i].private = !this.files[i].private;
|
||||
},
|
||||
toggle_all_private(flag) {
|
||||
if (flag == null) {
|
||||
flag = this.files.every(file => file.private);
|
||||
}
|
||||
this.files = this.files.map(file => {
|
||||
file.private = flag;
|
||||
return file;
|
||||
|
|
@ -425,6 +422,20 @@ export default {
|
|||
</script>
|
||||
<style>
|
||||
.file-upload-area {
|
||||
min-height: 100px;
|
||||
min-height: 16rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px dashed var(--dark-border-color);
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
.btn-file-upload {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
76
frappe/public/js/frappe/file_uploader/ProgressRing.vue
Normal file
76
frappe/public/js/frappe/file_uploader/ProgressRing.vue
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<template>
|
||||
<svg :height="radius * 2" :width="radius * 2">
|
||||
<circle
|
||||
:stroke-dasharray="circumference + ' ' + circumference"
|
||||
:style="{
|
||||
stroke: secondary,
|
||||
strokeDashoffset: 0
|
||||
}"
|
||||
:stroke-width="stroke"
|
||||
fill="transparent"
|
||||
:r="normalizedRadius"
|
||||
:cx="radius"
|
||||
:cy="radius"
|
||||
/>
|
||||
<circle
|
||||
:stroke-dasharray="circumference + ' ' + circumference"
|
||||
:style="{
|
||||
stroke: primary,
|
||||
strokeDashoffset: strokeDashoffset
|
||||
}"
|
||||
:stroke-width="stroke"
|
||||
fill="transparent"
|
||||
:r="normalizedRadius"
|
||||
:cx="radius"
|
||||
:cy="radius"
|
||||
/>
|
||||
<text
|
||||
dominant-baseline="middle"
|
||||
text-anchor="middle"
|
||||
:x="radius"
|
||||
:y="radius"
|
||||
:style="{
|
||||
color: 'var(--text-color)',
|
||||
fontSize: 'var(--text-xs)',
|
||||
fontWeight: 'var(--text-bold)'
|
||||
}"
|
||||
>
|
||||
{{ progress }}%
|
||||
</text>
|
||||
</svg>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: "ProgressRing",
|
||||
props: {
|
||||
primary: String,
|
||||
secondary: String,
|
||||
radius: Number,
|
||||
progress: Number,
|
||||
stroke: Number
|
||||
},
|
||||
data() {
|
||||
const normalizedRadius = this.radius - this.stroke * 2;
|
||||
const circumference = normalizedRadius * 2 * Math.PI;
|
||||
|
||||
return {
|
||||
normalizedRadius,
|
||||
circumference
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
strokeDashoffset() {
|
||||
return (
|
||||
this.circumference - (this.progress / 100) * this.circumference
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
circle {
|
||||
transition: stroke-dashoffset 0.35s;
|
||||
transform: rotate(-90deg);
|
||||
transform-origin: 50% 50%;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -60,17 +60,13 @@ export default class FileUploader {
|
|||
make_dialog() {
|
||||
this.dialog = new frappe.ui.Dialog({
|
||||
title: 'Upload',
|
||||
fields: [
|
||||
{
|
||||
fieldtype: 'HTML',
|
||||
fieldname: 'upload_area'
|
||||
}
|
||||
],
|
||||
primary_action_label: __('Upload'),
|
||||
primary_action: () => this.upload_files()
|
||||
primary_action: () => this.upload_files(),
|
||||
secondary_action_label: __('Toggle Private'),
|
||||
secondary_action: () => this.uploader.toggle_all_private()
|
||||
});
|
||||
|
||||
this.wrapper = this.dialog.fields_dict.upload_area.$wrapper[0];
|
||||
this.wrapper = this.dialog.body;
|
||||
this.dialog.show();
|
||||
this.dialog.$wrapper.on('hidden.bs.modal', function() {
|
||||
$(this).data('bs.modal', null);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue