@adminforth/upload 2.6.0 β†’ 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 CHANGED
@@ -11,5 +11,5 @@ custom/preview.vue
11
11
  custom/tsconfig.json
12
12
  custom/uploader.vue
13
13
 
14
- sent 49,208 bytes received 134 bytes 98,684.00 bytes/sec
15
- total size is 48,726 speedup is 0.99
14
+ sent 51,239 bytes received 134 bytes 102,746.00 bytes/sec
15
+ total size is 50,757 speedup is 0.99
@@ -376,20 +376,22 @@ async function generateImages() {
376
376
  method: 'POST',
377
377
  body: { jobId },
378
378
  });
379
- if (jobResponse?.error) {
380
- error = jobResponse.error;
381
- break;
382
- };
383
- jobStatus = jobResponse?.job?.status;
384
- if (jobStatus === 'failed') {
385
- error = jobResponse?.job?.error || $t('Image generation job failed');
386
- }
387
- if (jobStatus === 'timeout') {
388
- error = jobResponse?.job?.error || $t('Image generation job timeout');
379
+ if (jobResponse !== null) {
380
+ if (jobResponse?.error) {
381
+ error = jobResponse.error;
382
+ break;
383
+ };
384
+ jobStatus = jobResponse?.job?.status;
385
+ if (jobStatus === 'failed') {
386
+ error = jobResponse?.job?.error || $t('Image generation job failed');
387
+ }
388
+ if (jobStatus === 'timeout') {
389
+ error = jobResponse?.job?.error || $t('Image generation job timeout');
390
+ }
389
391
  }
390
392
  await new Promise((resolve) => setTimeout(resolve, 2000));
391
- } while (jobStatus === 'in_progress')
392
-
393
+ } while (jobStatus === 'in_progress' || jobStatus === null)
394
+
393
395
  if (error) {
394
396
  adminforth.alert({
395
397
  message: error,
@@ -1,31 +1,36 @@
1
1
  <template>
2
2
  <div>
3
- <template v-if="url">
4
- <img
5
- v-if="contentType && contentType.startsWith('image')"
6
- :src="url"
7
- class="rounded-md"
8
- :style="[maxWidth, minWidth]"
9
- ref="img"
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-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"
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
- {{ $t('Download file') }}
28
- </a>
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
- // try to get HEAD request
78
- try {
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
- if (contentType?.startsWith('image')) {
152
- zoom.value = mediumZoom(img.value, {
153
- margin: 24,
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>
@@ -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 inputId = computed(() => `dropzone-file-${props.meta.pluginInstanceId}`);
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(() => uploaded, (value) => {
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
- if (queryValues[props.meta.pathColumnName]) {
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
- } else if (props.record[previewColumnName]) {
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);
@@ -376,20 +376,22 @@ async function generateImages() {
376
376
  method: 'POST',
377
377
  body: { jobId },
378
378
  });
379
- if (jobResponse?.error) {
380
- error = jobResponse.error;
381
- break;
382
- };
383
- jobStatus = jobResponse?.job?.status;
384
- if (jobStatus === 'failed') {
385
- error = jobResponse?.job?.error || $t('Image generation job failed');
386
- }
387
- if (jobStatus === 'timeout') {
388
- error = jobResponse?.job?.error || $t('Image generation job timeout');
379
+ if (jobResponse !== null) {
380
+ if (jobResponse?.error) {
381
+ error = jobResponse.error;
382
+ break;
383
+ };
384
+ jobStatus = jobResponse?.job?.status;
385
+ if (jobStatus === 'failed') {
386
+ error = jobResponse?.job?.error || $t('Image generation job failed');
387
+ }
388
+ if (jobStatus === 'timeout') {
389
+ error = jobResponse?.job?.error || $t('Image generation job timeout');
390
+ }
389
391
  }
390
392
  await new Promise((resolve) => setTimeout(resolve, 2000));
391
- } while (jobStatus === 'in_progress')
392
-
393
+ } while (jobStatus === 'in_progress' || jobStatus === null)
394
+
393
395
  if (error) {
394
396
  adminforth.alert({
395
397
  message: error,
@@ -1,31 +1,36 @@
1
1
  <template>
2
2
  <div>
3
- <template v-if="url">
4
- <img
5
- v-if="contentType && contentType.startsWith('image')"
6
- :src="url"
7
- class="rounded-md"
8
- :style="[maxWidth, minWidth]"
9
- ref="img"
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-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"
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
- {{ $t('Download file') }}
28
- </a>
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
- // try to get HEAD request
78
- try {
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
- if (contentType?.startsWith('image')) {
152
- zoom.value = mediumZoom(img.value, {
153
- margin: 24,
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>
@@ -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 inputId = computed(() => `dropzone-file-${props.meta.pluginInstanceId}`);
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(() => uploaded, (value) => {
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
- if (queryValues[props.meta.pathColumnName]) {
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
- } else if (props.record[previewColumnName]) {
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
- var _a;
112
- if ((_a = this.options.preview) === null || _a === void 0 ? void 0 : _a.previewUrl) {
113
- record[`previewUrl_${this.pluginInstanceId}`] = this.options.preview.previewUrl({ filePath: record[this.options.pathColumnName] });
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
- const previewUrl = yield this.options.storageAdapter.getDownloadUrl(record[this.options.pathColumnName], 1800);
117
- record[`previewUrl_${this.pluginInstanceId}`] = previewUrl;
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
- if (record[pathColumnName]) {
192
- process.env.HEAVY_DEBUG && console.log('πŸͺ₯πŸͺ₯ remove ObjectTagging', record[pathColumnName]);
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
- if (this.options.storageAdapter.markKeyForNotDeletion !== undefined) {
195
- yield this.options.storageAdapter.markKeyForNotDeletion(record[pathColumnName]);
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
- if (record[pathColumnName]) {
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
- if (this.options.storageAdapter.markKeyForDeletion !== undefined) {
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 ${record[pathColumnName]}. File will not be auto-cleaned up`, e);
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
- if (oldRecord[pathColumnName]) {
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
- if (this.options.storageAdapter.markKeyForDeletion !== undefined) {
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 ${oldRecord[pathColumnName]}. File will not be auto-cleaned up`, e);
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
- if (updates[pathColumnName] !== null) {
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
- if (this.options.storageAdapter.markKeyForNotDeletion !== undefined) {
270
- yield this.options.storageAdapter.markKeyForNotDeletion(updates[pathColumnName]);
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
- if (this.options.preview?.previewUrl) {
132
- record[`previewUrl_${this.pluginInstanceId}`] = this.options.preview.previewUrl({ filePath: record[this.options.pathColumnName] });
133
- return;
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
- record[`previewUrl_${this.pluginInstanceId}`] = previewUrl;
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
- if (record[pathColumnName]) {
218
- process.env.HEAVY_DEBUG && console.log('πŸͺ₯πŸͺ₯ remove ObjectTagging', record[pathColumnName]);
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
- if (this.options.storageAdapter.markKeyForNotDeletion !== undefined) {
221
- await this.options.storageAdapter.markKeyForNotDeletion(record[pathColumnName]);
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
- if (record[pathColumnName]) {
291
+ const paths = this.normalizePaths(record?.[pathColumnName]);
292
+ await Promise.all(paths.map(async (p) => {
266
293
  try {
267
- if (this.options.storageAdapter.markKeyForDeletion !== undefined) {
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 ${record[pathColumnName]}. File will not be auto-cleaned up`, e);
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
- if (oldRecord[pathColumnName]) {
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
- if (this.options.storageAdapter.markKeyForDeletion !== undefined) {
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 ${oldRecord[pathColumnName]}. File will not be auto-cleaned up`, e);
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
- if (updates[pathColumnName] !== null) {
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
- if (this.options.storageAdapter.markKeyForNotDeletion !== undefined) {
306
- await this.options.storageAdapter.markKeyForNotDeletion(updates[pathColumnName]);
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/upload",
3
- "version": "2.6.0",
3
+ "version": "2.7.0",
4
4
  "description": "Plugin for uploading files for adminforth",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",