|
1 | | -import React, { useEffect, useState, useMemo, useCallback } from 'react'; |
| 1 | +import React, { useEffect, useRef, useState, useMemo, useCallback } from 'react'; |
2 | 2 | import { |
3 | 3 | ThemeProvider, |
4 | 4 | createTheme, |
@@ -357,6 +357,75 @@ const App: React.FC = () => { |
357 | 357 | showToast, |
358 | 358 | }); |
359 | 359 |
|
| 360 | + // Modes that represent an active workflow worth persisting |
| 361 | + const PERSISTABLE_MODES = new Set([ |
| 362 | + 'scan-results', 'preview', 'platform-select', 'container-type', |
| 363 | + 'container-form', 'existing-containers', 'entity', 'not-found', |
| 364 | + 'investigation', 'scenario-overview', 'scenario-form', |
| 365 | + 'atomic-testing', 'unified-search', 'add', 'add-selection', |
| 366 | + 'add-to-scan-results', |
| 367 | + ]); |
| 368 | + |
| 369 | + // Track whether we've restored state to avoid save/restore loops |
| 370 | + const hasRestoredState = useRef(false); |
| 371 | + // Platform ID that needs URL resolution once platforms finish loading |
| 372 | + const pendingPlatformIdRef = useRef<string | null>(null); |
| 373 | + |
| 374 | + // Debounced persistence of panel workflow state. |
| 375 | + // Reads scan data from refs to always capture the latest values without |
| 376 | + // requiring them in the dependency array (which would trigger too-frequent saves). |
| 377 | + useEffect(() => { |
| 378 | + if (!hasRestoredState.current) return; |
| 379 | + if (!PERSISTABLE_MODES.has(panelMode)) return; |
| 380 | + if (typeof chrome === 'undefined' || !chrome.runtime?.sendMessage) return; |
| 381 | + |
| 382 | + const timer = setTimeout(() => { |
| 383 | + const currentScanEntities = scanResultsEntitiesRef.current; |
| 384 | + const stateToSave = { |
| 385 | + panelMode, |
| 386 | + containerType, |
| 387 | + containerForm, |
| 388 | + containerWorkflowOrigin, |
| 389 | + selectedPlatformId, |
| 390 | + currentPageUrl, |
| 391 | + currentPageTitle, |
| 392 | + currentPdfFileName, |
| 393 | + isPdfSource, |
| 394 | + scanResultsEntities: currentScanEntities.length > 0 ? currentScanEntities : undefined, |
| 395 | + selectedScanItems: selectedScanItems.size > 0 ? Array.from(selectedScanItems) : undefined, |
| 396 | + scanPageContent: scanPageContent || undefined, |
| 397 | + timestamp: Date.now(), |
| 398 | + }; |
| 399 | + chrome.runtime.sendMessage({ type: 'SAVE_PANEL_STATE', payload: stateToSave }); |
| 400 | + }, 500); |
| 401 | + |
| 402 | + return () => clearTimeout(timer); |
| 403 | + // eslint-disable-next-line react-hooks/exhaustive-deps |
| 404 | + }, [panelMode, containerType, containerForm, containerWorkflowOrigin, |
| 405 | + selectedPlatformId, currentPageUrl, currentPageTitle, |
| 406 | + selectedScanItems, scanPageContent]); |
| 407 | + |
| 408 | + // Resolve deferred platform URL once platforms finish loading |
| 409 | + useEffect(() => { |
| 410 | + const pid = pendingPlatformIdRef.current; |
| 411 | + if (pid && availablePlatforms.length > 0) { |
| 412 | + const platform = availablePlatforms.find(p => p.id === pid); |
| 413 | + if (platform) { |
| 414 | + setPlatformUrl(platform.url); |
| 415 | + pendingPlatformIdRef.current = null; |
| 416 | + } |
| 417 | + } |
| 418 | + }, [availablePlatforms, setPlatformUrl]); |
| 419 | + |
| 420 | + // Clear persisted state when a workflow completes or user returns to empty |
| 421 | + useEffect(() => { |
| 422 | + if (panelMode === 'empty' || panelMode === 'import-results') { |
| 423 | + if (typeof chrome !== 'undefined' && chrome.runtime?.sendMessage) { |
| 424 | + chrome.runtime.sendMessage({ type: 'CLEAR_PANEL_STATE' }); |
| 425 | + } |
| 426 | + } |
| 427 | + }, [panelMode]); |
| 428 | + |
360 | 429 | // Sync entitiesToAdd with selectedScanItems when in preview mode |
361 | 430 | React.useEffect(() => { |
362 | 431 | if (panelMode === 'preview' && scanResultsEntitiesRef.current.length > 0) { |
@@ -645,7 +714,15 @@ const App: React.FC = () => { |
645 | 714 | setTimeout(() => { |
646 | 715 | chrome.runtime.sendMessage({ type: 'GET_PANEL_STATE' }, (response) => { |
647 | 716 | if (chrome.runtime.lastError) return; |
648 | | - if (response?.success && response.data) handlePanelState(response.data); |
| 717 | + if (response?.success && response.data) { |
| 718 | + handlePanelState(response.data); |
| 719 | + // Restore persisted workflow state if available |
| 720 | + const ws = response.data.workflowState; |
| 721 | + if (ws && ws.panelMode && ws.panelMode !== 'empty' && ws.panelMode !== 'loading') { |
| 722 | + restoreWorkflowState(ws); |
| 723 | + } |
| 724 | + } |
| 725 | + hasRestoredState.current = true; |
649 | 726 | }); |
650 | 727 | }, 50); |
651 | 728 |
|
@@ -845,6 +922,49 @@ const App: React.FC = () => { |
845 | 922 | // eslint-disable-next-line react-hooks/exhaustive-deps |
846 | 923 | }, [panelMode, selectedPlatformId]); |
847 | 924 |
|
| 925 | + // Restore workflow state from persisted data (called on panel mount) |
| 926 | + const restoreWorkflowState = (ws: { |
| 927 | + panelMode?: string; |
| 928 | + containerType?: string; |
| 929 | + containerForm?: { name: string; description: string; content?: string }; |
| 930 | + containerWorkflowOrigin?: string; |
| 931 | + selectedPlatformId?: string; |
| 932 | + currentPageUrl?: string; |
| 933 | + currentPageTitle?: string; |
| 934 | + currentPdfFileName?: string; |
| 935 | + isPdfSource?: boolean; |
| 936 | + scanResultsEntities?: unknown[]; |
| 937 | + selectedScanItems?: string[]; |
| 938 | + scanPageContent?: string; |
| 939 | + }) => { |
| 940 | + log.debug('[PANEL] Restoring workflow state, mode:', ws.panelMode); |
| 941 | + if (ws.currentPageUrl) setCurrentPageUrl(ws.currentPageUrl); |
| 942 | + if (ws.currentPageTitle) setCurrentPageTitle(ws.currentPageTitle); |
| 943 | + if (ws.currentPdfFileName) setCurrentPdfFileName(ws.currentPdfFileName); |
| 944 | + if (ws.isPdfSource !== undefined) setIsPdfSource(ws.isPdfSource); |
| 945 | + if (ws.selectedPlatformId) { |
| 946 | + setSelectedPlatformId(ws.selectedPlatformId); |
| 947 | + const platform = availablePlatformsRef.current.find(p => p.id === ws.selectedPlatformId); |
| 948 | + if (platform) { |
| 949 | + setPlatformUrl(platform.url); |
| 950 | + } else { |
| 951 | + pendingPlatformIdRef.current = ws.selectedPlatformId; |
| 952 | + } |
| 953 | + } |
| 954 | + if (ws.scanResultsEntities && Array.isArray(ws.scanResultsEntities) && ws.scanResultsEntities.length > 0) { |
| 955 | + setScanResultsEntities(ws.scanResultsEntities as typeof scanResultsEntities); |
| 956 | + scanResultsEntitiesRef.current = ws.scanResultsEntities as typeof scanResultsEntities; |
| 957 | + } |
| 958 | + if (ws.selectedScanItems && Array.isArray(ws.selectedScanItems)) { |
| 959 | + setSelectedScanItems(new Set(ws.selectedScanItems)); |
| 960 | + } |
| 961 | + if (ws.scanPageContent) setScanPageContent(ws.scanPageContent); |
| 962 | + if (ws.containerType) setOCTIContainerType(ws.containerType as Parameters<typeof setOCTIContainerType>[0]); |
| 963 | + if (ws.containerForm) setContainerForm(ws.containerForm as Parameters<typeof setContainerForm>[0]); |
| 964 | + if (ws.containerWorkflowOrigin) setContainerWorkflowOrigin(ws.containerWorkflowOrigin as 'preview' | 'direct' | 'import'); |
| 965 | + if (ws.panelMode) setPanelMode(ws.panelMode as PanelMode); |
| 966 | + }; |
| 967 | + |
848 | 968 | const handleMessage = (event: MessageEvent) => { |
849 | 969 | const { type, payload } = event.data; |
850 | 970 | handlePanelState({ type, payload }); |
|
0 commit comments