@adminforth/upload 1.0.0
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/.woodpecker/buildRelease.sh +13 -0
- package/.woodpecker/buildSlackNotify.sh +44 -0
- package/.woodpecker/release.yml +42 -0
- package/CHANGELOG.md +5 -0
- package/LICENSE +21 -0
- package/build.log +15 -0
- package/custom/imageGenerator.vue +229 -0
- package/custom/package-lock.json +310 -0
- package/custom/package.json +16 -0
- package/custom/preview.vue +111 -0
- package/custom/tsconfig.json +19 -0
- package/custom/uploader.vue +304 -0
- package/dist/custom/imageGenerator.vue +229 -0
- package/dist/custom/package-lock.json +310 -0
- package/dist/custom/package.json +16 -0
- package/dist/custom/preview.vue +111 -0
- package/dist/custom/tsconfig.json +19 -0
- package/dist/custom/uploader.vue +304 -0
- package/dist/index.js +482 -0
- package/dist/types.js +1 -0
- package/index.ts +546 -0
- package/package.json +52 -0
- package/tsconfig.json +112 -0
- package/types.ts +162 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="relative w-full">
|
|
3
|
+
<ImageGenerator v-if="showImageGen" @close="showImageGen = false" :record="record" :meta="meta"
|
|
4
|
+
@uploadImage="uploadGeneratedImage"
|
|
5
|
+
></ImageGenerator>
|
|
6
|
+
|
|
7
|
+
<button v-if="meta.generateImages"
|
|
8
|
+
type="button" @click="showImageGen = true"
|
|
9
|
+
class="text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-purple-300 dark:focus:ring-purple-800
|
|
10
|
+
font-medium rounded-lg text-sm px-2.5 py-2.5 text-center me-2 mb-2 absolute right-0 top-2">
|
|
11
|
+
<IconMagic class="w-5 h-5"/>
|
|
12
|
+
</button>
|
|
13
|
+
|
|
14
|
+
<label :for="inputId"
|
|
15
|
+
class="flex flex-col px-3 items-center justify-center w-full h-64 border-2 border-dashed rounded-lg cursor-pointer dark:hover:bg-gray-800 hover:bg-gray-100 dark:hover:border-gray-500 dark:hover:bg-gray-600"
|
|
16
|
+
@dragover.prevent="() => dragging = true"
|
|
17
|
+
@dragleave.prevent="() => dragging = false"
|
|
18
|
+
@drop.prevent="onFileChange"
|
|
19
|
+
:class="{
|
|
20
|
+
'border-blue-600 dark:border-blue-400': dragging,
|
|
21
|
+
'border-gray-300 dark:border-gray-600': !dragging,
|
|
22
|
+
'bg-blue-50 dark:bg-blue-800': dragging,
|
|
23
|
+
'bg-gray-50 dark:bg-gray-800': !dragging,
|
|
24
|
+
}"
|
|
25
|
+
>
|
|
26
|
+
<div class="flex flex-col items-center justify-center pt-5 pb-6">
|
|
27
|
+
<img v-if="imgPreview" :src="imgPreview" class="w-100 mt-4 rounded-lg h-40 object-contain" />
|
|
28
|
+
|
|
29
|
+
<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">
|
|
30
|
+
<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"/>
|
|
31
|
+
</svg>
|
|
32
|
+
|
|
33
|
+
<template v-if="!uploaded">
|
|
34
|
+
<p class="mb-2 text-sm text-gray-500 dark:text-gray-400"><span class="font-semibold">{{ $t('Click to upload') }}</span> {{ $t('or drag and drop') }}</p>
|
|
35
|
+
<p class="text-xs text-gray-500 dark:text-gray-400">
|
|
36
|
+
{{ allowedExtensionsLabel }} {{ meta.maxFileSize ? $t(`(up to {size})`, { size: humanifySize(meta.maxFileSize) }) : '' }}
|
|
37
|
+
</p>
|
|
38
|
+
</template>
|
|
39
|
+
|
|
40
|
+
<div class="w-full bg-gray-200 rounded-full dark:bg-gray-700 mt-1 mb-2" v-if="progress > 0 && !uploaded">
|
|
41
|
+
<!-- progress bar with smooth progress animation -->
|
|
42
|
+
<div class="bg-blue-600 text-xs font-medium text-blue-100 text-center p-0.5 leading-none rounded-full
|
|
43
|
+
transition-all duration-200 ease-in-out"
|
|
44
|
+
:style="{width: `${progress}%`}">{{ progress }}%
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div v-else-if="uploaded" class="flex items-center justify-center w-full mt-1">
|
|
49
|
+
<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">
|
|
50
|
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
|
51
|
+
</svg>
|
|
52
|
+
<p class="ml-2 text-sm text-green-600 dark:text-green-400 flex items-center">
|
|
53
|
+
{{ $t('File uploaded') }}
|
|
54
|
+
<span class="text-xs text-gray-500 dark:text-gray-400">{{ humanifySize(uploadedSize) }}</span>
|
|
55
|
+
</p>
|
|
56
|
+
|
|
57
|
+
<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
|
|
58
|
+
hover:underline dark:hover:underline focus:outline-none">{{ $t('Clear') }}</button>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
</div>
|
|
62
|
+
<input :id="inputId" type="file" :accept="allowedExtensionsAttribute" class="hidden" @change="onFileChange" ref="uploadInputRef" />
|
|
63
|
+
</label>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
</template>
|
|
67
|
+
|
|
68
|
+
<script setup lang="ts">
|
|
69
|
+
import { computed, ref, onMounted, watch } from 'vue'
|
|
70
|
+
import { callAdminForthApi } from '@/utils'
|
|
71
|
+
import { IconMagic } from '@iconify-prerendered/vue-mdi';
|
|
72
|
+
import { useI18n } from 'vue-i18n';
|
|
73
|
+
import { useRoute } from 'vue-router';
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
const route = useRoute();
|
|
77
|
+
const { t } = useI18n();
|
|
78
|
+
|
|
79
|
+
const inputId = computed(() => `dropzone-file-${props.meta.pluginInstanceId}`);
|
|
80
|
+
|
|
81
|
+
import ImageGenerator from '@@/plugins/UploadPlugin/imageGenerator.vue';
|
|
82
|
+
import adminforth from '@/adminforth';
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
const props = defineProps({
|
|
86
|
+
meta: Object,
|
|
87
|
+
record: Object,
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const emit = defineEmits([
|
|
91
|
+
'update:value',
|
|
92
|
+
'update:inValidity',
|
|
93
|
+
'update:emptiness',
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
const uploadInputRef = ref(null);
|
|
97
|
+
|
|
98
|
+
const showImageGen = ref(false);
|
|
99
|
+
const dragging = ref(false);
|
|
100
|
+
|
|
101
|
+
const imgPreview = ref(null);
|
|
102
|
+
const progress = ref(0);
|
|
103
|
+
|
|
104
|
+
const uploaded = ref(false);
|
|
105
|
+
const uploadedSize = ref(0);
|
|
106
|
+
|
|
107
|
+
watch(() => uploaded, (value) => {
|
|
108
|
+
emit('update:emptiness', !value);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
function uploadGeneratedImage(imgBlob) {
|
|
112
|
+
// update the value
|
|
113
|
+
|
|
114
|
+
const file = new File([imgBlob], 'generated.png', { type: imgBlob.type });
|
|
115
|
+
onFileChange({
|
|
116
|
+
target: {
|
|
117
|
+
files: [file],
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
onMounted(() => {
|
|
123
|
+
const previewColumnName = `previewUrl_${props.meta.pluginInstanceId}`;
|
|
124
|
+
if (props.record[previewColumnName]) {
|
|
125
|
+
imgPreview.value = props.record[previewColumnName];
|
|
126
|
+
uploaded.value = true;
|
|
127
|
+
emit('update:emptiness', false);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const allowedExtensionsLabel = computed(() => {
|
|
132
|
+
const allowedExtensions = props.meta.allowedExtensions || []
|
|
133
|
+
if (allowedExtensions.length === 0) {
|
|
134
|
+
return 'Any file type'
|
|
135
|
+
}
|
|
136
|
+
// make upper case and write in format EXT1, EXT2 or EXT3
|
|
137
|
+
let label = allowedExtensions.map(ext => ext.toUpperCase()).join(', ');
|
|
138
|
+
// last comma to 'or'
|
|
139
|
+
label = label.replace(/,([^,]*)$/, ` ${t('or')}$1`)
|
|
140
|
+
return label
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const allowedExtensionsAttribute = computed(() => {
|
|
144
|
+
const allowedExtensions = props.meta.allowedExtensions || [];
|
|
145
|
+
return allowedExtensions.length > 0
|
|
146
|
+
? allowedExtensions.map(ext => `.${ext}`).join(', ')
|
|
147
|
+
: '';
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
function clear() {
|
|
151
|
+
imgPreview.value = null;
|
|
152
|
+
progress.value = 0;
|
|
153
|
+
uploaded.value = false;
|
|
154
|
+
uploadedSize.value = 0;
|
|
155
|
+
uploadInputRef.value.value = null;
|
|
156
|
+
emit('update:value', null);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function humanifySize(size) {
|
|
160
|
+
if (!size) {
|
|
161
|
+
return '';
|
|
162
|
+
}
|
|
163
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
|
164
|
+
let i = 0
|
|
165
|
+
while (size >= 1024 && i < units.length - 1) {
|
|
166
|
+
size /= 1024
|
|
167
|
+
i++
|
|
168
|
+
}
|
|
169
|
+
return `${size.toFixed(1)} ${units[i]}`
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
const onFileChange = async (e) => {
|
|
174
|
+
// if empty then return
|
|
175
|
+
const files = e.target?.files || e.dataTransfer.files
|
|
176
|
+
|
|
177
|
+
if (!files || files.length === 0) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
imgPreview.value = null;
|
|
182
|
+
progress.value = 0;
|
|
183
|
+
uploaded.value = false;
|
|
184
|
+
|
|
185
|
+
const file = files[0];
|
|
186
|
+
|
|
187
|
+
// get filename, extension, size, mimeType
|
|
188
|
+
const { name, size, type } = file;
|
|
189
|
+
|
|
190
|
+
uploadedSize.value = size;
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
const extension = name.split('.').pop();
|
|
194
|
+
const nameNoExtension = name.replace(`.${extension}`, '');
|
|
195
|
+
console.log('File details:', { name, extension, size, type });
|
|
196
|
+
// validate file extension
|
|
197
|
+
const allowedExtensions = props.meta.allowedExtensions || []
|
|
198
|
+
if (allowedExtensions.length > 0 && !allowedExtensions.includes(extension)) {
|
|
199
|
+
adminforth.alert({
|
|
200
|
+
message: t('Sorry but the file type {extension} is not allowed. Please upload a file with one of the following extensions: {allowedExtensionsLabel}', {
|
|
201
|
+
extension,
|
|
202
|
+
allowedExtensionsLabel: allowedExtensionsLabel.value,
|
|
203
|
+
}),
|
|
204
|
+
variant: 'danger'
|
|
205
|
+
});
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// validate file size
|
|
210
|
+
if (props.meta.maxFileSize && size > props.meta.maxFileSize) {
|
|
211
|
+
adminforth.alert({
|
|
212
|
+
message: t('Sorry but the file size {size} is too large. Please upload a file with a maximum size of {maxFileSize}', {
|
|
213
|
+
size: humanifySize(size),
|
|
214
|
+
maxFileSize: humanifySize(props.meta.maxFileSize),
|
|
215
|
+
}),
|
|
216
|
+
variant: 'danger'
|
|
217
|
+
});
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
emit('update:inValidity', t('Upload in progress...'));
|
|
222
|
+
try {
|
|
223
|
+
// supports preview
|
|
224
|
+
if (type.startsWith('image/')) {
|
|
225
|
+
const reader = new FileReader();
|
|
226
|
+
reader.onload = (e) => {
|
|
227
|
+
imgPreview.value = e.target.result;
|
|
228
|
+
}
|
|
229
|
+
reader.readAsDataURL(file);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const { uploadUrl, tagline, s3Path, error } = await callAdminForthApi({
|
|
233
|
+
path: `/plugin/${props.meta.pluginInstanceId}/get_s3_upload_url`,
|
|
234
|
+
method: 'POST',
|
|
235
|
+
body: {
|
|
236
|
+
originalFilename: nameNoExtension,
|
|
237
|
+
contentType: type,
|
|
238
|
+
size,
|
|
239
|
+
originalExtension: extension,
|
|
240
|
+
recordPk: route?.params?.primaryKey,
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
if (error) {
|
|
245
|
+
adminforth.alert({
|
|
246
|
+
message: t('File was not uploaded because of error: {error}', { error }),
|
|
247
|
+
variant: 'danger'
|
|
248
|
+
});
|
|
249
|
+
imgPreview.value = null;
|
|
250
|
+
uploaded.value = false;
|
|
251
|
+
progress.value = 0;
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const xhr = new XMLHttpRequest();
|
|
256
|
+
const success = await new Promise((resolve) => {
|
|
257
|
+
xhr.upload.onprogress = (e) => {
|
|
258
|
+
if (e.lengthComputable) {
|
|
259
|
+
progress.value = Math.round((e.loaded / e.total) * 100);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
xhr.addEventListener('loadend', () => {
|
|
263
|
+
const success = xhr.readyState === 4 && xhr.status === 200;
|
|
264
|
+
// try to read response
|
|
265
|
+
resolve(success);
|
|
266
|
+
});
|
|
267
|
+
xhr.open('PUT', uploadUrl, true);
|
|
268
|
+
xhr.setRequestHeader('Content-Type', type);
|
|
269
|
+
xhr.setRequestHeader('x-amz-tagging', tagline);
|
|
270
|
+
xhr.send(file);
|
|
271
|
+
});
|
|
272
|
+
if (!success) {
|
|
273
|
+
adminforth.alert({
|
|
274
|
+
messageHtml: `<div>${t('Sorry but the file was not uploaded because of S3 Request Error:')}</div>
|
|
275
|
+
<pre style="white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; max-width: 100%;">${
|
|
276
|
+
xhr.responseText.replace(/</g, '<').replace(/>/g, '>')
|
|
277
|
+
}</pre>`,
|
|
278
|
+
variant: 'danger',
|
|
279
|
+
timeout: 30,
|
|
280
|
+
});
|
|
281
|
+
imgPreview.value = null;
|
|
282
|
+
uploaded.value = false;
|
|
283
|
+
progress.value = 0;
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
uploaded.value = true;
|
|
287
|
+
emit('update:value', s3Path);
|
|
288
|
+
} catch (error) {
|
|
289
|
+
console.error('Error uploading file:', error);
|
|
290
|
+
adminforth.alert({
|
|
291
|
+
message: t('Sorry but the file was not be uploaded. Please try again: {error}', { error: error.message }),
|
|
292
|
+
variant: 'danger'
|
|
293
|
+
});
|
|
294
|
+
imgPreview.value = null;
|
|
295
|
+
uploaded.value = false;
|
|
296
|
+
progress.value = 0;
|
|
297
|
+
} finally {
|
|
298
|
+
emit('update:inValidity', false);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
</script>
|