From b32b2e8b9eba4f28549782b8a471dddd921adcb0 Mon Sep 17 00:00:00 2001 From: yangdx Date: Tue, 28 Oct 2025 01:52:49 +0800 Subject: [PATCH] Refactor merge dialog and improve search history sync - Extract MergeDialog to separate component - Update search history on entity rename - Add dropdown refresh trigger mechanism - Sync query label with entity changes - Force graph re-render after updates --- .../components/graph/EditablePropertyRow.tsx | 82 ++++++++++--------- .../src/components/graph/GraphLabels.tsx | 16 ++++ .../src/components/graph/MergeDialog.tsx | 70 ++++++++++++++++ lightrag_webui/src/stores/settings.ts | 13 ++- 4 files changed, 140 insertions(+), 41 deletions(-) create mode 100644 lightrag_webui/src/components/graph/MergeDialog.tsx diff --git a/lightrag_webui/src/components/graph/EditablePropertyRow.tsx b/lightrag_webui/src/components/graph/EditablePropertyRow.tsx index 8f6639c1..5a1297d3 100644 --- a/lightrag_webui/src/components/graph/EditablePropertyRow.tsx +++ b/lightrag_webui/src/components/graph/EditablePropertyRow.tsx @@ -4,17 +4,10 @@ import { toast } from 'sonner' import { updateEntity, updateRelation, checkEntityNameExists } from '@/api/lightrag' import { useGraphStore } from '@/stores/graph' import { useSettingsStore } from '@/stores/settings' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle -} from '@/components/ui/Dialog' -import Button from '@/components/ui/Button' +import { SearchHistoryManager } from '@/utils/SearchHistoryManager' import { PropertyName, EditIcon, PropertyValue } from './PropertyRowComponents' import PropertyEditDialog from './PropertyEditDialog' +import MergeDialog from './MergeDialog' /** * Interface for the EditablePropertyRow component props @@ -123,6 +116,12 @@ const EditablePropertyRow = ({ sourceEntity: entityId, }) setMergeDialogOpen(true) + + // Remove old entity name from search history + SearchHistoryManager.removeLabel(entityId) + + // Note: Search Label update is deferred until user clicks refresh button in merge dialog + toast.success(t('graphPanel.propertiesView.success.entityMerged')) } else { // Node was updated/renamed normally @@ -134,6 +133,23 @@ const EditablePropertyRow = ({ console.error('Error updating node in graph:', error) throw new Error('Failed to update node in graph') } + + // Update search history: remove old name, add new name + if (name === 'entity_id') { + const currentLabel = useSettingsStore.getState().queryLabel + + SearchHistoryManager.removeLabel(entityId) + SearchHistoryManager.addToHistory(finalValue) + + // Trigger dropdown refresh to show updated search history + useSettingsStore.getState().triggerSearchLabelDropdownRefresh() + + // If current queryLabel is the old entity name, update to new name + if (currentLabel === entityId) { + useSettingsStore.getState().setQueryLabel(finalValue) + } + } + toast.success(t('graphPanel.propertiesView.success.entityUpdated')) } @@ -207,17 +223,28 @@ const EditablePropertyRow = ({ const info = mergeDialogInfo const graphState = useGraphStore.getState() const settingsState = useSettingsStore.getState() + const currentLabel = settingsState.queryLabel + // Clear graph state graphState.clearSelection() graphState.setGraphDataFetchAttempted(false) graphState.setLastSuccessfulQueryLabel('') if (useMergedStart && info?.targetEntity) { + // Use merged entity as new start point (might already be set in handleSave) settingsState.setQueryLabel(info.targetEntity) } else { - graphState.incrementGraphDataVersion() + // Keep current start point - refresh by resetting and restoring label + // This handles the case where user wants to stay with current label + settingsState.setQueryLabel('') + setTimeout(() => { + settingsState.setQueryLabel(currentLabel) + }, 50) } + // Force graph re-render and reset zoom/scale (same as refresh button behavior) + graphState.incrementGraphDataVersion() + setMergeDialogOpen(false) setMergeDialogInfo(null) toast.info(t('graphPanel.propertiesView.mergeDialog.refreshing')) @@ -242,42 +269,17 @@ const EditablePropertyRow = ({ errorMessage={errorMessage} /> - { setMergeDialogOpen(open) if (!open) { setMergeDialogInfo(null) } }} - > - - - {t('graphPanel.propertiesView.mergeDialog.title')} - - {t('graphPanel.propertiesView.mergeDialog.description', { - source: mergeDialogInfo?.sourceEntity ?? '', - target: mergeDialogInfo?.targetEntity ?? '', - })} - - -

- {t('graphPanel.propertiesView.mergeDialog.refreshHint')} -

- - - - -
-
+ onRefresh={handleMergeRefresh} + /> ) } diff --git a/lightrag_webui/src/components/graph/GraphLabels.tsx b/lightrag_webui/src/components/graph/GraphLabels.tsx index ca2d1691..17e7a4d3 100644 --- a/lightrag_webui/src/components/graph/GraphLabels.tsx +++ b/lightrag_webui/src/components/graph/GraphLabels.tsx @@ -17,6 +17,7 @@ import { getPopularLabels, searchLabels } from '@/api/lightrag' const GraphLabels = () => { const { t } = useTranslation() const label = useSettingsStore.use.queryLabel() + const dropdownRefreshTrigger = useSettingsStore.use.searchLabelDropdownRefreshTrigger() const [isRefreshing, setIsRefreshing] = useState(false) const [refreshTrigger, setRefreshTrigger] = useState(0) const [selectKey, setSelectKey] = useState(0) @@ -54,6 +55,18 @@ const GraphLabels = () => { initializeHistory() }, []) + // Force AsyncSelect to re-render when label changes externally (e.g., from entity rename/merge) + useEffect(() => { + setSelectKey(prev => prev + 1) + }, [label]) + + // Force AsyncSelect to re-render when dropdown refresh is triggered (e.g., after entity rename) + useEffect(() => { + if (dropdownRefreshTrigger > 0) { + setSelectKey(prev => prev + 1) + } + }, [dropdownRefreshTrigger]) + const fetchData = useCallback( async (query?: string): Promise => { let results: string[] = []; @@ -223,6 +236,9 @@ const GraphLabels = () => { // Update the label to trigger data loading useSettingsStore.getState().setQueryLabel(newLabel); + + // Force graph re-render and reset zoom/scale (must be AFTER setQueryLabel) + useGraphStore.getState().incrementGraphDataVersion(); }} clearable={false} // Prevent clearing value on reselect debounceTime={500} diff --git a/lightrag_webui/src/components/graph/MergeDialog.tsx b/lightrag_webui/src/components/graph/MergeDialog.tsx new file mode 100644 index 00000000..5ba18e3f --- /dev/null +++ b/lightrag_webui/src/components/graph/MergeDialog.tsx @@ -0,0 +1,70 @@ +import { useTranslation } from 'react-i18next' +import { useSettingsStore } from '@/stores/settings' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle +} from '@/components/ui/Dialog' +import Button from '@/components/ui/Button' + +interface MergeDialogProps { + mergeDialogOpen: boolean + mergeDialogInfo: { + targetEntity: string + sourceEntity: string + } | null + onOpenChange: (open: boolean) => void + onRefresh: (useMergedStart: boolean) => void +} + +/** + * MergeDialog component that appears after a successful entity merge + * Allows user to choose whether to use the merged entity or keep current start point + */ +const MergeDialog = ({ + mergeDialogOpen, + mergeDialogInfo, + onOpenChange, + onRefresh +}: MergeDialogProps) => { + const { t } = useTranslation() + const currentQueryLabel = useSettingsStore.use.queryLabel() + + return ( + + + + {t('graphPanel.propertiesView.mergeDialog.title')} + + {t('graphPanel.propertiesView.mergeDialog.description', { + source: mergeDialogInfo?.sourceEntity ?? '', + target: mergeDialogInfo?.targetEntity ?? '', + })} + + +

+ {t('graphPanel.propertiesView.mergeDialog.refreshHint')} +

+ + {currentQueryLabel !== mergeDialogInfo?.sourceEntity && ( + + )} + + +
+
+ ) +} + +export default MergeDialog diff --git a/lightrag_webui/src/stores/settings.ts b/lightrag_webui/src/stores/settings.ts index 79d3f3d1..983f5c43 100644 --- a/lightrag_webui/src/stores/settings.ts +++ b/lightrag_webui/src/stores/settings.ts @@ -78,6 +78,10 @@ interface SettingsState { currentTab: Tab setCurrentTab: (tab: Tab) => void + + // Search label dropdown refresh trigger (non-persistent, runtime only) + searchLabelDropdownRefreshTrigger: number + triggerSearchLabelDropdownRefresh: () => void } const useSettingsStoreBase = create()( @@ -229,7 +233,14 @@ const useSettingsStoreBase = create()( }) }, - setUserPromptHistory: (history: string[]) => set({ userPromptHistory: history }) + setUserPromptHistory: (history: string[]) => set({ userPromptHistory: history }), + + // Search label dropdown refresh trigger (not persisted) + searchLabelDropdownRefreshTrigger: 0, + triggerSearchLabelDropdownRefresh: () => + set((state) => ({ + searchLabelDropdownRefreshTrigger: state.searchLabelDropdownRefreshTrigger + 1 + })) }), { name: 'settings-storage',