Sync notes through GPX, Journal, and Planner roundtrip
Full notes lifecycle: - GPX: description field in GpxData, <metadata><desc> in generate/parse - Export: Plan GPX and Save to Journal include notes as description - Journal: updateRoute extracts description from GPX, stores on route - Reimport: Edit in Planner passes notes via URL params → Yjs Y.Text - Drop import: GPX with <desc> restores notes in session Spec updated: session-notes gains GPX export, Journal sync, and reimport requirements. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b12b706b77
commit
08070cdd90
13 changed files with 62 additions and 9 deletions
|
|
@ -69,7 +69,8 @@ export function ExportButton({ yjs }: { yjs: YjsState }) {
|
|||
const tracks = getTracks(yjs);
|
||||
const waypoints = getWaypoints(yjs);
|
||||
const noGoAreas = getNoGoAreas(yjs);
|
||||
const gpx = generateGpx({ name: "trails.cool route", waypoints, tracks, noGoAreas });
|
||||
const notes = yjs.notes.toString() || undefined;
|
||||
const gpx = generateGpx({ name: "trails.cool route", description: notes, waypoints, tracks, noGoAreas });
|
||||
download(gpx, "route-plan.gpx");
|
||||
setOpen(false);
|
||||
}, [yjs]);
|
||||
|
|
|
|||
|
|
@ -614,6 +614,12 @@ export function PlannerMap({ yjs, onRouteRequest, highlightPosition, highlighted
|
|||
yMap.set("points", area.points);
|
||||
yjs.noGoAreas.push([yMap]);
|
||||
}
|
||||
|
||||
// Replace notes if GPX has a description
|
||||
if (gpxData.description) {
|
||||
yjs.notes.delete(0, yjs.notes.length);
|
||||
yjs.notes.insert(0, gpxData.description);
|
||||
}
|
||||
}, "local");
|
||||
} catch {
|
||||
onImportError?.(t("importGpxError"));
|
||||
|
|
|
|||
|
|
@ -48,7 +48,8 @@ export function SaveToJournalButton({ yjs, callbackUrl, callbackToken, returnUrl
|
|||
isDayBreak: yMap.get("overnight") === true ? true : undefined,
|
||||
}));
|
||||
|
||||
const gpx = generateGpx({ name: "trails.cool route", waypoints, tracks, noGoAreas });
|
||||
const notes = yjs.notes.toString() || undefined;
|
||||
const gpx = generateGpx({ name: "trails.cool route", description: notes, waypoints, tracks, noGoAreas });
|
||||
|
||||
// POST to Journal callback
|
||||
const response = await fetch(callbackUrl, {
|
||||
|
|
|
|||
|
|
@ -152,12 +152,13 @@ interface SessionViewProps {
|
|||
returnUrl?: string;
|
||||
initialWaypoints?: Array<{ lat: number; lon: number; name?: string; isDayBreak?: boolean }>;
|
||||
initialNoGoAreas?: Array<{ points: Array<{ lat: number; lon: number }> }>;
|
||||
initialNotes?: string;
|
||||
}
|
||||
|
||||
export function SessionView({ sessionId, callbackUrl, callbackToken, returnUrl, initialWaypoints, initialNoGoAreas }: SessionViewProps) {
|
||||
export function SessionView({ sessionId, callbackUrl, callbackToken, returnUrl, initialWaypoints, initialNoGoAreas, initialNotes }: SessionViewProps) {
|
||||
const { t } = useTranslation("planner");
|
||||
useEffect(() => { Sentry.setTag("session_id", sessionId); }, [sessionId]);
|
||||
const yjs = useYjs(sessionId, initialWaypoints, initialNoGoAreas);
|
||||
const yjs = useYjs(sessionId, initialWaypoints, initialNoGoAreas, initialNotes);
|
||||
const { computing, routeError, routeStats, requestRoute } = useRouting(yjs);
|
||||
const { canUndo, canRedo, undo, redo } = useUndo(yjs?.undoManager ?? null);
|
||||
useUndoShortcuts(yjs?.undoManager ?? null);
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ export function useYjs(
|
|||
sessionId: string,
|
||||
initialWaypoints?: Array<{ lat: number; lon: number; name?: string; isDayBreak?: boolean }>,
|
||||
initialNoGoAreas?: Array<{ points: Array<{ lat: number; lon: number }> }>,
|
||||
initialNotes?: string,
|
||||
): YjsState | null {
|
||||
const [state, setState] = useState<YjsState | null>(null);
|
||||
const providerRef = useRef<WebsocketProvider | null>(null);
|
||||
|
|
@ -116,6 +117,9 @@ export function useYjs(
|
|||
noGoAreas.push([yMap]);
|
||||
}
|
||||
}
|
||||
if (initialNotes && notes.length === 0) {
|
||||
notes.insert(0, initialNotes);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,19 +21,21 @@ export async function action({ request }: Route.ActionArgs) {
|
|||
|
||||
let initialWaypoints: Array<{ lat: number; lon: number; name?: string; isDayBreak?: boolean }> | undefined;
|
||||
let initialNoGoAreas: Array<{ points: Array<{ lat: number; lon: number }> }> | undefined;
|
||||
let initialNotes: string | undefined;
|
||||
if (gpx) {
|
||||
try {
|
||||
const gpxData = await parseGpxAsync(gpx);
|
||||
const wps = extractWaypoints(gpxData);
|
||||
if (wps.length > 0) initialWaypoints = wps;
|
||||
if (gpxData.noGoAreas.length > 0) initialNoGoAreas = gpxData.noGoAreas;
|
||||
if (gpxData.description) initialNotes = gpxData.description;
|
||||
} catch {
|
||||
// Continue with empty session if GPX is invalid
|
||||
}
|
||||
}
|
||||
|
||||
return data(
|
||||
{ sessionId: session.id, url: `/session/${session.id}`, initialWaypoints, initialNoGoAreas },
|
||||
{ sessionId: session.id, url: `/session/${session.id}`, initialWaypoints, initialNoGoAreas, initialNotes },
|
||||
{ status: 201 },
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ export default function SessionPage({ loaderData }: Route.ComponentProps) {
|
|||
if (noGoParam) {
|
||||
try { initialNoGoAreas = JSON.parse(noGoParam); } catch { /* ignore */ }
|
||||
}
|
||||
const initialNotes = searchParams.get("notes") ?? undefined;
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
|
|
@ -67,6 +68,7 @@ export default function SessionPage({ loaderData }: Route.ComponentProps) {
|
|||
returnUrl={returnUrl}
|
||||
initialWaypoints={initialWaypoints}
|
||||
initialNoGoAreas={initialNoGoAreas}
|
||||
initialNotes={initialNotes}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue