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 { 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}
/>
<Dialog
open={mergeDialogOpen}
<MergeDialog
mergeDialogOpen={mergeDialogOpen}
mergeDialogInfo={mergeDialogInfo}
onOpenChange={(open) => {
setMergeDialogOpen(open)
if (!open) {
setMergeDialogInfo(null)
}
}}
>
<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>
onRefresh={handleMergeRefresh}
/>
</div>
)
}

View file

@ -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<string[]> => {
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}

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
setCurrentTab: (tab: Tab) => void
// Search label dropdown refresh trigger (non-persistent, runtime only)
searchLabelDropdownRefreshTrigger: number
triggerSearchLabelDropdownRefresh: () => void
}
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',