@adminforth/upload 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/custom/package-lock.json +21 -0
- package/custom/package.json +15 -0
- package/custom/preview.vue +105 -0
- package/custom/uploader.vue +221 -0
- package/dist/custom/package-lock.json +21 -0
- package/dist/custom/package.json +15 -0
- package/dist/custom/preview.vue +105 -0
- package/dist/custom/uploader.vue +221 -0
- package/dist/package-lock.json +21 -0
- package/dist/package.json +15 -0
- package/dist/preview.vue +56 -0
- package/dist/uploader.vue +163 -0
- package/index.ts +349 -0
- package/package.json +16 -0
- package/types.ts +81 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex items-center justify-center w-full">
|
|
3
|
+
<label for="dropzone-file" class="flex flex-col items-center justify-center w-full h-64 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50 dark:hover:bg-gray-800 dark:bg-gray-700 hover:bg-gray-100 dark:border-gray-600 dark:hover:border-gray-500 dark:hover:bg-gray-600">
|
|
4
|
+
<div class="flex flex-col items-center justify-center pt-5 pb-6">
|
|
5
|
+
<img v-if="imgPreview" :src="imgPreview" class="w-100 mt-4 rounded-lg h-40 object-contain" />
|
|
6
|
+
|
|
7
|
+
<svg v-else class="w-8 h-8 mb-4 text-gray-500 dark:text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 16">
|
|
8
|
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 13h3a3 3 0 0 0 0-6h-.025A5.56 5.56 0 0 0 16 6.5 5.5 5.5 0 0 0 5.207 5.021C5.137 5.017 5.071 5 5 5a4 4 0 0 0 0 8h2.167M10 15V6m0 0L8 8m2-2 2 2"/>
|
|
9
|
+
</svg>
|
|
10
|
+
|
|
11
|
+
<template v-if="!uploaded">
|
|
12
|
+
<p class="mb-2 text-sm text-gray-500 dark:text-gray-400"><span class="font-semibold">Click to upload</span> or drag and drop</p>
|
|
13
|
+
<p class="text-xs text-gray-500 dark:text-gray-400">
|
|
14
|
+
{{ allowedExtensionsLabel }} {{ meta.maxFileSize ? `(up to ${maxFileSizeHumanized})` : '' }}
|
|
15
|
+
</p>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<div class="w-full bg-gray-200 rounded-full dark:bg-gray-700 mt-1 mb-2" v-if="progress > 0 && !uploaded">
|
|
19
|
+
<!-- progress bar with smooth progress animation -->
|
|
20
|
+
<div class="bg-blue-600 text-xs font-medium text-blue-100 text-center p-0.5 leading-none rounded-full
|
|
21
|
+
transition-all duration-200 ease-in-out
|
|
22
|
+
"
|
|
23
|
+
:style="{width: `${progress}%`}">{{ progress }}%
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div v-else-if="uploaded" class="flex items-center justify-center w-full mt-1">
|
|
28
|
+
<svg class="w-4 h-4 text-green-600 dark:text-green-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
29
|
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
|
30
|
+
</svg>
|
|
31
|
+
<p class="ml-2 text-sm text-green-600 dark:text-green-400 flex items-center">
|
|
32
|
+
File uploaded
|
|
33
|
+
<span class="text-xs text-gray-500 dark:text-gray-400">{{ humanifySize(uploadedSize) }}</span>
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
<button @click.stop.prevent="clear" class="ml-2 text-xs text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-500
|
|
37
|
+
hover:underline dark:hover:underline focus:outline-none">Clear</button>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
</div>
|
|
41
|
+
<input id="dropzone-file" type="file" class="hidden" @change="onFileChange" />
|
|
42
|
+
</label>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
</template>
|
|
46
|
+
|
|
47
|
+
<script setup>
|
|
48
|
+
import { computed, ref, onMounted, watch } from 'vue'
|
|
49
|
+
import { callAdminForthApi } from '@/utils'
|
|
50
|
+
|
|
51
|
+
const props = defineProps({
|
|
52
|
+
meta: String,
|
|
53
|
+
record: Object,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const emit = defineEmits([
|
|
57
|
+
'update:value',
|
|
58
|
+
'update:inValidity',
|
|
59
|
+
'update:emptiness',
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
const imgPreview = ref(null);
|
|
63
|
+
const progress = ref(0);
|
|
64
|
+
|
|
65
|
+
const uploaded = ref(false);
|
|
66
|
+
const uploadedSize = ref(0);
|
|
67
|
+
|
|
68
|
+
watch(() => uploaded, (value) => {
|
|
69
|
+
emit('update:emptiness', !value);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
onMounted(() => {
|
|
73
|
+
const previewColumnName = `previewUrl_${props.meta.pluginInstanceId}`;
|
|
74
|
+
if (props.record[previewColumnName]) {
|
|
75
|
+
imgPreview.value = props.record[previewColumnName];
|
|
76
|
+
uploaded.value = true;
|
|
77
|
+
emit('update:emptiness', false);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const allowedExtensionsLabel = computed(() => {
|
|
82
|
+
const allowedExtensions = props.meta.allowedExtensions || []
|
|
83
|
+
if (allowedExtensions.length === 0) {
|
|
84
|
+
return 'Any file type'
|
|
85
|
+
}
|
|
86
|
+
// make upper case and write in format EXT1, EXT2 or EXT3
|
|
87
|
+
let label = allowedExtensions.map(ext => ext.toUpperCase()).join(', ');
|
|
88
|
+
// last comma to 'or'
|
|
89
|
+
label = label.replace(/,([^,]*)$/, ' or$1')
|
|
90
|
+
return label
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
function clear() {
|
|
94
|
+
imgPreview.value = null;
|
|
95
|
+
progress.value = 0;
|
|
96
|
+
uploaded.value = false;
|
|
97
|
+
uploadedSize.value = 0;
|
|
98
|
+
emit('update:value', null);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function humanifySize(size) {
|
|
102
|
+
if (!size) {
|
|
103
|
+
return '';
|
|
104
|
+
}
|
|
105
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
|
106
|
+
let i = 0
|
|
107
|
+
while (size >= 1024 && i < units.length - 1) {
|
|
108
|
+
size /= 1024
|
|
109
|
+
i++
|
|
110
|
+
}
|
|
111
|
+
return `${size.toFixed(1)} ${units[i]}`
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
const onFileChange = async (e) => {
|
|
116
|
+
imgPreview.value = null;
|
|
117
|
+
progress.value = 0;
|
|
118
|
+
uploaded.value = false;
|
|
119
|
+
|
|
120
|
+
const file = e.target.files[0]
|
|
121
|
+
|
|
122
|
+
// get filename, extension, size, mimeType
|
|
123
|
+
const { name, size, type } = file;
|
|
124
|
+
|
|
125
|
+
uploadedSize.value = size;
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
const extension = name.split('.').pop();
|
|
129
|
+
const nameNoExtension = name.replace(`.${extension}`, '');
|
|
130
|
+
console.log('File details:', { name, extension, size, type });
|
|
131
|
+
// validate file extension
|
|
132
|
+
const allowedExtensions = props.meta.allowedExtensions || []
|
|
133
|
+
if (allowedExtensions.length > 0 && !allowedExtensions.includes(extension)) {
|
|
134
|
+
window.adminforth.alert({
|
|
135
|
+
message: `Sorry but the file type ${extension} is not allowed. Please upload a file with one of the following extensions: ${allowedExtensionsLabel.value}`,
|
|
136
|
+
variant: 'danger'
|
|
137
|
+
});
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// validate file size
|
|
142
|
+
if (props.meta.maxFileSize && size > props.meta.maxFileSize) {
|
|
143
|
+
window.adminforth.alert({
|
|
144
|
+
message: `Sorry but the file size ${humanifySize(size)} is too large. Please upload a file with a maximum size of ${humanifySize(props.meta.maxFileSize)}`,
|
|
145
|
+
variant: 'danger'
|
|
146
|
+
});
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
emit('update:inValidity', 'Upload in progress...');
|
|
151
|
+
try {
|
|
152
|
+
// supports preview
|
|
153
|
+
if (type.startsWith('image/')) {
|
|
154
|
+
const reader = new FileReader();
|
|
155
|
+
reader.onload = (e) => {
|
|
156
|
+
imgPreview.value = e.target.result;
|
|
157
|
+
}
|
|
158
|
+
reader.readAsDataURL(file);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const { uploadUrl, s3Path } = await callAdminForthApi({
|
|
162
|
+
path: `/plugin/${props.meta.pluginInstanceId}/get_s3_upload_url`,
|
|
163
|
+
method: 'POST',
|
|
164
|
+
body: {
|
|
165
|
+
originalFilename: nameNoExtension,
|
|
166
|
+
contentType: type,
|
|
167
|
+
size,
|
|
168
|
+
originalExtension: extension,
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const xhr = new XMLHttpRequest();
|
|
173
|
+
const success = await new Promise((resolve) => {
|
|
174
|
+
xhr.upload.onprogress = (e) => {
|
|
175
|
+
if (e.lengthComputable) {
|
|
176
|
+
progress.value = Math.round((e.loaded / e.total) * 100);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
xhr.addEventListener('loadend', () => {
|
|
180
|
+
const success = xhr.readyState === 4 && xhr.status === 200;
|
|
181
|
+
// try to read response
|
|
182
|
+
resolve(success);
|
|
183
|
+
});
|
|
184
|
+
xhr.open('PUT', uploadUrl, true);
|
|
185
|
+
xhr.setRequestHeader('Content-Type', type);
|
|
186
|
+
xhr.setRequestHeader('x-amz-tagging', (new URL(uploadUrl)).searchParams.get('x-amz-tagging'));
|
|
187
|
+
xhr.send(file);
|
|
188
|
+
});
|
|
189
|
+
if (!success) {
|
|
190
|
+
window.adminforth.alert({
|
|
191
|
+
messageHtml: `<div>Sorry but the file was not be uploaded because of S3 Request Error: </div>
|
|
192
|
+
<pre style="white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; max-width: 100%;">${
|
|
193
|
+
xhr.responseText.replace(/</g, '<').replace(/>/g, '>')
|
|
194
|
+
}</pre>`,
|
|
195
|
+
variant: 'danger',
|
|
196
|
+
timeout: 30,
|
|
197
|
+
});
|
|
198
|
+
imgPreview.value = null;
|
|
199
|
+
uploaded.value = false;
|
|
200
|
+
progress.value = 0;
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
uploaded.value = true;
|
|
204
|
+
emit('update:value', s3Path);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.error('Error uploading file:', error);
|
|
207
|
+
window.adminforth.alert({
|
|
208
|
+
message: `Sorry but the file was not be uploaded. Please try again: ${error.message}`,
|
|
209
|
+
variant: 'danger'
|
|
210
|
+
});
|
|
211
|
+
imgPreview.value = null;
|
|
212
|
+
uploaded.value = false;
|
|
213
|
+
progress.value = 0;
|
|
214
|
+
} finally {
|
|
215
|
+
emit('update:inValidity', false);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
</script>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "custom",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"lockfileVersion": 3,
|
|
5
|
+
"requires": true,
|
|
6
|
+
"packages": {
|
|
7
|
+
"": {
|
|
8
|
+
"name": "custom",
|
|
9
|
+
"version": "1.0.0",
|
|
10
|
+
"license": "ISC",
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"medium-zoom": "^1.1.0"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"node_modules/medium-zoom": {
|
|
16
|
+
"version": "1.1.0",
|
|
17
|
+
"resolved": "https://registry.npmjs.org/medium-zoom/-/medium-zoom-1.1.0.tgz",
|
|
18
|
+
"integrity": "sha512-ewyDsp7k4InCUp3jRmwHBRFGyjBimKps/AJLjRSox+2q/2H4p/PNpQf+pwONWlJiOudkBXtbdmVbFjqyybfTmQ=="
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "custom",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [],
|
|
10
|
+
"author": "",
|
|
11
|
+
"license": "ISC",
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"medium-zoom": "^1.1.0"
|
|
14
|
+
}
|
|
15
|
+
}
|
package/dist/preview.vue
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<img v-if="url" :src="url"
|
|
4
|
+
class="rounded-md"
|
|
5
|
+
ref="img"
|
|
6
|
+
data-zoomable
|
|
7
|
+
@click.stop="zoom.open()"
|
|
8
|
+
/>
|
|
9
|
+
</div>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<style>
|
|
13
|
+
.medium-zoom-image {
|
|
14
|
+
z-index: 999999;
|
|
15
|
+
background: rgba(0, 0, 0, 0.8);
|
|
16
|
+
}
|
|
17
|
+
.medium-zoom-overlay {
|
|
18
|
+
background: rgba(255, 255, 255, 0.8) !important
|
|
19
|
+
}
|
|
20
|
+
body.medium-zoom--opened aside {
|
|
21
|
+
filter: grayscale(1)
|
|
22
|
+
}
|
|
23
|
+
</style>
|
|
24
|
+
|
|
25
|
+
<style scoped>
|
|
26
|
+
img {
|
|
27
|
+
min-width: 200px;
|
|
28
|
+
}
|
|
29
|
+
</style>
|
|
30
|
+
<script setup>
|
|
31
|
+
import { ref, computed , onMounted, watch} from 'vue'
|
|
32
|
+
import mediumZoom from 'medium-zoom'
|
|
33
|
+
|
|
34
|
+
const img = ref(null);
|
|
35
|
+
|
|
36
|
+
const props = defineProps({
|
|
37
|
+
record: Object,
|
|
38
|
+
column: Object,
|
|
39
|
+
meta: Object,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const url = computed(() => {
|
|
43
|
+
return props.record[`previewUrl_${props.meta.pluginInstanceId}`];
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const zoom = ref(null);
|
|
47
|
+
|
|
48
|
+
onMounted(() => {
|
|
49
|
+
zoom.value = mediumZoom(img.value, {
|
|
50
|
+
margin: 24,
|
|
51
|
+
// container: '#app',
|
|
52
|
+
});
|
|
53
|
+
console.log('mounted', props.meta)
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
</script>
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex items-center justify-center w-full">
|
|
3
|
+
<label for="dropzone-file" class="flex flex-col items-center justify-center w-full h-64 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50 dark:hover:bg-gray-800 dark:bg-gray-700 hover:bg-gray-100 dark:border-gray-600 dark:hover:border-gray-500 dark:hover:bg-gray-600">
|
|
4
|
+
<div class="flex flex-col items-center justify-center pt-5 pb-6">
|
|
5
|
+
<img v-if="imgPreview" :src="imgPreview" class="w-100 mt-4 rounded-lg h-40 object-contain" />
|
|
6
|
+
|
|
7
|
+
<svg v-else class="w-8 h-8 mb-4 text-gray-500 dark:text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 16">
|
|
8
|
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 13h3a3 3 0 0 0 0-6h-.025A5.56 5.56 0 0 0 16 6.5 5.5 5.5 0 0 0 5.207 5.021C5.137 5.017 5.071 5 5 5a4 4 0 0 0 0 8h2.167M10 15V6m0 0L8 8m2-2 2 2"/>
|
|
9
|
+
</svg>
|
|
10
|
+
|
|
11
|
+
<template v-if="!uploaded">
|
|
12
|
+
<p class="mb-2 text-sm text-gray-500 dark:text-gray-400"><span class="font-semibold">Click to upload</span> or drag and drop</p>
|
|
13
|
+
<p class="text-xs text-gray-500 dark:text-gray-400">
|
|
14
|
+
{{ allowedExtensionsLabel }} {{ meta.maxFileSize ? `(up to ${maxFileSizeHumanized})` : '' }}
|
|
15
|
+
</p>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<div class="w-full bg-gray-200 rounded-full dark:bg-gray-700 mt-1 mb-2" v-if="progress > 0 && !uploaded">
|
|
19
|
+
<div class="bg-blue-600 text-xs font-medium text-blue-100 text-center p-0.5 leading-none rounded-full"
|
|
20
|
+
:style="{width: `${progress}%`}">{{ progress }}%
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div v-else-if="uploaded" class="flex items-center justify-center w-full mt-1">
|
|
25
|
+
<svg class="w-4 h-4 text-green-600 dark:text-green-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
26
|
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
|
27
|
+
</svg>
|
|
28
|
+
<p class="ml-2 text-sm text-green-600 dark:text-green-400">File uploaded</p>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
<input id="dropzone-file" type="file" class="hidden" @change="onFileChange" />
|
|
32
|
+
</label>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
</template>
|
|
36
|
+
|
|
37
|
+
<script setup>
|
|
38
|
+
import { computed, ref } from 'vue'
|
|
39
|
+
import { callAdminForthApi } from '@/utils'
|
|
40
|
+
|
|
41
|
+
const props = defineProps({
|
|
42
|
+
meta: String
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const emit = defineEmits(['update:value']);
|
|
46
|
+
|
|
47
|
+
const imgPreview = ref(null);
|
|
48
|
+
const progress = ref(0);
|
|
49
|
+
|
|
50
|
+
const uploadedPath = ref(null);
|
|
51
|
+
const uploaded = ref(false);
|
|
52
|
+
|
|
53
|
+
const allowedExtensionsLabel = computed(() => {
|
|
54
|
+
const allowedExtensions = props.meta.allowedExtensions || []
|
|
55
|
+
if (allowedExtensions.length === 0) {
|
|
56
|
+
return 'Any file type'
|
|
57
|
+
}
|
|
58
|
+
// make upper case and write in format EXT1, EXT2 or EXT3
|
|
59
|
+
let label = allowedExtensions.map(ext => ext.toUpperCase()).join(', ');
|
|
60
|
+
// last comma to 'or'
|
|
61
|
+
label = label.replace(/,([^,]*)$/, ' or$1')
|
|
62
|
+
return label
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const maxFileSizeHumanized = computed(() => {
|
|
66
|
+
let maxFileSize = props.meta.maxFileSize || 0
|
|
67
|
+
if (maxFileSize === 0) {
|
|
68
|
+
return ''
|
|
69
|
+
}
|
|
70
|
+
// if maxFileSize is in bytes, convert to KB, MB, GB, TB
|
|
71
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
|
72
|
+
let i = 0
|
|
73
|
+
while (maxFileSize >= 1024 && i < units.length - 1) {
|
|
74
|
+
maxFileSize /= 1024
|
|
75
|
+
i++
|
|
76
|
+
}
|
|
77
|
+
return `${maxFileSize.toFixed(2)} ${units[i]}`
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const onFileChange = async (e) => {
|
|
81
|
+
const file = e.target.files[0]
|
|
82
|
+
if (!file) {
|
|
83
|
+
imgPreview.value = null;
|
|
84
|
+
progress.value = 0;
|
|
85
|
+
uploaded.value = false;
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
console.log('File selected:', file);
|
|
89
|
+
|
|
90
|
+
// get filename, extension, size, mimeType
|
|
91
|
+
const { name, size, type } = file;
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
const extension = name.split('.').pop();
|
|
95
|
+
console.log('File details:', { name, extension, size, type });
|
|
96
|
+
// validate file extension
|
|
97
|
+
const allowedExtensions = props.meta.allowedExtensions || []
|
|
98
|
+
if (allowedExtensions.length > 0 && !allowedExtensions.includes(extension)) {
|
|
99
|
+
window.adminforth.alert({
|
|
100
|
+
message: `Sorry but the file type ${extension} is not allowed. Please upload a file with one of the following extensions: ${allowedExtensionsLabel.value}`,
|
|
101
|
+
variant: 'danger'
|
|
102
|
+
});
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// validate file size
|
|
107
|
+
if (props.meta.maxFileSize && size > props.meta.maxFileSize) {
|
|
108
|
+
window.adminforth.alert({
|
|
109
|
+
message: `Sorry but the file size is too large. Please upload a file with a maximum size of ${maxFileSizeHumanized.value}`,
|
|
110
|
+
variant: 'danger'
|
|
111
|
+
});
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
// supports preview
|
|
115
|
+
if (type.startsWith('image/')) {
|
|
116
|
+
const reader = new FileReader();
|
|
117
|
+
reader.onload = (e) => {
|
|
118
|
+
imgPreview.value = e.target.result;
|
|
119
|
+
}
|
|
120
|
+
reader.readAsDataURL(file);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const { uploadUrl, previewUrl, s3Path } = await callAdminForthApi({
|
|
124
|
+
path: `/plugin/${props.meta.pluginInstanceId}/get_s3_upload_url`,
|
|
125
|
+
method: 'POST',
|
|
126
|
+
body: {
|
|
127
|
+
originalFilename: name,
|
|
128
|
+
contentType: type,
|
|
129
|
+
size,
|
|
130
|
+
originalExtension: extension,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
console.log('S3 upload URL:', uploadUrl);
|
|
135
|
+
|
|
136
|
+
const xhr = new XMLHttpRequest();
|
|
137
|
+
const success = await new Promise((resolve) => {
|
|
138
|
+
xhr.upload.onprogress = (e) => {
|
|
139
|
+
if (e.lengthComputable) {
|
|
140
|
+
progress.value = Math.round((e.loaded / e.total) * 100);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
xhr.addEventListener('loadend', () => {
|
|
144
|
+
resolve(xhr.readyState === 4 && xhr.status === 200);
|
|
145
|
+
});
|
|
146
|
+
xhr.open('PUT', uploadUrl, true);
|
|
147
|
+
xhr.setRequestHeader('Content-Type', type);
|
|
148
|
+
xhr.send(file);
|
|
149
|
+
});
|
|
150
|
+
if (!success) {
|
|
151
|
+
window.adminforth.alert({
|
|
152
|
+
message: 'Sorry but the file was not be uploaded. Please try again.',
|
|
153
|
+
variant: 'danger'
|
|
154
|
+
});
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
uploaded.value = true;
|
|
158
|
+
emit('update:value', s3Path);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
</script>
|