mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
fix: show error dialog on shared URL failure and fix import annotation merge (#205)
* chore: open v0.10.0 pre-release fix branch Tracking two bugs found during pre-release testing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: show error dialog on shared URL failure and fix import annotation merge Bug 1: When a shared URL fails to load (truncated hash, expired paste, missing encryption key), the portal now shows a warning dialog explaining the failure and an amber "Demo" badge so users know they're seeing sample content — not their data. Bug 2: Importing a teammate's share URL no longer replaces existing annotations. Uses React functional state updaters to avoid stale closures and passes the full merged annotation list to pendingSharedAnnotations so DOM highlights are re-applied for all annotations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -453,6 +453,8 @@ const App: React.FC = () => {
|
||||
clearPendingSharedAnnotations,
|
||||
generateShortUrl,
|
||||
importFromShareUrl,
|
||||
shareLoadError,
|
||||
clearShareLoadError,
|
||||
} = useSharing(
|
||||
markdown,
|
||||
annotations,
|
||||
@@ -1277,6 +1279,7 @@ const App: React.FC = () => {
|
||||
isPlanDiffActive={isPlanDiffActive}
|
||||
onPlanDiffToggle={() => setIsPlanDiffActive(!isPlanDiffActive)}
|
||||
hasPreviousVersion={!linkedDocHook.isActive && planDiff.hasPreviousVersion}
|
||||
showDemoBadge={!isApiMode && !isLoadingShared && !isSharedSession}
|
||||
onOpenLinkedDoc={linkedDocHook.open}
|
||||
linkedDocInfo={linkedDocHook.isActive ? { filepath: linkedDocHook.filepath!, onBack: linkedDocHook.back } : null}
|
||||
/>
|
||||
@@ -1386,6 +1389,16 @@ const App: React.FC = () => {
|
||||
showCancel
|
||||
/>
|
||||
|
||||
{/* Shared URL load failure warning */}
|
||||
<ConfirmDialog
|
||||
isOpen={!!shareLoadError && !isApiMode}
|
||||
onClose={clearShareLoadError}
|
||||
title="Shared Plan Could Not Be Loaded"
|
||||
message={shareLoadError}
|
||||
subMessage="You are viewing a demo plan. This is sample content — it is not your data or anyone else's."
|
||||
variant="warning"
|
||||
/>
|
||||
|
||||
{/* Save-to-notes toast */}
|
||||
{noteSaveToast && (
|
||||
<div className={`fixed top-16 right-4 z-50 px-3 py-2 rounded-lg text-xs font-medium shadow-lg transition-opacity ${
|
||||
|
||||
@@ -33,6 +33,8 @@ interface ViewerProps {
|
||||
isPlanDiffActive?: boolean;
|
||||
onPlanDiffToggle?: () => void;
|
||||
hasPreviousVersion?: boolean;
|
||||
/** Show amber "Demo" badge (portal mode, no shared content loaded) */
|
||||
showDemoBadge?: boolean;
|
||||
}
|
||||
|
||||
export interface ViewerHandle {
|
||||
@@ -93,6 +95,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
||||
isPlanDiffActive,
|
||||
onPlanDiffToggle,
|
||||
hasPreviousVersion,
|
||||
showDemoBadge,
|
||||
onOpenLinkedDoc,
|
||||
linkedDocInfo,
|
||||
}, ref) => {
|
||||
@@ -648,8 +651,8 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
||||
linkedDocInfo ? 'border-2 border-primary' : 'border border-border/50'
|
||||
}`}
|
||||
>
|
||||
{/* Repo info + plan diff badge + linked doc badge - top left */}
|
||||
{(repoInfo || hasPreviousVersion || linkedDocInfo) && (
|
||||
{/* Repo info + plan diff badge + demo badge + linked doc badge - top left */}
|
||||
{(repoInfo || hasPreviousVersion || showDemoBadge || linkedDocInfo) && (
|
||||
<div className="absolute top-3 left-3 md:top-4 md:left-5 flex flex-col items-start gap-1 text-[9px] text-muted-foreground/50 font-mono">
|
||||
{repoInfo && !linkedDocInfo && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
@@ -674,6 +677,11 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
||||
hasPreviousVersion={hasPreviousVersion ?? false}
|
||||
/>
|
||||
)}
|
||||
{showDemoBadge && !linkedDocInfo && (
|
||||
<span className="px-1.5 py-0.5 rounded text-[9px] font-mono bg-amber-500/15 text-amber-600 dark:text-amber-400">
|
||||
Demo
|
||||
</span>
|
||||
)}
|
||||
{linkedDocInfo && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<button
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* - Tracking whether current session is from a shared link
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Annotation, type ImageAttachment } from '../types';
|
||||
import {
|
||||
type SharePayload,
|
||||
@@ -68,6 +68,12 @@ interface UseSharingResult {
|
||||
|
||||
/** Import annotations from a teammate's share URL */
|
||||
importFromShareUrl: (url: string) => Promise<ImportResult>;
|
||||
|
||||
/** Error message when a shared URL failed to load on mount */
|
||||
shareLoadError: string;
|
||||
|
||||
/** Clear the share load error */
|
||||
clearShareLoadError: () => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -76,8 +82,8 @@ export function useSharing(
|
||||
annotations: Annotation[],
|
||||
globalAttachments: ImageAttachment[],
|
||||
setMarkdown: (m: string) => void,
|
||||
setAnnotations: (a: Annotation[]) => void,
|
||||
setGlobalAttachments: (g: ImageAttachment[]) => void,
|
||||
setAnnotations: React.Dispatch<React.SetStateAction<Annotation[]>>,
|
||||
setGlobalAttachments: React.Dispatch<React.SetStateAction<ImageAttachment[]>>,
|
||||
onSharedLoad?: () => void,
|
||||
shareBaseUrl?: string,
|
||||
pasteApiUrl?: string
|
||||
@@ -91,12 +97,15 @@ export function useSharing(
|
||||
const [shortUrlError, setShortUrlError] = useState('');
|
||||
const [pendingSharedAnnotations, setPendingSharedAnnotations] = useState<Annotation[] | null>(null);
|
||||
const [sharedGlobalAttachments, setSharedGlobalAttachments] = useState<ImageAttachment[] | null>(null);
|
||||
const [shareLoadError, setShareLoadError] = useState('');
|
||||
|
||||
const clearPendingSharedAnnotations = useCallback(() => {
|
||||
setPendingSharedAnnotations(null);
|
||||
setSharedGlobalAttachments(null);
|
||||
}, []);
|
||||
|
||||
const clearShareLoadError = useCallback(() => setShareLoadError(''), []);
|
||||
|
||||
// Load shared state from URL hash (or paste-service short URL)
|
||||
const loadFromHash = useCallback(async () => {
|
||||
try {
|
||||
@@ -135,9 +144,11 @@ export function useSharing(
|
||||
}
|
||||
// Paste fetch failed — short URL path can't fall back to hash parsing
|
||||
// (the hash contains #key=, not plan data).
|
||||
setShareLoadError('Failed to load shared plan — the link may be expired or incomplete.');
|
||||
return false;
|
||||
}
|
||||
|
||||
const hash = window.location.hash.slice(1);
|
||||
const payload = await parseShareHash();
|
||||
|
||||
if (payload) {
|
||||
@@ -173,9 +184,15 @@ export function useSharing(
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Hash was present but failed to decompress (likely truncated by browser)
|
||||
if (hash) {
|
||||
setShareLoadError('Failed to load shared plan — the URL may have been truncated by your browser.');
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
console.error('Failed to load from share hash:', e);
|
||||
setShareLoadError('Failed to load shared plan — an unexpected error occurred.');
|
||||
return false;
|
||||
}
|
||||
}, [setMarkdown, setAnnotations, setGlobalAttachments, onSharedLoad, pasteApiUrl]);
|
||||
@@ -297,8 +314,9 @@ export function useSharing(
|
||||
return { success: true, count: 0, planTitle, error: 'No annotations found in share link' };
|
||||
}
|
||||
|
||||
// Deduplicate: skip annotations that already exist (by originalText + type + text)
|
||||
const newAnnotations = importedAnnotations.filter(imp =>
|
||||
// Estimate count from current closure (may be slightly stale, but
|
||||
// the actual merge below uses the latest state via functional updater)
|
||||
const estimatedNew = importedAnnotations.filter(imp =>
|
||||
!annotations.some(existing =>
|
||||
existing.originalText === imp.originalText &&
|
||||
existing.type === imp.type &&
|
||||
@@ -306,26 +324,36 @@ export function useSharing(
|
||||
)
|
||||
);
|
||||
|
||||
if (newAnnotations.length > 0) {
|
||||
// Merge: append new annotations to existing ones
|
||||
setAnnotations([...annotations, ...newAnnotations]);
|
||||
|
||||
// Set as pending so they get applied to DOM highlights
|
||||
setPendingSharedAnnotations(newAnnotations);
|
||||
if (estimatedNew.length > 0) {
|
||||
// Merge using functional updater to avoid stale closure
|
||||
setAnnotations(prev => {
|
||||
const newAnnotations = importedAnnotations.filter(imp =>
|
||||
!prev.some(existing =>
|
||||
existing.originalText === imp.originalText &&
|
||||
existing.type === imp.type &&
|
||||
existing.text === imp.text
|
||||
)
|
||||
);
|
||||
if (newAnnotations.length === 0) return prev;
|
||||
const merged = [...prev, ...newAnnotations];
|
||||
// Set ALL annotations as pending so DOM highlights include originals
|
||||
setPendingSharedAnnotations(merged);
|
||||
return merged;
|
||||
});
|
||||
|
||||
// Handle global attachments (deduplicate by path)
|
||||
if (payload.g?.length) {
|
||||
const parsed = parseShareableImages(payload.g) ?? [];
|
||||
const existingPaths = new Set(globalAttachments.map(g => g.path));
|
||||
const newAttachments = parsed.filter(p => !existingPaths.has(p.path));
|
||||
if (newAttachments.length > 0) {
|
||||
setGlobalAttachments([...globalAttachments, ...newAttachments]);
|
||||
}
|
||||
setGlobalAttachments(prev => {
|
||||
const existingPaths = new Set(prev.map(g => g.path));
|
||||
const newAttachments = parsed.filter(p => !existingPaths.has(p.path));
|
||||
return newAttachments.length > 0 ? [...prev, ...newAttachments] : prev;
|
||||
});
|
||||
setSharedGlobalAttachments(parsed);
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, count: newAnnotations.length, planTitle };
|
||||
return { success: true, count: estimatedNew.length, planTitle };
|
||||
} catch (e) {
|
||||
const errorMessage = e instanceof Error ? e.message : 'Failed to decompress share URL';
|
||||
return { success: false, count: 0, planTitle: '', error: errorMessage };
|
||||
@@ -346,5 +374,7 @@ export function useSharing(
|
||||
refreshShareUrl,
|
||||
generateShortUrl,
|
||||
importFromShareUrl,
|
||||
shareLoadError,
|
||||
clearShareLoadError,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user