refactor(base): expose extractFileId, dedupe URL→file-id parsing
Promote ImageUploadService.extractFileId from private to public so edit-flow consumers don't re-implement the `/image/original/<id>` parse. Used by market's CreateProductDialog and activities' CreateEventDialog when re-populating <ImageUpload> from a stored URL. Also clarify ImageUpload.removeImage: the `delete_token: ''` placeholder on re-populated images intentionally skips the server-side DELETE. We don't own the original upload's one-time token, and removing client-side shouldn't reach back and wipe shared files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b9bca36b50
commit
2b376bb244
4 changed files with 35 additions and 42 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, nextTick, watch } from 'vue'
|
||||||
import { useForm } from 'vee-validate'
|
import { useForm } from 'vee-validate'
|
||||||
import { toTypedSchema } from '@vee-validate/zod'
|
import { toTypedSchema } from '@vee-validate/zod'
|
||||||
import * as z from 'zod'
|
import * as z from 'zod'
|
||||||
|
|
@ -171,7 +171,7 @@ const availableCurrencies = ref<string[]>(['sat'])
|
||||||
const loadingCurrencies = ref(false)
|
const loadingCurrencies = ref(false)
|
||||||
const selectedCategories = ref<string[]>([])
|
const selectedCategories = ref<string[]>([])
|
||||||
|
|
||||||
function populateFromEvent(event: TicketedEvent) {
|
async function populateFromEvent(event: TicketedEvent) {
|
||||||
isPopulating.value = true
|
isPopulating.value = true
|
||||||
const start = splitDateTime(event.event_start_date)
|
const start = splitDateTime(event.event_start_date)
|
||||||
const end = splitDateTime(event.event_end_date)
|
const end = splitDateTime(event.event_end_date)
|
||||||
|
|
@ -189,25 +189,24 @@ function populateFromEvent(event: TicketedEvent) {
|
||||||
})
|
})
|
||||||
selectedCategories.value = [...(event.categories ?? [])]
|
selectedCategories.value = [...(event.categories ?? [])]
|
||||||
if (event.banner) {
|
if (event.banner) {
|
||||||
// Mirror the URL-to-alias bridge from market's CreateProductDialog
|
// Re-render the stored banner via its pict-rs file ID. delete_token
|
||||||
// so the <ImageUpload> renders the existing banner via its pict-rs
|
// is intentionally empty: we don't own the original upload's token
|
||||||
// file ID. delete_token is unknown for already-uploaded images, so
|
// and removing the image on the client should NOT delete the
|
||||||
// removal just clears the slot client-side.
|
// server-side file (it may be the user changing their mind about
|
||||||
const url = event.banner
|
// re-using it, or the same image referenced elsewhere).
|
||||||
const alias = url.includes('/image/original/')
|
const alias = imageService.extractFileId(event.banner)
|
||||||
? url.split('/image/original/')[1]
|
|
||||||
: url
|
|
||||||
bannerImages.value = [
|
bannerImages.value = [
|
||||||
{ alias, isPrimary: true, delete_token: '', details: {} as any },
|
{ alias, isPrimary: true, delete_token: '', details: {} as any },
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
bannerImages.value = []
|
bannerImages.value = []
|
||||||
}
|
}
|
||||||
// Release the watcher guard on the next tick so vee-validate's batched
|
// Release the watcher guard after Vue's microtask queue drains so
|
||||||
// updates settle before user input can drive the auto-mirror.
|
// vee-validate's batched setValues lands before user input can drive
|
||||||
setTimeout(() => {
|
// the auto-mirror. nextTick is more reliable than setTimeout(0) here
|
||||||
|
// — it waits for the DOM tick *after* all current microtasks.
|
||||||
|
await nextTick()
|
||||||
isPopulating.value = false
|
isPopulating.value = false
|
||||||
}, 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => props.open, async (isOpen) => {
|
watch(() => props.open, async (isOpen) => {
|
||||||
|
|
@ -223,7 +222,7 @@ watch(() => props.open, async (isOpen) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (props.event) {
|
if (props.event) {
|
||||||
populateFromEvent(props.event)
|
await populateFromEvent(props.event)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
selectedCategories.value = []
|
selectedCategories.value = []
|
||||||
|
|
|
||||||
|
|
@ -449,7 +449,11 @@ const removeImage = async (imageToRemove: ImageWithMetadata) => {
|
||||||
if (props.disabled) return
|
if (props.disabled) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Only try to delete from pict-rs if we have a delete token (newly uploaded images)
|
// Server-side delete only when we have a delete_token (newly
|
||||||
|
// uploaded this session). Pre-existing images re-populated from a
|
||||||
|
// stored URL ship `delete_token: ''` by convention — we don't own
|
||||||
|
// the original upload's one-time token, and removing on the client
|
||||||
|
// shouldn't reach back and wipe the server-side file.
|
||||||
if (imageToRemove.delete_token) {
|
if (imageToRemove.delete_token) {
|
||||||
await imageService.deleteImage(imageToRemove.delete_token, imageToRemove.alias)
|
await imageService.deleteImage(imageToRemove.delete_token, imageToRemove.alias)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -300,9 +300,12 @@ export class ImageUploadService extends BaseService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract file ID from alias, handling both file IDs and full URLs
|
* Extract a pict-rs file ID from an alias, accepting both bare IDs
|
||||||
|
* and full `/image/original/<id>` URLs. Public so callers
|
||||||
|
* re-populating uploads from stored URLs (edit flows) don't have to
|
||||||
|
* re-implement the parse.
|
||||||
*/
|
*/
|
||||||
private extractFileId(alias: string): string {
|
extractFileId(alias: string): string {
|
||||||
if (!alias) {
|
if (!alias) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -516,30 +516,17 @@ watch(() => props.isOpen, async (isOpen) => {
|
||||||
// Reset form with appropriate initial values
|
// Reset form with appropriate initial values
|
||||||
resetForm({ values: initialValues })
|
resetForm({ values: initialValues })
|
||||||
|
|
||||||
// Convert existing image URLs to the format expected by ImageUpload component
|
// Convert existing image URLs to the format expected by ImageUpload.
|
||||||
|
// delete_token is intentionally empty for pre-existing images: see
|
||||||
|
// ImageUploadService.deleteImage gate — removing on the client
|
||||||
|
// should not delete the server-side file.
|
||||||
if (props.product?.images && props.product.images.length > 0) {
|
if (props.product?.images && props.product.images.length > 0) {
|
||||||
// For existing products, we need to convert URLs back to a format ImageUpload can display
|
uploadedImages.value = props.product.images.map((url, index) => ({
|
||||||
uploadedImages.value = props.product.images.map((url, index) => {
|
alias: imageService.extractFileId(url),
|
||||||
let alias = url
|
|
||||||
|
|
||||||
// If it's a full pict-rs URL, extract just the file ID
|
|
||||||
if (url.includes('/image/original/')) {
|
|
||||||
const parts = url.split('/image/original/')
|
|
||||||
if (parts.length > 1 && parts[1]) {
|
|
||||||
alias = parts[1]
|
|
||||||
}
|
|
||||||
} else if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
||||||
// Keep full URLs as-is
|
|
||||||
alias = url
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
alias: alias,
|
|
||||||
delete_token: '',
|
delete_token: '',
|
||||||
isPrimary: index === 0,
|
isPrimary: index === 0,
|
||||||
details: {}
|
details: {} as any,
|
||||||
}
|
}))
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
uploadedImages.value = []
|
uploadedImages.value = []
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue