Skip to content

Commit 53c42b2

Browse files
committed
feat: add S3 storage support
1 parent 7917962 commit 53c42b2

File tree

2 files changed

+225
-125
lines changed

2 files changed

+225
-125
lines changed

custom/visionAction.vue

Lines changed: 218 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
<Button
3434
class="w-64"
3535
@click="saveData"
36+
:disabled="isLoading"
37+
:loader="isLoading"
3638
>
3739
{{ props.checkboxes.length > 1 ? 'Save fields' : 'Save field' }}
3840
</Button>
@@ -49,12 +51,18 @@ import { ref, watch } from 'vue'
4951
import { Dialog, Button } from '@/afcl';
5052
import VisionTable from './visionTable.vue'
5153
import adminforth from '@/adminforth';
54+
import { useI18n } from 'vue-i18n';
55+
import { useRoute } from 'vue-router';
56+
import { type AdminUser, type AdminForthResourceCommon } from '@/types';
57+
58+
const route = useRoute();
59+
const { t } = useI18n();
5260
5361
const props = defineProps<{
5462
checkboxes: any,
5563
meta: any,
56-
resource: any,
57-
adminUser: any,
64+
resource: AdminForthResourceCommon,
65+
adminUser: AdminUser,
5866
updateList: {
5967
type: Function,
6068
required: true
@@ -76,6 +84,7 @@ const isAiResponseReceived = ref([]);
7684
const isAiResponseReceivedImage = ref([]);
7785
const primaryKey = props.meta.primaryKey;
7886
const openGenerationCarousel = ref([]);
87+
const isLoading = ref(false);
7988
8089
const openDialog = async () => {
8190
confirmDialog.value.open();
@@ -94,20 +103,28 @@ const openDialog = async () => {
94103
return acc;
95104
},{[primaryKey]: records.value[i][primaryKey]} as Record<string, boolean>);
96105
}
106+
isLoading.value = true;
97107
await Promise.all([
98108
//analyzeFields(),
99109
generateImages()
100110
]);
111+
isLoading.value = false;
101112
}
102113
103-
watch(selected, (val) => {
104-
console.log('Selected changed:', val);
105-
}, { deep: true });
114+
// watch(selected, (val) => {
115+
// console.log('Selected changed:', val);
116+
// }, { deep: true });
106117
107118
const closeDialog = () => {
108119
confirmDialog.value.close();
109120
isAiResponseReceived.value = [];
110121
isAiResponseReceivedImage.value = [];
122+
123+
records.value = [];
124+
images.value = [];
125+
selected.value = [];
126+
tableColumns.value = [];
127+
tableColumnsIndexes.value = [];
111128
}
112129
113130
function formatLabel(str) {
@@ -185,29 +202,38 @@ function isInColumnEnum(key: string): boolean {
185202
}
186203
187204
async function getRecords() {
188-
const res = await callAdminForthApi({
189-
path: `/plugin/${props.meta.pluginInstanceId}/get_records`,
190-
method: 'POST',
191-
body: {
192-
record: props.checkboxes,
193-
},
194-
});
195-
records.value = res.records;
205+
try {
206+
const res = await callAdminForthApi({
207+
path: `/plugin/${props.meta.pluginInstanceId}/get_records`,
208+
method: 'POST',
209+
body: {
210+
record: props.checkboxes,
211+
},
212+
});
213+
records.value = res.records;
214+
} catch (error) {
215+
console.error('Failed to get records:', error);
216+
// Handle error appropriately
217+
}
196218
}
197219
198220
async function getImages() {
199-
const res = await callAdminForthApi({
200-
path: `/plugin/${props.meta.pluginInstanceId}/get_images`,
201-
method: 'POST',
202-
body: {
203-
record: records.value,
204-
},
205-
});
206-
207-
images.value = res.images;
221+
try {
222+
const res = await callAdminForthApi({
223+
path: `/plugin/${props.meta.pluginInstanceId}/get_images`,
224+
method: 'POST',
225+
body: {
226+
record: records.value,
227+
},
228+
});
229+
images.value = res.images;
230+
} catch (error) {
231+
console.error('Failed to get images:', error);
232+
// Handle error appropriately
233+
}
208234
}
209235
210-
function prepareDataForSave() {
236+
async function prepareDataForSave() {
211237
const checkedItems = selected.value
212238
.filter(item => item.isChecked === true)
213239
.map(item => {
@@ -217,55 +243,119 @@ function prepareDataForSave() {
217243
const checkedItemsIDs = selected.value
218244
.filter(item => item.isChecked === true)
219245
.map(item => item[primaryKey]);
246+
247+
const promises = [];
248+
for (const item of checkedItems) {
249+
for (const [key, value] of Object.entries(item)) {
250+
if(props.meta.outputImageFields?.includes(key)) {
251+
const p = convertImages(key, value).then(result => {
252+
item[key] = result;
253+
});
254+
255+
promises.push(p);
256+
}
257+
}
258+
}
259+
await Promise.all(promises);
260+
220261
return [checkedItemsIDs, checkedItems];
221262
}
222263
264+
async function convertImages(fieldName, img) {
265+
let imgBlob;
266+
if (img.startsWith('data:')) {
267+
const base64 = img.split(',')[1];
268+
const mimeType = img.split(';')[0].split(':')[1];
269+
const byteCharacters = atob(base64);
270+
const byteNumbers = new Array(byteCharacters.length);
271+
for (let i = 0; i < byteCharacters.length; i++) {
272+
byteNumbers[i] = byteCharacters.charCodeAt(i);
273+
}
274+
const byteArray = new Uint8Array(byteNumbers);
275+
imgBlob = new Blob([byteArray], { type: mimeType });
276+
} else {
277+
imgBlob = await fetch(
278+
`/adminapi/v1/plugin/${props.meta.outputImagesPluginInstanceIds[fieldName]}/cors-proxy?url=${encodeURIComponent(img)}`
279+
).then(res => { return res.blob() });
280+
}
281+
return imgBlob;
282+
}
283+
284+
223285
async function analyzeFields() {
224-
isAiResponseReceived.value = props.checkboxes.map(() => false);
225-
226-
const res = await callAdminForthApi({
227-
path: `/plugin/${props.meta.pluginInstanceId}/analyze`,
228-
method: 'POST',
229-
body: {
230-
selectedIds: props.checkboxes,
231-
},
232-
});
286+
try {
287+
isAiResponseReceived.value = props.checkboxes.map(() => false);
233288
234-
isAiResponseReceived.value = props.checkboxes.map(() => true);
289+
const res = await callAdminForthApi({
290+
path: `/plugin/${props.meta.pluginInstanceId}/analyze`,
291+
method: 'POST',
292+
body: {
293+
selectedIds: props.checkboxes,
294+
},
295+
});
296+
297+
isAiResponseReceived.value = props.checkboxes.map(() => true);
235298
236-
res.result.forEach((item, idx) => {
237-
const pk = selected.value[idx]?.[primaryKey]
299+
res.result.forEach((item, idx) => {
300+
const pk = selected.value[idx]?.[primaryKey]
238301
239-
if (pk) {
240-
selected.value[idx] = {
241-
...selected.value[idx],
242-
...item,
243-
isChecked: true,
244-
[primaryKey]: pk
302+
if (pk) {
303+
selected.value[idx] = {
304+
...selected.value[idx],
305+
...item,
306+
isChecked: true,
307+
[primaryKey]: pk
308+
}
245309
}
246-
}
247-
})
310+
})
311+
} catch (error) {
312+
console.error('Failed to get records:', error);
248313
314+
}
249315
}
250316
251317
async function saveData() {
252-
const [checkedItemsIDs, reqData] = prepareDataForSave();
253-
254-
const res = await callAdminForthApi({
255-
path: `/plugin/${props.meta.pluginInstanceId}/update_fields`,
256-
method: 'POST',
257-
body: {
258-
selectedIds: checkedItemsIDs,
259-
fields: reqData,
260-
},
261-
});
318+
if (!selected.value?.length) {
319+
adminforth.alert({ message: 'No items selected', variant: 'warning' });
320+
return;
321+
}
322+
try {
323+
isLoading.value = true;
324+
const [checkedItemsIDs, reqData] = await prepareDataForSave();
262325
263-
if(res.ok) {
264-
confirmDialog.value.close();
265-
props.updateList();
266-
props.clearCheckboxes();
267-
} else {
268-
console.error('Error saving data:', res);
326+
const imagesToUpload = [];
327+
for (const item of reqData) {
328+
for (const [key, value] of Object.entries(item)) {
329+
if(props.meta.outputImageFields?.includes(key)) {
330+
const p = uploadImage(value, item[primaryKey], key).then(result => {
331+
item[key] = result;
332+
});
333+
imagesToUpload.push(p);
334+
}
335+
}
336+
}
337+
await Promise.all(imagesToUpload);
338+
339+
const res = await callAdminForthApi({
340+
path: `/plugin/${props.meta.pluginInstanceId}/update_fields`,
341+
method: 'POST',
342+
body: {
343+
selectedIds: checkedItemsIDs,
344+
fields: reqData,
345+
},
346+
});
347+
348+
if(res.ok) {
349+
confirmDialog.value.close();
350+
props.updateList();
351+
props.clearCheckboxes();
352+
} else {
353+
console.error('Error saving data:', res);
354+
}
355+
} catch (error) {
356+
console.error('Error saving data:', error);
357+
} finally {
358+
isLoading.value = false;
269359
}
270360
}
271361
@@ -314,4 +404,73 @@ async function generateImages() {
314404
}
315405
}
316406
407+
408+
async function uploadImage(imgBlob, id, fieldName) {
409+
const file = new File([imgBlob], `generated_${fieldName}_${id}.${imgBlob.type}`, { type: imgBlob.type });
410+
const { name, size, type } = file;
411+
412+
const extension = name.split('.').pop();
413+
const nameNoExtension = name.replace(`.${extension}`, '');
414+
415+
try {
416+
const { uploadUrl, uploadExtraParams, filePath, error } = await callAdminForthApi({
417+
path: `/plugin/${props.meta.outputImagesPluginInstanceIds[fieldName]}/get_file_upload_url`,
418+
method: 'POST',
419+
body: {
420+
originalFilename: nameNoExtension,
421+
contentType: type,
422+
size,
423+
originalExtension: extension,
424+
recordPk: route?.params?.primaryKey,
425+
},
426+
});
427+
428+
if (error) {
429+
adminforth.alert({
430+
message: t('File was not uploaded because of error: {error}', { error }),
431+
variant: 'danger'
432+
});
433+
return;
434+
}
435+
436+
const xhr = new XMLHttpRequest();
437+
const success = await new Promise((resolve) => {
438+
xhr.upload.onprogress = (e) => {
439+
if (e.lengthComputable) {
440+
}
441+
};
442+
xhr.addEventListener('loadend', () => {
443+
const success = xhr.readyState === 4 && xhr.status === 200;
444+
// try to read response
445+
resolve(success);
446+
});
447+
xhr.open('PUT', uploadUrl, true);
448+
xhr.setRequestHeader('Content-Type', type);
449+
uploadExtraParams && Object.entries(uploadExtraParams).forEach(([key, value]: [string, string]) => {
450+
xhr.setRequestHeader(key, value);
451+
})
452+
xhr.send(file);
453+
});
454+
if (!success) {
455+
adminforth.alert({
456+
messageHtml: `<div>${t('Sorry but the file was not uploaded because of internal storage Request Error:')}</div>
457+
<pre style="white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; max-width: 100%;">${
458+
xhr.responseText.replace(/</g, '&lt;').replace(/>/g, '&gt;')
459+
}</pre>`,
460+
variant: 'danger',
461+
timeout: 30,
462+
});
463+
return;
464+
}
465+
return filePath;
466+
} catch (error) {
467+
console.error('Error uploading file:', error);
468+
adminforth.alert({
469+
message: 'Sorry but the file was not be uploaded. Please try again.',
470+
variant: 'danger'
471+
});
472+
return null;
473+
}
474+
}
475+
317476
</script>

0 commit comments

Comments
 (0)