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
This commit is contained in:
yangdx 2025-10-28 01:52:49 +08:00
parent ea006bd386
commit b32b2e8b9e
4 changed files with 140 additions and 41 deletions

View file

@ -4,17 +4,10 @@ import { toast } from 'sonner'
import { updateEntity, updateRelation, checkEntityNameExists } from '@/api/lightrag' import { updateEntity, updateRelation, checkEntityNameExists } from '@/api/lightrag'
import { useGraphStore } from '@/stores/graph' import { useGraphStore } from '@/stores/graph'
import { useSettingsStore } from '@/stores/settings' import { useSettingsStore } from '@/stores/settings'
import { import { SearchHistoryManager } from '@/utils/SearchHistoryManager'
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
} from '@/components/ui/Dialog'
import Button from '@/components/ui/Button'
import { PropertyName, EditIcon, PropertyValue } from './PropertyRowComponents' import { PropertyName, EditIcon, PropertyValue } from './PropertyRowComponents'
import PropertyEditDialog from './PropertyEditDialog' import PropertyEditDialog from './PropertyEditDialog'
import MergeDialog from './MergeDialog'
/** /**
* Interface for the EditablePropertyRow component props * Interface for the EditablePropertyRow component props
@ -123,6 +116,12 @@ const EditablePropertyRow = ({
sourceEntity: entityId, sourceEntity: entityId,
}) })
setMergeDialogOpen(true) 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')) toast.success(t('graphPanel.propertiesView.success.entityMerged'))
} else { } else {
// Node was updated/renamed normally // Node was updated/renamed normally
@ -134,6 +133,23 @@ const EditablePropertyRow = ({
console.error('Error updating node in graph:', error) console.error('Error updating node in graph:', error)
throw new Error('Failed to update node in graph') 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')) toast.success(t('graphPanel.propertiesView.success.entityUpdated'))
} }
@ -207,17 +223,28 @@ const EditablePropertyRow = ({
const info = mergeDialogInfo const info = mergeDialogInfo
const graphState = useGraphStore.getState() const graphState = useGraphStore.getState()
const settingsState = useSettingsStore.getState() const settingsState = useSettingsStore.getState()
const currentLabel = settingsState.queryLabel
// Clear graph state
graphState.clearSelection() graphState.clearSelection()
graphState.setGraphDataFetchAttempted(false) graphState.setGraphDataFetchAttempted(false)
graphState.setLastSuccessfulQueryLabel('') graphState.setLastSuccessfulQueryLabel('')
if (useMergedStart && info?.targetEntity) { if (useMergedStart && info?.targetEntity) {
// Use merged entity as new start point (might already be set in handleSave)
settingsState.setQueryLabel(info.targetEntity) settingsState.setQueryLabel(info.targetEntity)
} else { } 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) setMergeDialogOpen(false)
setMergeDialogInfo(null) setMergeDialogInfo(null)
toast.info(t('graphPanel.propertiesView.mergeDialog.refreshing')) toast.info(t('graphPanel.propertiesView.mergeDialog.refreshing'))
@ -242,42 +269,17 @@ const EditablePropertyRow = ({
errorMessage={errorMessage} errorMessage={errorMessage}
/> />
<Dialog <MergeDialog
open={mergeDialogOpen} mergeDialogOpen={mergeDialogOpen}
mergeDialogInfo={mergeDialogInfo}
onOpenChange={(open) => { onOpenChange={(open) => {
setMergeDialogOpen(open) setMergeDialogOpen(open)
if (!open) { if (!open) {
setMergeDialogInfo(null) setMergeDialogInfo(null)
} }
}} }}
> onRefresh={handleMergeRefresh}
<DialogContent> />
<DialogHeader>
<DialogTitle>{t('graphPanel.propertiesView.mergeDialog.title')}</DialogTitle>
<DialogDescription>
{t('graphPanel.propertiesView.mergeDialog.description', {
source: mergeDialogInfo?.sourceEntity ?? '',
target: mergeDialogInfo?.targetEntity ?? '',
})}
</DialogDescription>
</DialogHeader>
<p className="text-sm text-muted-foreground">
{t('graphPanel.propertiesView.mergeDialog.refreshHint')}
</p>
<DialogFooter className="mt-4 flex-col gap-2 sm:flex-row sm:justify-end">
<Button
type="button"
variant="outline"
onClick={() => handleMergeRefresh(false)}
>
{t('graphPanel.propertiesView.mergeDialog.keepCurrentStart')}
</Button>
<Button type="button" onClick={() => handleMergeRefresh(true)}>
{t('graphPanel.propertiesView.mergeDialog.useMergedStart')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div> </div>
) )
} }

View file

@ -17,6 +17,7 @@ import { getPopularLabels, searchLabels } from '@/api/lightrag'
const GraphLabels = () => { const GraphLabels = () => {
const { t } = useTranslation() const { t } = useTranslation()
const label = useSettingsStore.use.queryLabel() const label = useSettingsStore.use.queryLabel()
const dropdownRefreshTrigger = useSettingsStore.use.searchLabelDropdownRefreshTrigger()
const [isRefreshing, setIsRefreshing] = useState(false) const [isRefreshing, setIsRefreshing] = useState(false)
const [refreshTrigger, setRefreshTrigger] = useState(0) const [refreshTrigger, setRefreshTrigger] = useState(0)
const [selectKey, setSelectKey] = useState(0) const [selectKey, setSelectKey] = useState(0)
@ -54,6 +55,18 @@ const GraphLabels = () => {
initializeHistory() 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( const fetchData = useCallback(
async (query?: string): Promise<string[]> => { async (query?: string): Promise<string[]> => {
let results: string[] = []; let results: string[] = [];
@ -223,6 +236,9 @@ const GraphLabels = () => {
// Update the label to trigger data loading // Update the label to trigger data loading
useSettingsStore.getState().setQueryLabel(newLabel); 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 clearable={false} // Prevent clearing value on reselect
debounceTime={500} debounceTime={500}

View file

@ -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 (
<Dialog open={mergeDialogOpen} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('graphPanel.propertiesView.mergeDialog.title')}</DialogTitle>
<DialogDescription>
{t('graphPanel.propertiesView.mergeDialog.description', {
source: mergeDialogInfo?.sourceEntity ?? '',
target: mergeDialogInfo?.targetEntity ?? '',
})}
</DialogDescription>
</DialogHeader>
<p className="text-sm text-muted-foreground">
{t('graphPanel.propertiesView.mergeDialog.refreshHint')}
</p>
<DialogFooter className="mt-4 flex-col gap-2 sm:flex-row sm:justify-end">
{currentQueryLabel !== mergeDialogInfo?.sourceEntity && (
<Button
type="button"
variant="outline"
onClick={() => onRefresh(false)}
>
{t('graphPanel.propertiesView.mergeDialog.keepCurrentStart')}
</Button>
)}
<Button type="button" onClick={() => onRefresh(true)}>
{t('graphPanel.propertiesView.mergeDialog.useMergedStart')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
export default MergeDialog

View file

@ -78,6 +78,10 @@ interface SettingsState {
currentTab: Tab currentTab: Tab
setCurrentTab: (tab: Tab) => void setCurrentTab: (tab: Tab) => void
// Search label dropdown refresh trigger (non-persistent, runtime only)
searchLabelDropdownRefreshTrigger: number
triggerSearchLabelDropdownRefresh: () => void
} }
const useSettingsStoreBase = create<SettingsState>()( const useSettingsStoreBase = create<SettingsState>()(
@ -229,7 +233,14 @@ const useSettingsStoreBase = create<SettingsState>()(
}) })
}, },
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', name: 'settings-storage',