@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.
@@ -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, '&lt;').replace(/>/g, '&gt;')
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
+ }
@@ -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>