@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,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,105 @@
1
+ <template>
2
+ <div>
3
+ <template v-if="url">
4
+ <img
5
+ v-if="contentType && contentType.startsWith('image')"
6
+ :src="url"
7
+ class="rounded-md"
8
+ ref="img"
9
+ data-zoomable
10
+ @click.stop="zoom.open()"
11
+ />
12
+ <video
13
+ v-else-if="contentType && contentType.startsWith('video')"
14
+ :src="url"
15
+ class="rounded-md"
16
+ controls
17
+ @click.stop >
18
+ </video>
19
+
20
+ <a v-else :href="url" target="_blank"
21
+ class="flex gap-1 items-center py-1 px-3 me-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-darkListTable dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 rounded-default"
22
+ >
23
+ <!-- download file icon -->
24
+ <svg class="w-4 h-4 me-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
25
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
26
+ </svg>
27
+ Download file
28
+ </a>
29
+ </template>
30
+
31
+
32
+ </div>
33
+ </template>
34
+
35
+ <style>
36
+ .medium-zoom-image {
37
+ z-index: 999999;
38
+ background: rgba(0, 0, 0, 0.8);
39
+ }
40
+ .medium-zoom-overlay {
41
+ background: rgba(255, 255, 255, 0.8) !important
42
+ }
43
+ body.medium-zoom--opened aside {
44
+ filter: grayscale(1)
45
+ }
46
+ </style>
47
+
48
+ <style scoped>
49
+ img {
50
+ min-width: 200px;
51
+ }
52
+ video {
53
+ min-width: 200px;
54
+ }
55
+ </style>
56
+ <script setup>
57
+ import { ref, computed , onMounted, watch} from 'vue'
58
+ import mediumZoom from 'medium-zoom'
59
+
60
+ const img = ref(null);
61
+ const zoom = ref(null);
62
+
63
+ const props = defineProps({
64
+ record: Object,
65
+ column: Object,
66
+ meta: Object,
67
+ })
68
+
69
+ const url = computed(() => {
70
+ return props.record[`previewUrl_${props.meta.pluginInstanceId}`];
71
+ });
72
+
73
+
74
+ // since we have no way to know the content type of the file, we will try to guess it from extension
75
+ // for better experience probably we should check whether user saves content type in the database and use it here
76
+ const contentType = computed(() => {
77
+ const u = new URL(url.value);
78
+ return guessContentType(u.pathname);
79
+ });
80
+
81
+ function guessContentType(url) {
82
+ if (!url) {
83
+ return null;
84
+ }
85
+ const ext = url.split('.').pop();
86
+ if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico', 'tiff'].includes(ext)) {
87
+ return 'image';
88
+ }
89
+ if (['mp4', 'webm', 'ogg', 'avi', 'mov', 'flv', 'wmv', 'mkv'].includes(ext)) {
90
+ return 'video';
91
+ }
92
+ }
93
+
94
+
95
+ onMounted(async () => {
96
+
97
+ if (contentType.value?.startsWith('image')) {
98
+ zoom.value = mediumZoom(img.value, {
99
+ margin: 24,
100
+ });
101
+ }
102
+
103
+ });
104
+
105
+ </script>
@@ -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,105 @@
1
+ <template>
2
+ <div>
3
+ <template v-if="url">
4
+ <img
5
+ v-if="contentType && contentType.startsWith('image')"
6
+ :src="url"
7
+ class="rounded-md"
8
+ ref="img"
9
+ data-zoomable
10
+ @click.stop="zoom.open()"
11
+ />
12
+ <video
13
+ v-else-if="contentType && contentType.startsWith('video')"
14
+ :src="url"
15
+ class="rounded-md"
16
+ controls
17
+ @click.stop >
18
+ </video>
19
+
20
+ <a v-else :href="url" target="_blank"
21
+ class="flex gap-1 items-center py-1 px-3 me-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-darkListTable dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 rounded-default"
22
+ >
23
+ <!-- download file icon -->
24
+ <svg class="w-4 h-4 me-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
25
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
26
+ </svg>
27
+ Download file
28
+ </a>
29
+ </template>
30
+
31
+
32
+ </div>
33
+ </template>
34
+
35
+ <style>
36
+ .medium-zoom-image {
37
+ z-index: 999999;
38
+ background: rgba(0, 0, 0, 0.8);
39
+ }
40
+ .medium-zoom-overlay {
41
+ background: rgba(255, 255, 255, 0.8) !important
42
+ }
43
+ body.medium-zoom--opened aside {
44
+ filter: grayscale(1)
45
+ }
46
+ </style>
47
+
48
+ <style scoped>
49
+ img {
50
+ min-width: 200px;
51
+ }
52
+ video {
53
+ min-width: 200px;
54
+ }
55
+ </style>
56
+ <script setup>
57
+ import { ref, computed , onMounted, watch} from 'vue'
58
+ import mediumZoom from 'medium-zoom'
59
+
60
+ const img = ref(null);
61
+ const zoom = ref(null);
62
+
63
+ const props = defineProps({
64
+ record: Object,
65
+ column: Object,
66
+ meta: Object,
67
+ })
68
+
69
+ const url = computed(() => {
70
+ return props.record[`previewUrl_${props.meta.pluginInstanceId}`];
71
+ });
72
+
73
+
74
+ // since we have no way to know the content type of the file, we will try to guess it from extension
75
+ // for better experience probably we should check whether user saves content type in the database and use it here
76
+ const contentType = computed(() => {
77
+ const u = new URL(url.value);
78
+ return guessContentType(u.pathname);
79
+ });
80
+
81
+ function guessContentType(url) {
82
+ if (!url) {
83
+ return null;
84
+ }
85
+ const ext = url.split('.').pop();
86
+ if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico', 'tiff'].includes(ext)) {
87
+ return 'image';
88
+ }
89
+ if (['mp4', 'webm', 'ogg', 'avi', 'mov', 'flv', 'wmv', 'mkv'].includes(ext)) {
90
+ return 'video';
91
+ }
92
+ }
93
+
94
+
95
+ onMounted(async () => {
96
+
97
+ if (contentType.value?.startsWith('image')) {
98
+ zoom.value = mediumZoom(img.value, {
99
+ margin: 24,
100
+ });
101
+ }
102
+
103
+ });
104
+
105
+ </script>