Add store-product Excel bulk edit and polish the business dashboard.
Export/import price and stock from Store Products, keep card order after saves, localize variations, and drop unused website nav items. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
7b51dda82a
commit
832ac32a09
@@ -120,3 +120,98 @@ export async function apiRequest<T>(path: string, options: RequestOptions = {}):
|
||||
|
||||
return payload as T
|
||||
}
|
||||
|
||||
function filenameFromDisposition(header: string | null) {
|
||||
if (!header) return null
|
||||
const utf = header.match(/filename\*=UTF-8''([^;]+)/i)
|
||||
if (utf?.[1]) {
|
||||
try {
|
||||
return decodeURIComponent(utf[1])
|
||||
} catch {
|
||||
return utf[1]
|
||||
}
|
||||
}
|
||||
const plain = header.match(/filename="?([^"]+)"?/i)
|
||||
return plain?.[1] ?? null
|
||||
}
|
||||
|
||||
export async function apiRequestBlob(
|
||||
path: string,
|
||||
options: { auth?: boolean; signal?: AbortSignal } = {},
|
||||
): Promise<{ blob: Blob; filename: string | null }> {
|
||||
const { auth = false, signal } = options
|
||||
const requestHeaders = new Headers()
|
||||
|
||||
if (auth) {
|
||||
const accessToken = getAccessToken()
|
||||
if (accessToken) {
|
||||
requestHeaders.set('Authorization', `Bearer ${accessToken}`)
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}${path}`, {
|
||||
headers: requestHeaders,
|
||||
signal,
|
||||
})
|
||||
|
||||
if (response.status === 401 && auth) {
|
||||
const refreshed = await refreshAccessToken()
|
||||
if (refreshed) {
|
||||
return apiRequestBlob(path, options)
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const payload = await response.json().catch(() => null)
|
||||
throw new ApiError(
|
||||
parseErrorMessage(payload, `Request failed with status ${response.status}`),
|
||||
response.status,
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
blob: await response.blob(),
|
||||
filename: filenameFromDisposition(response.headers.get('Content-Disposition')),
|
||||
}
|
||||
}
|
||||
|
||||
export async function apiRequestForm<T>(
|
||||
path: string,
|
||||
formData: FormData,
|
||||
options: { method?: string; auth?: boolean; signal?: AbortSignal } = {},
|
||||
): Promise<T> {
|
||||
const { method = 'POST', auth = false, signal } = options
|
||||
const requestHeaders = new Headers()
|
||||
|
||||
if (auth) {
|
||||
const accessToken = getAccessToken()
|
||||
if (accessToken) {
|
||||
requestHeaders.set('Authorization', `Bearer ${accessToken}`)
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}${path}`, {
|
||||
method,
|
||||
headers: requestHeaders,
|
||||
body: formData,
|
||||
signal,
|
||||
})
|
||||
|
||||
if (response.status === 401 && auth) {
|
||||
const refreshed = await refreshAccessToken()
|
||||
if (refreshed) {
|
||||
return apiRequestForm<T>(path, formData, options)
|
||||
}
|
||||
}
|
||||
|
||||
const payload = await response.json().catch(() => null)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new ApiError(
|
||||
parseErrorMessage(payload, `Request failed with status ${response.status}`),
|
||||
response.status,
|
||||
)
|
||||
}
|
||||
|
||||
return payload as T
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user