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:
parent
ea006bd386
commit
b32b2e8b9e
4 changed files with 140 additions and 41 deletions
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
70
lightrag_webui/src/components/graph/MergeDialog.tsx
Normal file
70
lightrag_webui/src/components/graph/MergeDialog.tsx
Normal 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
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue