Addressed further pr comments about the api, onSave async, and adding a comment about rehypeRaw

This commit is contained in:
Kenneth Brewer 2026-04-29 02:57:37 -04:00
parent aad4d2af7c
commit 2f00db362d
5 changed files with 54 additions and 28 deletions

View File

@ -137,9 +137,11 @@ export default function MapComponent() {
<MapMarkerFormPopup
longitude={placingMarker.lng}
latitude={placingMarker.lat}
onDirtyChange={setHasUnsavedMarkerChanges}
onSave={async ({ name, notes, color }) => {
await addMarker(name, placingMarker.lng, placingMarker.lat, color, notes || undefined)
setPlacingMarker(null)
setHasUnsavedMarkerChanges(false)
}}
onCancel={() => {
if (!confirmDiscardMarkerChanges()) return

View File

@ -1,4 +1,4 @@
import {useCallback, useEffect, useLayoutEffect, useRef, useState} from 'react'
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
import { Popup } from 'react-map-gl/maplibre'
import { PIN_COLORS } from '~/hooks/useMapMarkers'
@ -18,7 +18,7 @@ type MapMarkerFormPopupProps = {
name: string
notes: string
color: PinColorId
}) => void
}) => Promise<void> | void
onCancel: () => void
onDirtyChange?: (dirty: boolean) => void
}
@ -34,6 +34,7 @@ export default function MapMarkerFormPopup({
const [name, setName] = useState(initialMarker?.name ?? '')
const [notes, setNotes] = useState(initialMarker?.notes ?? '')
const [color, setColor] = useState<PinColorId>(initialMarker?.color ?? 'orange')
const [isSaving, setIsSaving] = useState(false)
const textareaRef = useRef<HTMLTextAreaElement | null>(null)
@ -49,10 +50,6 @@ export default function MapMarkerFormPopup({
resizeTextarea()
}, [resizeTextarea])
useLayoutEffect(() => {
resizeTextarea()
}, [])
const isDirty =
name !== (initialMarker?.name ?? '') ||
notes !== (initialMarker?.notes ?? '') ||
@ -62,15 +59,21 @@ export default function MapMarkerFormPopup({
onDirtyChange?.(isDirty)
}, [isDirty, onDirtyChange])
const handleSave = () => {
if (!name.trim()) return
const handleSave = async () => {
if (!name.trim() || isSaving) return
onSave({
id: initialMarker?.id,
name: name.trim(),
notes: notes.trim(),
color,
})
try {
setIsSaving(true)
await onSave({
id: initialMarker?.id,
name: name.trim(),
notes: notes.trim(),
color,
})
} finally {
setIsSaving(false)
}
}
return (
@ -136,7 +139,8 @@ export default function MapMarkerFormPopup({
<button
type="button"
onClick={onCancel}
className="text-xs text-gray-500 hover:text-gray-700 px-2 py-1 rounded transition-colors"
disabled={isSaving}
className="text-xs text-gray-500 hover:text-gray-700 px-2 py-1 rounded transition-colors disabled:opacity-40"
>
Cancel
</button>
@ -144,10 +148,10 @@ export default function MapMarkerFormPopup({
<button
type="button"
onClick={handleSave}
disabled={!name.trim()}
disabled={!name.trim() || isSaving}
className="text-xs bg-[#424420] text-white rounded px-2.5 py-1 hover:bg-[#525530] disabled:opacity-40 transition-colors"
>
Save
{isSaving ? 'Saving...' : 'Save'}
</button>
</div>
</div>

View File

@ -29,6 +29,8 @@ export default function ViewMapMarkerPopup({
{marker.notes && (
<div className="mt-1 max-w-[240px] break-all whitespace-pre-wrap text-xs text-gray-500">
{/* react-markdown is intentionally used without rehypeRaw.
Do not enable raw HTML rendering unless notes are sanitized first. */}
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{

View File

@ -10,6 +10,7 @@ import { catchInternal } from './util'
import { NomadChatResponse, NomadInstalledModel, NomadOllamaModel, OllamaChatRequest } from '../../types/ollama'
import BenchmarkResult from '#models/benchmark_result'
import { BenchmarkType, RunBenchmarkResponse, SubmitBenchmarkResponse, UpdateBuilderTagResponse } from '../../types/benchmark'
import type { MapMarkerResponse } from '../../types/maps'
class API {
private client: AxiosInstance
@ -580,27 +581,30 @@ class API {
async listMapMarkers() {
return catchInternal(async () => {
const response = await this.client.get<
Array<{ id: number; name: string; longitude: number; latitude: number; color: string; created_at: string }>
>('/maps/markers')
const response = await this.client.get<MapMarkerResponse[]>('/maps/markers')
return response.data
})()
}
async createMapMarker(data: { name: string; longitude: number; latitude: number; color?: string }) {
async createMapMarker(data: {
name: string
notes?: string | null
longitude: number
latitude: number
color?: string
}) {
return catchInternal(async () => {
const response = await this.client.post<
{ id: number; name: string; longitude: number; latitude: number; color: string; created_at: string }
>('/maps/markers', data)
const response = await this.client.post<MapMarkerResponse>('/maps/markers', data)
return response.data
})()
}
async updateMapMarker(id: number, data: { name?: string; color?: string }) {
async updateMapMarker(
id: number,
data: { name?: string; notes?: string | null; color?: string }
) {
return catchInternal(async () => {
const response = await this.client.patch<
{ id: number; name: string; longitude: number; latitude: number; color: string }
>(`/maps/markers/${id}`, data)
const response = await this.client.patch<MapMarkerResponse>(`/maps/markers/${id}`, data)
return response.data
})()
}

View File

@ -21,3 +21,17 @@ export type MapLayer = {
'source-layer'?: string
[key: string]: any
}
export type MapMarkerResponse = {
id: number
name: string
longitude: number
latitude: number
color: string
notes?: string | null
marker_type?: string
route_id?: string | null
route_order?: number | null
created_at: string
updated_at?: string
}