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 <MapMarkerFormPopup
longitude={placingMarker.lng} longitude={placingMarker.lng}
latitude={placingMarker.lat} latitude={placingMarker.lat}
onDirtyChange={setHasUnsavedMarkerChanges}
onSave={async ({ name, notes, color }) => { onSave={async ({ name, notes, color }) => {
await addMarker(name, placingMarker.lng, placingMarker.lat, color, notes || undefined) await addMarker(name, placingMarker.lng, placingMarker.lat, color, notes || undefined)
setPlacingMarker(null) setPlacingMarker(null)
setHasUnsavedMarkerChanges(false)
}} }}
onCancel={() => { onCancel={() => {
if (!confirmDiscardMarkerChanges()) return 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 { Popup } from 'react-map-gl/maplibre'
import { PIN_COLORS } from '~/hooks/useMapMarkers' import { PIN_COLORS } from '~/hooks/useMapMarkers'
@ -18,7 +18,7 @@ type MapMarkerFormPopupProps = {
name: string name: string
notes: string notes: string
color: PinColorId color: PinColorId
}) => void }) => Promise<void> | void
onCancel: () => void onCancel: () => void
onDirtyChange?: (dirty: boolean) => void onDirtyChange?: (dirty: boolean) => void
} }
@ -34,6 +34,7 @@ export default function MapMarkerFormPopup({
const [name, setName] = useState(initialMarker?.name ?? '') const [name, setName] = useState(initialMarker?.name ?? '')
const [notes, setNotes] = useState(initialMarker?.notes ?? '') const [notes, setNotes] = useState(initialMarker?.notes ?? '')
const [color, setColor] = useState<PinColorId>(initialMarker?.color ?? 'orange') const [color, setColor] = useState<PinColorId>(initialMarker?.color ?? 'orange')
const [isSaving, setIsSaving] = useState(false)
const textareaRef = useRef<HTMLTextAreaElement | null>(null) const textareaRef = useRef<HTMLTextAreaElement | null>(null)
@ -49,10 +50,6 @@ export default function MapMarkerFormPopup({
resizeTextarea() resizeTextarea()
}, [resizeTextarea]) }, [resizeTextarea])
useLayoutEffect(() => {
resizeTextarea()
}, [])
const isDirty = const isDirty =
name !== (initialMarker?.name ?? '') || name !== (initialMarker?.name ?? '') ||
notes !== (initialMarker?.notes ?? '') || notes !== (initialMarker?.notes ?? '') ||
@ -62,15 +59,21 @@ export default function MapMarkerFormPopup({
onDirtyChange?.(isDirty) onDirtyChange?.(isDirty)
}, [isDirty, onDirtyChange]) }, [isDirty, onDirtyChange])
const handleSave = () => { const handleSave = async () => {
if (!name.trim()) return if (!name.trim() || isSaving) return
onSave({ try {
id: initialMarker?.id, setIsSaving(true)
name: name.trim(),
notes: notes.trim(), await onSave({
color, id: initialMarker?.id,
}) name: name.trim(),
notes: notes.trim(),
color,
})
} finally {
setIsSaving(false)
}
} }
return ( return (
@ -136,7 +139,8 @@ export default function MapMarkerFormPopup({
<button <button
type="button" type="button"
onClick={onCancel} 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 Cancel
</button> </button>
@ -144,10 +148,10 @@ export default function MapMarkerFormPopup({
<button <button
type="button" type="button"
onClick={handleSave} 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" 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> </button>
</div> </div>
</div> </div>

View File

@ -29,6 +29,8 @@ export default function ViewMapMarkerPopup({
{marker.notes && ( {marker.notes && (
<div className="mt-1 max-w-[240px] break-all whitespace-pre-wrap text-xs text-gray-500"> <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 <ReactMarkdown
remarkPlugins={[remarkGfm]} remarkPlugins={[remarkGfm]}
components={{ components={{

View File

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

View File

@ -21,3 +21,17 @@ export type MapLayer = {
'source-layer'?: string 'source-layer'?: string
[key: string]: any [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
}