import { APP_API_BASE_URL } from '../../api/client'
import { RECORD_VIEW_APP_ID, isDateType, toISODate } from '../../api/recordView'
import { formatDate } from '../../utils/format'
const ISO_DATE = /^\d{4}-\d{2}-\d{2}(T|\s|$)/
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
const IMAGE_URL = /^https?:\/\/\S+\.(png|jpe?g|gif|webp|svg)(\?\S*)?$/i
const isImageSrc = (s) => typeof s === 'string' && (IMAGE_URL.test(s.trim()) || s.startsWith('data:image/'))
/**
* A viewable URL for an uploaded file object ({ uuid, blob_path, … }), built
* the way the Zino app does. Both preview routes are public, so they work as
* a plain :
* GET {APP_API_BASE_URL}/app/{APP_ID}/view/files/{uuid}/preview
* GET {APP_API_BASE_URL}/app/{APP_ID}/view/files/preview?path=
*/
export function fileUrl(file) {
if (!file || typeof file !== 'object') return null
if (file.preview_url) return file.preview_url
const base = `${APP_API_BASE_URL}/app/${RECORD_VIEW_APP_ID}/view/files`
const uuid = file.uuid || file.repo_file_uuid
if (uuid) return `${base}/${encodeURIComponent(uuid)}/preview`
if (file.blob_path) return `${base}/preview?path=${encodeURIComponent(file.blob_path)}`
return file.url || null
}
/** Parse a value that may arrive as JSON text ('[{…}]', '{…}'). */
function parseJson(v) {
if (typeof v !== 'string' || !/^[[{]/.test(v.trim())) return v
try {
return JSON.parse(v)
} catch {
return v
}
}
/**
* The image sources in an image value: one URL or uploaded file object, a
* list of them (photo slots), or that list as JSON text.
*/
export function imageSources(value) {
const v = parseJson(value)
return toList(v)
.map(parseJson)
.flatMap(toList)
.map((s) => (typeof s === 'string' ? s.trim() : fileUrl(s)))
.filter(Boolean)
}
/** The documents in a file value: [{ url, name }], from file objects or URLs. */
export function fileLinks(value) {
return toList(parseJson(value))
.map(parseJson)
.flatMap(toList)
.map((f) =>
typeof f === 'string'
? { url: f.trim(), name: f.trim().split('/').pop() }
: { url: fileUrl(f), name: f?.original_name || 'File' },
)
.filter((f) => f.url)
}
/** Links that open each document in a new tab. Clicks don't reach a clickable table row. */
export function FileLinks({ value, label }) {
const links = fileLinks(value)
if (!links.length) return —
return (
{links.map((f, i) => (
e.stopPropagation()}
>
📄 {label ?? f.name}
))}
)
}
const isFile = (o) => ['uuid', 'repo_file_uuid', 'blob_path', 'preview_url', 'url'].some((k) => o[k])
/** A list as-is; photo slots ({ Front: {…}, Other: [...] }) as their photos; else one item. */
function toList(v) {
if (Array.isArray(v)) return v
if (v && typeof v === 'object' && !isFile(v)) return Object.values(v).flat()
return [v]
}
/**
* Renders one detail field ({ value, type, slots?, otherSlot? }) by its type,
* falling back to what the value looks like. Shared by every details page.
* `slots` names the photos of an image list (Front, Back…), in order; any
* photos past them are named `otherSlot`.
*/
export default function FieldValue({ field }) {
const { value, type, slots, otherSlot } = field
const caption = (i) => (slots ? slots[i] ?? otherSlot : undefined)
if (value === undefined || value === null || value === '') {
return —
}
const s = typeof value === 'string' ? value.trim() : value
if (type === 'file') return
const images = type === 'image' || isImageSrc(s) || (Array.isArray(s) && s.every(isImageSrc))
if (images) {
return (