fix(uploader): resolve upload endpoint URL and update ImageUploader props in CreateProperty
deploy-chabokan / deploy (push) Canceled after 0s
deploy-chabokan / deploy (push) Canceled after 0s
This commit is contained in:
@@ -417,8 +417,12 @@ const saveProperty = async () => {
|
|||||||
|
|
||||||
isSaving.value = true;
|
isSaving.value = true;
|
||||||
try {
|
try {
|
||||||
property.value.mainImageUrls = mainImageUrlsWithData.value.map((x) => x.url);
|
property.value.mainImageUrls = (mainImageUrlsWithData.value || [])
|
||||||
property.value.documentUrls = documentUrlsWithData.value.map((x) => x.url);
|
.map((x) => (typeof x === 'string' ? x : x.url || x.source || ''))
|
||||||
|
.filter(Boolean);
|
||||||
|
property.value.documentUrls = (documentUrlsWithData.value || [])
|
||||||
|
.map((x) => (typeof x === 'string' ? x : x.url || x.source || ''))
|
||||||
|
.filter(Boolean);
|
||||||
property.value.features = serializeFeaturesPayload(featuresValues.value);
|
property.value.features = serializeFeaturesPayload(featuresValues.value);
|
||||||
|
|
||||||
let status = 'failed';
|
let status = 'failed';
|
||||||
@@ -857,7 +861,7 @@ const saveProperty = async () => {
|
|||||||
<i class="pi pi-file-pdf text-xl"></i>
|
<i class="pi pi-file-pdf text-xl"></i>
|
||||||
<span>اسناد و مدارک تصویری / پیدیاف ملک</span>
|
<span>اسناد و مدارک تصویری / پیدیاف ملک</span>
|
||||||
</div>
|
</div>
|
||||||
<ImageUploader v-model="documentUrlsWithData" uploadUrl="/api/files/admin/upload-file" @uploaded="onDocumentUploaded" />
|
<ImageUploader v-model="documentUrlsWithData" bucketName="documents" :maxFiles="10" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -895,7 +899,7 @@ const saveProperty = async () => {
|
|||||||
<i class="pi pi-image text-xl"></i>
|
<i class="pi pi-image text-xl"></i>
|
||||||
<span>تصاویر ملک</span>
|
<span>تصاویر ملک</span>
|
||||||
</div>
|
</div>
|
||||||
<ImageUploader v-model="mainImageUrlsWithData" uploadUrl="/api/files/admin/upload-file" @uploaded="onMainImageUploaded" />
|
<ImageUploader v-model="mainImageUrlsWithData" bucketName="properties" :maxFiles="10" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,42 +1,48 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div class="image-uploader-wrapper">
|
||||||
<FilePond
|
<FilePond
|
||||||
ref="pond"
|
ref="pond"
|
||||||
name="filepond-image"
|
name="filepond-image"
|
||||||
:max-files="maxFiles"
|
:max-files="maxFiles"
|
||||||
store-as-file="true"
|
:allow-multiple="allowMultiple"
|
||||||
|
:accepted-file-types="acceptedFileTypes"
|
||||||
:server="{
|
:server="{
|
||||||
process: {
|
process: {
|
||||||
url: API_URL + uploadEndpoint,
|
url: resolvedUploadUrl,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
headers: {
|
headers: authHeaders,
|
||||||
authorization: 'Bearer ' + Cookies.get('bm-token')
|
|
||||||
},
|
|
||||||
onload: (response) => handleProcessResponse(response)
|
onload: (response) => handleProcessResponse(response)
|
||||||
},
|
},
|
||||||
revert: {
|
revert: {
|
||||||
url: API_URL + deleteEndpoint,
|
url: resolvedDeleteUrl,
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
headers: {
|
headers: authHeaders,
|
||||||
authorization: 'Bearer ' + Cookies.get('bm-token')
|
|
||||||
},
|
|
||||||
onload: (response) => handleRevertResponse(response)
|
onload: (response) => handleRevertResponse(response)
|
||||||
|
},
|
||||||
|
load: (source, load, error, progress, abort, headers) => {
|
||||||
|
fetch(source)
|
||||||
|
.then((res) => res.blob())
|
||||||
|
.then(load)
|
||||||
|
.catch((err) => {
|
||||||
|
console.warn('FilePond failed to load existing image:', source, err);
|
||||||
|
error(err.message);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
label-idle="برای آپلود کلیک کنید"
|
:files="initialFiles"
|
||||||
label-tap-to-undo=" برای حذف کلیک کنید "
|
@removefile="handleRemoveFile"
|
||||||
label-file-processing=" در حال آپلود "
|
label-idle="برای آپلود کلیک کنید یا فایل را بکشید و رها کنید"
|
||||||
label-file-processing-complete=" آپلود فایل تکمیل شد "
|
label-tap-to-undo="برای حذف کلیک کنید"
|
||||||
:allow-multiple="true"
|
label-file-processing="در حال آپلود"
|
||||||
v-model="uploadedFiles"
|
label-file-processing-complete="آپلود فایل تکمیل شد"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, defineProps, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
import vueFilePond from 'vue-filepond';
|
import vueFilePond from 'vue-filepond';
|
||||||
import 'filepond/dist/filepond.min.css';
|
import 'filepond/dist/filepond.min.css';
|
||||||
@@ -46,17 +52,33 @@ import FilePondPluginImagePreview from 'filepond-plugin-image-preview';
|
|||||||
|
|
||||||
// Define props
|
// Define props
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
bucketName: {
|
||||||
|
type: String,
|
||||||
|
default: 'properties'
|
||||||
|
},
|
||||||
uploadEndpoint: {
|
uploadEndpoint: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
default: ''
|
||||||
|
},
|
||||||
|
uploadUrl: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
},
|
},
|
||||||
deleteEndpoint: {
|
deleteEndpoint: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
default: ''
|
||||||
|
},
|
||||||
|
deleteUrl: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
},
|
},
|
||||||
maxFiles: {
|
maxFiles: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 1
|
default: 10
|
||||||
|
},
|
||||||
|
allowMultiple: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
},
|
},
|
||||||
acceptedFiles: {
|
acceptedFiles: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -64,58 +86,154 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
mode: {
|
mode: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
|
||||||
default: 'public'
|
default: 'public'
|
||||||
},
|
},
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
|
||||||
default: () => []
|
default: () => []
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Setup reactive variables
|
const emit = defineEmits(['update:modelValue', 'upload', 'uploaded', 'delete']);
|
||||||
const API_URL = import.meta.env.VITE_API_URL;
|
|
||||||
const uploadedFiles = ref([...props.modelValue]);
|
|
||||||
|
|
||||||
// Define FilePond component
|
// Define FilePond component
|
||||||
const FilePond = vueFilePond(FilePondPluginFileValidateType, FilePondPluginImagePreview);
|
const FilePond = vueFilePond(FilePondPluginFileValidateType, FilePondPluginImagePreview);
|
||||||
const emit = defineEmits(['update:modelValue', 'upload', 'delete']);
|
const pond = ref(null);
|
||||||
|
|
||||||
|
const acceptedFileTypes = computed(() => {
|
||||||
|
if (!props.acceptedFiles) return undefined;
|
||||||
|
return props.acceptedFiles.split(',').map((s) => s.trim());
|
||||||
|
});
|
||||||
|
|
||||||
|
const API_URL = (import.meta.env.VITE_API_URL || '').replace(/\/$/, '');
|
||||||
|
|
||||||
|
const authHeaders = computed(() => {
|
||||||
|
const token = Cookies.get('bm-token') || (typeof localStorage !== 'undefined' ? localStorage.getItem('bm-token') : '') || '';
|
||||||
|
return token ? { authorization: `Bearer ${token}` } : {};
|
||||||
|
});
|
||||||
|
|
||||||
|
const resolvedUploadUrl = computed(() => {
|
||||||
|
let endpoint = props.uploadUrl || props.uploadEndpoint;
|
||||||
|
if (!endpoint || endpoint === '/api/files/admin/upload-file') {
|
||||||
|
endpoint = `/api/files/bucket/upload-single-filepond/${props.bucketName}`;
|
||||||
|
}
|
||||||
|
if (endpoint.startsWith('http://') || endpoint.startsWith('https://')) {
|
||||||
|
return endpoint;
|
||||||
|
}
|
||||||
|
const cleanEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
||||||
|
return `${API_URL}${cleanEndpoint}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const resolvedDeleteUrl = computed(() => {
|
||||||
|
let endpoint = props.deleteUrl || props.deleteEndpoint;
|
||||||
|
if (!endpoint) {
|
||||||
|
endpoint = `/api/files/bucket/delete-file/${props.bucketName}`;
|
||||||
|
}
|
||||||
|
if (endpoint.startsWith('http://') || endpoint.startsWith('https://')) {
|
||||||
|
return endpoint;
|
||||||
|
}
|
||||||
|
const cleanEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
||||||
|
return `${API_URL}${cleanEndpoint}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const initialFiles = computed(() => {
|
||||||
|
if (!Array.isArray(props.modelValue)) return [];
|
||||||
|
return props.modelValue
|
||||||
|
.map((item) => {
|
||||||
|
const url = typeof item === 'string' ? item : item.url || item.source || '';
|
||||||
|
if (!url) return null;
|
||||||
|
return {
|
||||||
|
source: url,
|
||||||
|
options: {
|
||||||
|
type: 'local'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
});
|
||||||
|
|
||||||
// Handle server responses for file processing
|
// Handle server responses for file processing
|
||||||
function handleProcessResponse(response) {
|
function handleProcessResponse(response) {
|
||||||
if (props.mode === 'public') {
|
let parsed;
|
||||||
const data = JSON.parse(response);
|
try {
|
||||||
const publicUrl = data.data[0].fileRecord.publicUrl;
|
parsed = typeof response === 'string' ? JSON.parse(response) : response;
|
||||||
const fileName = data.data[0].fileRecord.fileName;
|
} catch (e) {
|
||||||
uploadedFiles.value.push(publicUrl);
|
console.error('Failed to parse file upload response:', e);
|
||||||
emit('update:modelValue', uploadedFiles);
|
return '';
|
||||||
emit('upload', uploadedFiles);
|
|
||||||
return publicUrl;
|
|
||||||
} else {
|
|
||||||
const data = JSON.parse(response);
|
|
||||||
const privateUrl = data.data[0].fileRecord.privateUrl;
|
|
||||||
const fileName = data.data[0].fileRecord.fileName;
|
|
||||||
uploadedFiles.value.push(privateUrl);
|
|
||||||
emit('update:modelValue', uploadedFiles);
|
|
||||||
emit('upload', uploadedFiles);
|
|
||||||
return privateUrl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let fileRecord = null;
|
||||||
|
if (Array.isArray(parsed?.data)) {
|
||||||
|
fileRecord = parsed.data[0]?.fileRecord || parsed.data[0];
|
||||||
|
} else if (parsed?.data?.fileRecord) {
|
||||||
|
fileRecord = parsed.data.fileRecord;
|
||||||
|
} else if (parsed?.data) {
|
||||||
|
fileRecord = parsed.data;
|
||||||
|
} else {
|
||||||
|
fileRecord = parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileUrl =
|
||||||
|
props.mode === 'private'
|
||||||
|
? fileRecord?.privateUrl || fileRecord?.fileURL || fileRecord?.url || ''
|
||||||
|
: fileRecord?.publicUrl || fileRecord?.fileURL || fileRecord?.url || '';
|
||||||
|
|
||||||
|
const fileName = fileRecord?.fileName || fileRecord?.name || (fileUrl ? fileUrl.split('/').pop() : 'file');
|
||||||
|
|
||||||
|
const newItem = {
|
||||||
|
name: fileName,
|
||||||
|
url: fileUrl,
|
||||||
|
progress: 100,
|
||||||
|
bucketName: props.bucketName
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentList = Array.isArray(props.modelValue) ? [...props.modelValue] : [];
|
||||||
|
const isStringArray = currentList.length > 0 && typeof currentList[0] === 'string';
|
||||||
|
currentList.push(isStringArray ? fileUrl : newItem);
|
||||||
|
|
||||||
|
emit('update:modelValue', currentList);
|
||||||
|
emit('upload', newItem);
|
||||||
|
emit('uploaded', newItem);
|
||||||
|
|
||||||
|
return fileName || fileUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle server responses for file revert
|
// Handle server responses for file revert
|
||||||
function handleRevertResponse(response) {
|
function handleRevertResponse(response) {
|
||||||
const data = JSON.parse(response);
|
let deletedIdentifier = '';
|
||||||
const fileName = data.data;
|
try {
|
||||||
// Find and remove the file name from uploadedFiles
|
const parsed = typeof response === 'string' ? JSON.parse(response) : response;
|
||||||
const fileIndex = uploadedFiles.value.findIndex((name) => name === fileName);
|
deletedIdentifier = parsed?.data || parsed?.fileName || response;
|
||||||
if (fileIndex !== -1) {
|
} catch (e) {
|
||||||
uploadedFiles.value.splice(fileIndex, 1);
|
deletedIdentifier = response;
|
||||||
emit('update:modelValue', uploadedFiles);
|
|
||||||
emit('delete', uploadedFiles);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return fileName;
|
removeFromFileList(deletedIdentifier);
|
||||||
|
emit('delete', deletedIdentifier);
|
||||||
|
return deletedIdentifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRemoveFile(error, fileItem) {
|
||||||
|
if (error) return;
|
||||||
|
const identifier = fileItem?.serverId || fileItem?.source || fileItem?.filename || '';
|
||||||
|
if (identifier) {
|
||||||
|
removeFromFileList(identifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeFromFileList(identifier) {
|
||||||
|
if (!Array.isArray(props.modelValue) || !identifier) return;
|
||||||
|
const updated = props.modelValue.filter((item) => {
|
||||||
|
if (typeof item === 'string') {
|
||||||
|
return item !== identifier && !item.includes(identifier);
|
||||||
|
}
|
||||||
|
if (typeof item === 'object' && item !== null) {
|
||||||
|
const u = item.url || item.source || item.name || '';
|
||||||
|
return u !== identifier && !u.includes(identifier);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
emit('update:modelValue', updated);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user