@adminforth/upload 2.6.1 β 2.7.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/build.log +2 -2
- package/custom/preview.vue +61 -32
- package/custom/uploader.vue +30 -7
- package/dist/custom/preview.vue +61 -32
- package/dist/custom/uploader.vue +30 -7
- package/dist/index.js +62 -42
- package/index.ts +66 -38
- package/package.json +1 -1
package/build.log
CHANGED
|
@@ -11,5 +11,5 @@ custom/preview.vue
|
|
|
11
11
|
custom/tsconfig.json
|
|
12
12
|
custom/uploader.vue
|
|
13
13
|
|
|
14
|
-
sent
|
|
15
|
-
total size is
|
|
14
|
+
sent 51,239 bytes received 134 bytes 102,746.00 bytes/sec
|
|
15
|
+
total size is 50,757 speedup is 0.99
|
package/custom/preview.vue
CHANGED
|
@@ -1,31 +1,36 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div>
|
|
3
|
-
<template v-if="
|
|
4
|
-
<
|
|
5
|
-
v-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
3
|
+
<template v-if="urls.length">
|
|
4
|
+
<div class="flex flex-wrap gap-2 items-start">
|
|
5
|
+
<template v-for="(u, i) in urls" :key="`${u}-${i}`">
|
|
6
|
+
<img
|
|
7
|
+
v-if="guessContentTypeFromUrl(u)?.startsWith('image')"
|
|
8
|
+
:src="u"
|
|
9
|
+
class="rounded-md cursor-zoom-in"
|
|
10
|
+
:style="[maxWidth, minWidth]"
|
|
11
|
+
ref="img"
|
|
12
|
+
@click.stop="openZoom(i)"
|
|
13
|
+
/>
|
|
14
|
+
<video
|
|
15
|
+
v-else-if="guessContentTypeFromUrl(u)?.startsWith('video')"
|
|
16
|
+
:src="u"
|
|
17
|
+
class="rounded-md"
|
|
18
|
+
controls
|
|
19
|
+
@click.stop
|
|
20
|
+
/>
|
|
21
|
+
<a
|
|
22
|
+
v-else
|
|
23
|
+
:href="u"
|
|
24
|
+
target="_blank"
|
|
25
|
+
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-lightPrimary 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"
|
|
26
|
+
>
|
|
27
|
+
<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">
|
|
28
|
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
|
29
|
+
</svg>
|
|
30
|
+
{{ $t('Download file') }}
|
|
31
|
+
</a>
|
|
32
|
+
</template>
|
|
33
|
+
</div>
|
|
29
34
|
</template>
|
|
30
35
|
|
|
31
36
|
|
|
@@ -74,8 +79,10 @@ const props = defineProps({
|
|
|
74
79
|
const trueContentType = ref(null);
|
|
75
80
|
|
|
76
81
|
onMounted(async () => {
|
|
77
|
-
|
|
78
|
-
|
|
82
|
+
// try to get HEAD request (single url only). For arrays we just guess by extension.
|
|
83
|
+
if (!url.value) return;
|
|
84
|
+
if (Array.isArray(url.value)) return;
|
|
85
|
+
try {
|
|
79
86
|
const response = await fetch(url.value, {
|
|
80
87
|
method: 'HEAD',
|
|
81
88
|
mode: 'cors',
|
|
@@ -101,6 +108,11 @@ const url = computed(() => {
|
|
|
101
108
|
return props.record[`previewUrl_${props.meta.pluginInstanceId}`];
|
|
102
109
|
});
|
|
103
110
|
|
|
111
|
+
const urls = computed(() => {
|
|
112
|
+
if (!url.value) return [];
|
|
113
|
+
return Array.isArray(url.value) ? url.value : [url.value];
|
|
114
|
+
});
|
|
115
|
+
|
|
104
116
|
const maxWidth = computed(() => {
|
|
105
117
|
const isShowPage = route.path.includes('/show/');
|
|
106
118
|
const width = isShowPage
|
|
@@ -124,6 +136,7 @@ const guessedContentType = computed(() => {
|
|
|
124
136
|
if (!url.value) {
|
|
125
137
|
return null;
|
|
126
138
|
}
|
|
139
|
+
if (Array.isArray(url.value)) return null;
|
|
127
140
|
const u = new URL(url.value, url.value.startsWith('http') ? undefined : location.origin);
|
|
128
141
|
return guessContentType(u.pathname);
|
|
129
142
|
});
|
|
@@ -141,6 +154,15 @@ function guessContentType(url) {
|
|
|
141
154
|
}
|
|
142
155
|
}
|
|
143
156
|
|
|
157
|
+
function guessContentTypeFromUrl(u) {
|
|
158
|
+
if (!u) return null;
|
|
159
|
+
try {
|
|
160
|
+
const parsed = new URL(u, u.startsWith('http') ? undefined : location.origin);
|
|
161
|
+
return guessContentType(parsed.pathname);
|
|
162
|
+
} catch (e) {
|
|
163
|
+
return guessContentType(u);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
144
166
|
|
|
145
167
|
watch([contentType], async ([contentType]) => {
|
|
146
168
|
// since content type might change after true guessing (HEAD request might be slow) we need to try initializing zoom again
|
|
@@ -148,12 +170,19 @@ watch([contentType], async ([contentType]) => {
|
|
|
148
170
|
zoom.value.detach();
|
|
149
171
|
}
|
|
150
172
|
await nextTick();
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
});
|
|
173
|
+
// For arrays we use click-to-open per image, for single we keep existing behavior.
|
|
174
|
+
if (contentType?.startsWith('image') && !Array.isArray(url.value)) {
|
|
175
|
+
zoom.value = mediumZoom(img.value, { margin: 24 });
|
|
155
176
|
}
|
|
156
177
|
|
|
157
178
|
}, { immediate: true });
|
|
158
179
|
|
|
180
|
+
function openZoom(index) {
|
|
181
|
+
if (!urls.value?.length) return;
|
|
182
|
+
const el = Array.isArray(img.value) ? img.value[index] : img.value;
|
|
183
|
+
if (!el) return;
|
|
184
|
+
const z = mediumZoom(el, { margin: 24 });
|
|
185
|
+
z.open();
|
|
186
|
+
}
|
|
187
|
+
|
|
159
188
|
</script>
|
package/custom/uploader.vue
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
}"
|
|
25
25
|
>
|
|
26
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" />
|
|
27
|
+
<img v-if="typeof imgPreview === 'string' && imgPreview" :src="imgPreview" class="w-100 mt-4 rounded-lg h-40 object-contain" />
|
|
28
28
|
|
|
29
29
|
<svg v-else class="w-8 h-8 mb-4 text-gray-500 dark:text-gray-400 !text-lightDropzoneText dark:!text-darkDropzoneText" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 16">
|
|
30
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"/>
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
</template>
|
|
67
67
|
|
|
68
68
|
<script setup lang="ts">
|
|
69
|
-
import { computed, ref, onMounted, watch } from 'vue'
|
|
69
|
+
import { computed, ref, onMounted, watch, getCurrentInstance } from 'vue'
|
|
70
70
|
import { callAdminForthApi } from '@/utils'
|
|
71
71
|
import { IconMagic } from '@iconify-prerendered/vue-mdi';
|
|
72
72
|
import { useI18n } from 'vue-i18n';
|
|
@@ -75,7 +75,8 @@ import { useRoute } from 'vue-router';
|
|
|
75
75
|
const route = useRoute();
|
|
76
76
|
const { t } = useI18n();
|
|
77
77
|
|
|
78
|
-
const
|
|
78
|
+
const instanceUid = getCurrentInstance()?.uid ?? Math.floor(Math.random() * 1000000);
|
|
79
|
+
const inputId = computed(() => `dropzone-file-${props.meta.pluginInstanceId}-${instanceUid}`);
|
|
79
80
|
|
|
80
81
|
import ImageGenerator from '@@/plugins/UploadPlugin/imageGenerator.vue';
|
|
81
82
|
import adminforth from '@/adminforth';
|
|
@@ -84,6 +85,7 @@ import adminforth from '@/adminforth';
|
|
|
84
85
|
const props = defineProps({
|
|
85
86
|
meta: Object,
|
|
86
87
|
record: Object,
|
|
88
|
+
value: [String, Number, Boolean, Object, Array, null],
|
|
87
89
|
})
|
|
88
90
|
|
|
89
91
|
const emit = defineEmits([
|
|
@@ -102,10 +104,9 @@ const progress = ref(0);
|
|
|
102
104
|
|
|
103
105
|
const uploaded = ref(false);
|
|
104
106
|
const uploadedSize = ref(0);
|
|
105
|
-
|
|
106
107
|
const downloadFileUrl = ref('');
|
|
107
108
|
|
|
108
|
-
watch(
|
|
109
|
+
watch(uploaded, (value) => {
|
|
109
110
|
emit('update:emptiness', !value);
|
|
110
111
|
});
|
|
111
112
|
|
|
@@ -129,7 +130,8 @@ onMounted(async () => {
|
|
|
129
130
|
queryValues = {};
|
|
130
131
|
}
|
|
131
132
|
|
|
132
|
-
|
|
133
|
+
|
|
134
|
+
if (typeof queryValues?.[props.meta.pathColumnName] === 'string' && queryValues[props.meta.pathColumnName]) {
|
|
133
135
|
downloadFileUrl.value = queryValues[props.meta.pathColumnName];
|
|
134
136
|
|
|
135
137
|
const resp = await callAdminForthApi({
|
|
@@ -163,7 +165,28 @@ onMounted(async () => {
|
|
|
163
165
|
files: [file],
|
|
164
166
|
},
|
|
165
167
|
});
|
|
166
|
-
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const existingValue = (props as any).value;
|
|
171
|
+
const existingFilePath =
|
|
172
|
+
typeof existingValue === 'string' && existingValue.trim() ? existingValue : null;
|
|
173
|
+
|
|
174
|
+
if (!uploaded.value && existingFilePath) {
|
|
175
|
+
const resp = await callAdminForthApi({
|
|
176
|
+
path: `/plugin/${props.meta.pluginInstanceId}/get-file-download-url`,
|
|
177
|
+
method: 'POST',
|
|
178
|
+
body: { filePath: existingFilePath },
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (!resp?.error && resp?.url) {
|
|
182
|
+
imgPreview.value = resp.url;
|
|
183
|
+
uploaded.value = true;
|
|
184
|
+
emit('update:emptiness', false);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!uploaded.value && props.record?.[previewColumnName]) {
|
|
167
190
|
imgPreview.value = props.record[previewColumnName];
|
|
168
191
|
uploaded.value = true;
|
|
169
192
|
emit('update:emptiness', false);
|
package/dist/custom/preview.vue
CHANGED
|
@@ -1,31 +1,36 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div>
|
|
3
|
-
<template v-if="
|
|
4
|
-
<
|
|
5
|
-
v-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
3
|
+
<template v-if="urls.length">
|
|
4
|
+
<div class="flex flex-wrap gap-2 items-start">
|
|
5
|
+
<template v-for="(u, i) in urls" :key="`${u}-${i}`">
|
|
6
|
+
<img
|
|
7
|
+
v-if="guessContentTypeFromUrl(u)?.startsWith('image')"
|
|
8
|
+
:src="u"
|
|
9
|
+
class="rounded-md cursor-zoom-in"
|
|
10
|
+
:style="[maxWidth, minWidth]"
|
|
11
|
+
ref="img"
|
|
12
|
+
@click.stop="openZoom(i)"
|
|
13
|
+
/>
|
|
14
|
+
<video
|
|
15
|
+
v-else-if="guessContentTypeFromUrl(u)?.startsWith('video')"
|
|
16
|
+
:src="u"
|
|
17
|
+
class="rounded-md"
|
|
18
|
+
controls
|
|
19
|
+
@click.stop
|
|
20
|
+
/>
|
|
21
|
+
<a
|
|
22
|
+
v-else
|
|
23
|
+
:href="u"
|
|
24
|
+
target="_blank"
|
|
25
|
+
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-lightPrimary 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"
|
|
26
|
+
>
|
|
27
|
+
<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">
|
|
28
|
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
|
29
|
+
</svg>
|
|
30
|
+
{{ $t('Download file') }}
|
|
31
|
+
</a>
|
|
32
|
+
</template>
|
|
33
|
+
</div>
|
|
29
34
|
</template>
|
|
30
35
|
|
|
31
36
|
|
|
@@ -74,8 +79,10 @@ const props = defineProps({
|
|
|
74
79
|
const trueContentType = ref(null);
|
|
75
80
|
|
|
76
81
|
onMounted(async () => {
|
|
77
|
-
|
|
78
|
-
|
|
82
|
+
// try to get HEAD request (single url only). For arrays we just guess by extension.
|
|
83
|
+
if (!url.value) return;
|
|
84
|
+
if (Array.isArray(url.value)) return;
|
|
85
|
+
try {
|
|
79
86
|
const response = await fetch(url.value, {
|
|
80
87
|
method: 'HEAD',
|
|
81
88
|
mode: 'cors',
|
|
@@ -101,6 +108,11 @@ const url = computed(() => {
|
|
|
101
108
|
return props.record[`previewUrl_${props.meta.pluginInstanceId}`];
|
|
102
109
|
});
|
|
103
110
|
|
|
111
|
+
const urls = computed(() => {
|
|
112
|
+
if (!url.value) return [];
|
|
113
|
+
return Array.isArray(url.value) ? url.value : [url.value];
|
|
114
|
+
});
|
|
115
|
+
|
|
104
116
|
const maxWidth = computed(() => {
|
|
105
117
|
const isShowPage = route.path.includes('/show/');
|
|
106
118
|
const width = isShowPage
|
|
@@ -124,6 +136,7 @@ const guessedContentType = computed(() => {
|
|
|
124
136
|
if (!url.value) {
|
|
125
137
|
return null;
|
|
126
138
|
}
|
|
139
|
+
if (Array.isArray(url.value)) return null;
|
|
127
140
|
const u = new URL(url.value, url.value.startsWith('http') ? undefined : location.origin);
|
|
128
141
|
return guessContentType(u.pathname);
|
|
129
142
|
});
|
|
@@ -141,6 +154,15 @@ function guessContentType(url) {
|
|
|
141
154
|
}
|
|
142
155
|
}
|
|
143
156
|
|
|
157
|
+
function guessContentTypeFromUrl(u) {
|
|
158
|
+
if (!u) return null;
|
|
159
|
+
try {
|
|
160
|
+
const parsed = new URL(u, u.startsWith('http') ? undefined : location.origin);
|
|
161
|
+
return guessContentType(parsed.pathname);
|
|
162
|
+
} catch (e) {
|
|
163
|
+
return guessContentType(u);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
144
166
|
|
|
145
167
|
watch([contentType], async ([contentType]) => {
|
|
146
168
|
// since content type might change after true guessing (HEAD request might be slow) we need to try initializing zoom again
|
|
@@ -148,12 +170,19 @@ watch([contentType], async ([contentType]) => {
|
|
|
148
170
|
zoom.value.detach();
|
|
149
171
|
}
|
|
150
172
|
await nextTick();
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
});
|
|
173
|
+
// For arrays we use click-to-open per image, for single we keep existing behavior.
|
|
174
|
+
if (contentType?.startsWith('image') && !Array.isArray(url.value)) {
|
|
175
|
+
zoom.value = mediumZoom(img.value, { margin: 24 });
|
|
155
176
|
}
|
|
156
177
|
|
|
157
178
|
}, { immediate: true });
|
|
158
179
|
|
|
180
|
+
function openZoom(index) {
|
|
181
|
+
if (!urls.value?.length) return;
|
|
182
|
+
const el = Array.isArray(img.value) ? img.value[index] : img.value;
|
|
183
|
+
if (!el) return;
|
|
184
|
+
const z = mediumZoom(el, { margin: 24 });
|
|
185
|
+
z.open();
|
|
186
|
+
}
|
|
187
|
+
|
|
159
188
|
</script>
|
package/dist/custom/uploader.vue
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
}"
|
|
25
25
|
>
|
|
26
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" />
|
|
27
|
+
<img v-if="typeof imgPreview === 'string' && imgPreview" :src="imgPreview" class="w-100 mt-4 rounded-lg h-40 object-contain" />
|
|
28
28
|
|
|
29
29
|
<svg v-else class="w-8 h-8 mb-4 text-gray-500 dark:text-gray-400 !text-lightDropzoneText dark:!text-darkDropzoneText" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 16">
|
|
30
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"/>
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
</template>
|
|
67
67
|
|
|
68
68
|
<script setup lang="ts">
|
|
69
|
-
import { computed, ref, onMounted, watch } from 'vue'
|
|
69
|
+
import { computed, ref, onMounted, watch, getCurrentInstance } from 'vue'
|
|
70
70
|
import { callAdminForthApi } from '@/utils'
|
|
71
71
|
import { IconMagic } from '@iconify-prerendered/vue-mdi';
|
|
72
72
|
import { useI18n } from 'vue-i18n';
|
|
@@ -75,7 +75,8 @@ import { useRoute } from 'vue-router';
|
|
|
75
75
|
const route = useRoute();
|
|
76
76
|
const { t } = useI18n();
|
|
77
77
|
|
|
78
|
-
const
|
|
78
|
+
const instanceUid = getCurrentInstance()?.uid ?? Math.floor(Math.random() * 1000000);
|
|
79
|
+
const inputId = computed(() => `dropzone-file-${props.meta.pluginInstanceId}-${instanceUid}`);
|
|
79
80
|
|
|
80
81
|
import ImageGenerator from '@@/plugins/UploadPlugin/imageGenerator.vue';
|
|
81
82
|
import adminforth from '@/adminforth';
|
|
@@ -84,6 +85,7 @@ import adminforth from '@/adminforth';
|
|
|
84
85
|
const props = defineProps({
|
|
85
86
|
meta: Object,
|
|
86
87
|
record: Object,
|
|
88
|
+
value: [String, Number, Boolean, Object, Array, null],
|
|
87
89
|
})
|
|
88
90
|
|
|
89
91
|
const emit = defineEmits([
|
|
@@ -102,10 +104,9 @@ const progress = ref(0);
|
|
|
102
104
|
|
|
103
105
|
const uploaded = ref(false);
|
|
104
106
|
const uploadedSize = ref(0);
|
|
105
|
-
|
|
106
107
|
const downloadFileUrl = ref('');
|
|
107
108
|
|
|
108
|
-
watch(
|
|
109
|
+
watch(uploaded, (value) => {
|
|
109
110
|
emit('update:emptiness', !value);
|
|
110
111
|
});
|
|
111
112
|
|
|
@@ -129,7 +130,8 @@ onMounted(async () => {
|
|
|
129
130
|
queryValues = {};
|
|
130
131
|
}
|
|
131
132
|
|
|
132
|
-
|
|
133
|
+
|
|
134
|
+
if (typeof queryValues?.[props.meta.pathColumnName] === 'string' && queryValues[props.meta.pathColumnName]) {
|
|
133
135
|
downloadFileUrl.value = queryValues[props.meta.pathColumnName];
|
|
134
136
|
|
|
135
137
|
const resp = await callAdminForthApi({
|
|
@@ -163,7 +165,28 @@ onMounted(async () => {
|
|
|
163
165
|
files: [file],
|
|
164
166
|
},
|
|
165
167
|
});
|
|
166
|
-
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const existingValue = (props as any).value;
|
|
171
|
+
const existingFilePath =
|
|
172
|
+
typeof existingValue === 'string' && existingValue.trim() ? existingValue : null;
|
|
173
|
+
|
|
174
|
+
if (!uploaded.value && existingFilePath) {
|
|
175
|
+
const resp = await callAdminForthApi({
|
|
176
|
+
path: `/plugin/${props.meta.pluginInstanceId}/get-file-download-url`,
|
|
177
|
+
method: 'POST',
|
|
178
|
+
body: { filePath: existingFilePath },
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (!resp?.error && resp?.url) {
|
|
182
|
+
imgPreview.value = resp.url;
|
|
183
|
+
uploaded.value = true;
|
|
184
|
+
emit('update:emptiness', false);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!uploaded.value && props.record?.[previewColumnName]) {
|
|
167
190
|
imgPreview.value = props.record[previewColumnName];
|
|
168
191
|
uploaded.value = true;
|
|
169
192
|
emit('update:emptiness', false);
|
package/dist/index.js
CHANGED
|
@@ -33,6 +33,30 @@ export default class UploadPlugin extends AdminForthPlugin {
|
|
|
33
33
|
this.rateLimiter = new RateLimiter((_c = this.options.generation.rateLimit) === null || _c === void 0 ? void 0 : _c.limit);
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
+
normalizePaths(value) {
|
|
37
|
+
if (!value)
|
|
38
|
+
return [];
|
|
39
|
+
if (Array.isArray(value))
|
|
40
|
+
return value.filter(Boolean).map(String);
|
|
41
|
+
return [String(value)];
|
|
42
|
+
}
|
|
43
|
+
callStorageAdapter(primaryMethod, fallbackMethod, filePath) {
|
|
44
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
45
|
+
var _a;
|
|
46
|
+
const adapter = this.options.storageAdapter;
|
|
47
|
+
const fn = (_a = adapter === null || adapter === void 0 ? void 0 : adapter[primaryMethod]) !== null && _a !== void 0 ? _a : adapter === null || adapter === void 0 ? void 0 : adapter[fallbackMethod];
|
|
48
|
+
if (typeof fn !== 'function') {
|
|
49
|
+
throw new Error(`Storage adapter is missing method "${primaryMethod}" (fallback "${fallbackMethod}")`);
|
|
50
|
+
}
|
|
51
|
+
yield fn.call(adapter, filePath);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
markKeyForNotDeletion(filePath) {
|
|
55
|
+
return this.callStorageAdapter('markKeyForNotDeletion', 'markKeyForNotDeletation', filePath);
|
|
56
|
+
}
|
|
57
|
+
markKeyForDeletion(filePath) {
|
|
58
|
+
return this.callStorageAdapter('markKeyForDeletion', 'markKeyForDeletation', filePath);
|
|
59
|
+
}
|
|
36
60
|
generateImages(jobId, prompt, recordId, adminUser, headers) {
|
|
37
61
|
return __awaiter(this, void 0, void 0, function* () {
|
|
38
62
|
var _a, _b;
|
|
@@ -108,13 +132,19 @@ export default class UploadPlugin extends AdminForthPlugin {
|
|
|
108
132
|
}
|
|
109
133
|
genPreviewUrl(record) {
|
|
110
134
|
return __awaiter(this, void 0, void 0, function* () {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
135
|
+
const value = record === null || record === void 0 ? void 0 : record[this.options.pathColumnName];
|
|
136
|
+
const paths = this.normalizePaths(value);
|
|
137
|
+
if (!paths.length)
|
|
114
138
|
return;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
139
|
+
const makeUrl = (filePath) => __awaiter(this, void 0, void 0, function* () {
|
|
140
|
+
var _a;
|
|
141
|
+
if ((_a = this.options.preview) === null || _a === void 0 ? void 0 : _a.previewUrl) {
|
|
142
|
+
return this.options.preview.previewUrl({ filePath });
|
|
143
|
+
}
|
|
144
|
+
return yield this.options.storageAdapter.getDownloadUrl(filePath, 1800);
|
|
145
|
+
});
|
|
146
|
+
const urls = yield Promise.all(paths.map(makeUrl));
|
|
147
|
+
record[`previewUrl_${this.pluginInstanceId}`] = Array.isArray(value) ? urls : urls[0];
|
|
118
148
|
});
|
|
119
149
|
}
|
|
120
150
|
modifyResourceConfig(adminforth, resourceConfig) {
|
|
@@ -188,16 +218,12 @@ export default class UploadPlugin extends AdminForthPlugin {
|
|
|
188
218
|
// in afterSave hook, aremove tag adminforth-not-yet-used from the file
|
|
189
219
|
resourceConfig.hooks.create.afterSave.push((_a) => __awaiter(this, [_a], void 0, function* ({ record }) {
|
|
190
220
|
process.env.HEAVY_DEBUG && console.log('πΎπΎ after save ', record === null || record === void 0 ? void 0 : record.id);
|
|
191
|
-
|
|
192
|
-
|
|
221
|
+
const paths = this.normalizePaths(record === null || record === void 0 ? void 0 : record[pathColumnName]);
|
|
222
|
+
yield Promise.all(paths.map((p) => __awaiter(this, void 0, void 0, function* () {
|
|
223
|
+
process.env.HEAVY_DEBUG && console.log('πͺ₯πͺ₯ remove ObjectTagging', p);
|
|
193
224
|
// let it crash if it fails: this is a new file which just was uploaded.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
else {
|
|
198
|
-
yield this.options.storageAdapter.markKeyForNotDeletation(record[pathColumnName]);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
225
|
+
yield this.markKeyForNotDeletion(p);
|
|
226
|
+
})));
|
|
201
227
|
return { ok: true };
|
|
202
228
|
}));
|
|
203
229
|
// ** HOOKS FOR SHOW **//
|
|
@@ -228,51 +254,45 @@ export default class UploadPlugin extends AdminForthPlugin {
|
|
|
228
254
|
// ** HOOKS FOR DELETE **//
|
|
229
255
|
// add delete hook which sets tag adminforth-candidate-for-cleanup to true
|
|
230
256
|
resourceConfig.hooks.delete.afterSave.push((_a) => __awaiter(this, [_a], void 0, function* ({ record }) {
|
|
231
|
-
|
|
257
|
+
const paths = this.normalizePaths(record === null || record === void 0 ? void 0 : record[pathColumnName]);
|
|
258
|
+
yield Promise.all(paths.map((p) => __awaiter(this, void 0, void 0, function* () {
|
|
232
259
|
try {
|
|
233
|
-
|
|
234
|
-
yield this.options.storageAdapter.markKeyForDeletion(record[pathColumnName]);
|
|
235
|
-
}
|
|
236
|
-
else {
|
|
237
|
-
yield this.options.storageAdapter.markKeyForDeletation(record[pathColumnName]);
|
|
238
|
-
}
|
|
260
|
+
yield this.markKeyForDeletion(p);
|
|
239
261
|
}
|
|
240
262
|
catch (e) {
|
|
241
263
|
// file might be e.g. already deleted, so we catch error
|
|
242
|
-
console.error(`Error setting tag ${ADMINFORTH_NOT_YET_USED_TAG} to true for object ${
|
|
264
|
+
console.error(`Error setting tag ${ADMINFORTH_NOT_YET_USED_TAG} to true for object ${p}. File will not be auto-cleaned up`, e);
|
|
243
265
|
}
|
|
244
|
-
}
|
|
266
|
+
})));
|
|
245
267
|
return { ok: true };
|
|
246
268
|
}));
|
|
247
269
|
// ** HOOKS FOR EDIT **//
|
|
248
270
|
// add edit postSave hook to delete old file and remove tag from new file
|
|
249
271
|
resourceConfig.hooks.edit.afterSave.push((_a) => __awaiter(this, [_a], void 0, function* ({ updates, oldRecord }) {
|
|
250
272
|
if (updates[pathColumnName] || updates[pathColumnName] === null) {
|
|
251
|
-
|
|
273
|
+
const oldValue = oldRecord === null || oldRecord === void 0 ? void 0 : oldRecord[pathColumnName];
|
|
274
|
+
const newValue = updates === null || updates === void 0 ? void 0 : updates[pathColumnName];
|
|
275
|
+
const oldPaths = this.normalizePaths(oldValue);
|
|
276
|
+
const newPaths = newValue === null ? [] : this.normalizePaths(newValue);
|
|
277
|
+
const oldSet = new Set(oldPaths);
|
|
278
|
+
const newSet = new Set(newPaths);
|
|
279
|
+
const toDelete = oldPaths.filter((p) => !newSet.has(p));
|
|
280
|
+
const toKeep = newPaths.filter((p) => !oldSet.has(p));
|
|
281
|
+
yield Promise.all(toDelete.map((p) => __awaiter(this, void 0, void 0, function* () {
|
|
252
282
|
// put tag to delete old file
|
|
253
283
|
try {
|
|
254
|
-
|
|
255
|
-
yield this.options.storageAdapter.markKeyForDeletion(oldRecord[pathColumnName]);
|
|
256
|
-
}
|
|
257
|
-
else {
|
|
258
|
-
yield this.options.storageAdapter.markKeyForDeletation(oldRecord[pathColumnName]);
|
|
259
|
-
}
|
|
284
|
+
yield this.markKeyForDeletion(p);
|
|
260
285
|
}
|
|
261
286
|
catch (e) {
|
|
262
287
|
// file might be e.g. already deleted, so we catch error
|
|
263
|
-
console.error(`Error setting tag ${ADMINFORTH_NOT_YET_USED_TAG} to true for object ${
|
|
288
|
+
console.error(`Error setting tag ${ADMINFORTH_NOT_YET_USED_TAG} to true for object ${p}. File will not be auto-cleaned up`, e);
|
|
264
289
|
}
|
|
265
|
-
}
|
|
266
|
-
|
|
290
|
+
})));
|
|
291
|
+
yield Promise.all(toKeep.map((p) => __awaiter(this, void 0, void 0, function* () {
|
|
267
292
|
// remove tag from new file
|
|
268
|
-
// in this case we let it crash if it fails: this is a new file which just was uploaded.
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
else {
|
|
273
|
-
yield this.options.storageAdapter.markKeyForNotDeletation(updates[pathColumnName]);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
293
|
+
// in this case we let it crash if it fails: this is a new file which just was uploaded.
|
|
294
|
+
yield this.markKeyForNotDeletion(p);
|
|
295
|
+
})));
|
|
276
296
|
}
|
|
277
297
|
return { ok: true };
|
|
278
298
|
}));
|
package/index.ts
CHANGED
|
@@ -41,6 +41,29 @@ export default class UploadPlugin extends AdminForthPlugin {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
private normalizePaths(value: any): string[] {
|
|
45
|
+
if (!value) return [];
|
|
46
|
+
if (Array.isArray(value)) return value.filter(Boolean).map(String);
|
|
47
|
+
return [String(value)];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private async callStorageAdapter(primaryMethod: string, fallbackMethod: string, filePath: string) {
|
|
51
|
+
const adapter: any = this.options.storageAdapter as any;
|
|
52
|
+
const fn = adapter?.[primaryMethod] ?? adapter?.[fallbackMethod];
|
|
53
|
+
if (typeof fn !== 'function') {
|
|
54
|
+
throw new Error(`Storage adapter is missing method "${primaryMethod}" (fallback "${fallbackMethod}")`);
|
|
55
|
+
}
|
|
56
|
+
await fn.call(adapter, filePath);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private markKeyForNotDeletion(filePath: string) {
|
|
60
|
+
return this.callStorageAdapter('markKeyForNotDeletion', 'markKeyForNotDeletation', filePath);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private markKeyForDeletion(filePath: string) {
|
|
64
|
+
return this.callStorageAdapter('markKeyForDeletion', 'markKeyForDeletation', filePath);
|
|
65
|
+
}
|
|
66
|
+
|
|
44
67
|
private async generateImages(jobId: string, prompt: string, recordId: any, adminUser: any, headers: any) {
|
|
45
68
|
if (this.options.generation.rateLimit?.limit) {
|
|
46
69
|
// rate limit
|
|
@@ -128,13 +151,19 @@ export default class UploadPlugin extends AdminForthPlugin {
|
|
|
128
151
|
}
|
|
129
152
|
|
|
130
153
|
async genPreviewUrl(record: any) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
const previewUrl = await this.options.storageAdapter.getDownloadUrl(record[this.options.pathColumnName], 1800);
|
|
154
|
+
const value = record?.[this.options.pathColumnName];
|
|
155
|
+
const paths = this.normalizePaths(value);
|
|
156
|
+
if (!paths.length) return;
|
|
136
157
|
|
|
137
|
-
|
|
158
|
+
const makeUrl = async (filePath: string) => {
|
|
159
|
+
if (this.options.preview?.previewUrl) {
|
|
160
|
+
return this.options.preview.previewUrl({ filePath });
|
|
161
|
+
}
|
|
162
|
+
return await this.options.storageAdapter.getDownloadUrl(filePath, 1800);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const urls = await Promise.all(paths.map(makeUrl));
|
|
166
|
+
record[`previewUrl_${this.pluginInstanceId}`] = Array.isArray(value) ? urls : urls[0];
|
|
138
167
|
}
|
|
139
168
|
|
|
140
169
|
async modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
|
|
@@ -214,15 +243,12 @@ export default class UploadPlugin extends AdminForthPlugin {
|
|
|
214
243
|
resourceConfig.hooks.create.afterSave.push(async ({ record }: { record: any }) => {
|
|
215
244
|
process.env.HEAVY_DEBUG && console.log('πΎπΎ after save ', record?.id);
|
|
216
245
|
|
|
217
|
-
|
|
218
|
-
|
|
246
|
+
const paths = this.normalizePaths(record?.[pathColumnName]);
|
|
247
|
+
await Promise.all(paths.map(async (p) => {
|
|
248
|
+
process.env.HEAVY_DEBUG && console.log('πͺ₯πͺ₯ remove ObjectTagging', p);
|
|
219
249
|
// let it crash if it fails: this is a new file which just was uploaded.
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
} else {
|
|
223
|
-
await this.options.storageAdapter.markKeyForNotDeletation(record[pathColumnName]);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
250
|
+
await this.markKeyForNotDeletion(p);
|
|
251
|
+
}));
|
|
226
252
|
return { ok: true };
|
|
227
253
|
});
|
|
228
254
|
|
|
@@ -262,18 +288,15 @@ export default class UploadPlugin extends AdminForthPlugin {
|
|
|
262
288
|
|
|
263
289
|
// add delete hook which sets tag adminforth-candidate-for-cleanup to true
|
|
264
290
|
resourceConfig.hooks.delete.afterSave.push(async ({ record }: { record: any }) => {
|
|
265
|
-
|
|
291
|
+
const paths = this.normalizePaths(record?.[pathColumnName]);
|
|
292
|
+
await Promise.all(paths.map(async (p) => {
|
|
266
293
|
try {
|
|
267
|
-
|
|
268
|
-
await this.options.storageAdapter.markKeyForDeletion(record[pathColumnName]);
|
|
269
|
-
} else {
|
|
270
|
-
await this.options.storageAdapter.markKeyForDeletation(record[pathColumnName]);
|
|
271
|
-
}
|
|
294
|
+
await this.markKeyForDeletion(p);
|
|
272
295
|
} catch (e) {
|
|
273
296
|
// file might be e.g. already deleted, so we catch error
|
|
274
|
-
console.error(`Error setting tag ${ADMINFORTH_NOT_YET_USED_TAG} to true for object ${
|
|
297
|
+
console.error(`Error setting tag ${ADMINFORTH_NOT_YET_USED_TAG} to true for object ${p}. File will not be auto-cleaned up`, e);
|
|
275
298
|
}
|
|
276
|
-
}
|
|
299
|
+
}));
|
|
277
300
|
return { ok: true };
|
|
278
301
|
});
|
|
279
302
|
|
|
@@ -286,28 +309,33 @@ export default class UploadPlugin extends AdminForthPlugin {
|
|
|
286
309
|
resourceConfig.hooks.edit.afterSave.push(async ({ updates, oldRecord }: { updates: any, oldRecord: any }) => {
|
|
287
310
|
|
|
288
311
|
if (updates[pathColumnName] || updates[pathColumnName] === null) {
|
|
289
|
-
|
|
312
|
+
const oldValue = oldRecord?.[pathColumnName];
|
|
313
|
+
const newValue = updates?.[pathColumnName];
|
|
314
|
+
|
|
315
|
+
const oldPaths = this.normalizePaths(oldValue);
|
|
316
|
+
const newPaths = newValue === null ? [] : this.normalizePaths(newValue);
|
|
317
|
+
|
|
318
|
+
const oldSet = new Set(oldPaths);
|
|
319
|
+
const newSet = new Set(newPaths);
|
|
320
|
+
|
|
321
|
+
const toDelete = oldPaths.filter((p) => !newSet.has(p));
|
|
322
|
+
const toKeep = newPaths.filter((p) => !oldSet.has(p));
|
|
323
|
+
|
|
324
|
+
await Promise.all(toDelete.map(async (p) => {
|
|
290
325
|
// put tag to delete old file
|
|
291
326
|
try {
|
|
292
|
-
|
|
293
|
-
await this.options.storageAdapter.markKeyForDeletion(oldRecord[pathColumnName]);
|
|
294
|
-
} else {
|
|
295
|
-
await this.options.storageAdapter.markKeyForDeletation(oldRecord[pathColumnName]);
|
|
296
|
-
}
|
|
327
|
+
await this.markKeyForDeletion(p);
|
|
297
328
|
} catch (e) {
|
|
298
329
|
// file might be e.g. already deleted, so we catch error
|
|
299
|
-
console.error(`Error setting tag ${ADMINFORTH_NOT_YET_USED_TAG} to true for object ${
|
|
330
|
+
console.error(`Error setting tag ${ADMINFORTH_NOT_YET_USED_TAG} to true for object ${p}. File will not be auto-cleaned up`, e);
|
|
300
331
|
}
|
|
301
|
-
}
|
|
302
|
-
|
|
332
|
+
}));
|
|
333
|
+
|
|
334
|
+
await Promise.all(toKeep.map(async (p) => {
|
|
303
335
|
// remove tag from new file
|
|
304
|
-
// in this case we let it crash if it fails: this is a new file which just was uploaded.
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
} else {
|
|
308
|
-
await this.options.storageAdapter.markKeyForNotDeletation(updates[pathColumnName]);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
336
|
+
// in this case we let it crash if it fails: this is a new file which just was uploaded.
|
|
337
|
+
await this.markKeyForNotDeletion(p);
|
|
338
|
+
}));
|
|
311
339
|
}
|
|
312
340
|
return { ok: true };
|
|
313
341
|
});
|