| import { useState, useEffect } from 'react' |
| import { useTranslation } from 'react-i18next' |
| import { toast } from 'sonner' |
| import { updateEntity, updateRelation, checkEntityNameExists } from '@/api/lightrag' |
| import { useGraphStore } from '@/stores/graph' |
| import { useSettingsStore } from '@/stores/settings' |
| import { SearchHistoryManager } from '@/utils/SearchHistoryManager' |
| import { PropertyName, EditIcon, PropertyValue } from './PropertyRowComponents' |
| import PropertyEditDialog from './PropertyEditDialog' |
| import MergeDialog from './MergeDialog' |
|
|
| const createErrorWithCause = (message: string, cause: unknown): Error => { |
| const error = new Error(message) as Error & { cause?: unknown } |
| error.cause = cause |
| return error |
| } |
|
|
| |
| |
| |
| interface EditablePropertyRowProps { |
| name: string |
| value: any |
| onClick?: () => void |
| nodeId?: string |
| entityId?: string |
| edgeId?: string |
| dynamicId?: string |
| entityType?: 'node' | 'edge' |
| sourceId?: string |
| targetId?: string |
| onValueChange?: (newValue: any) => void |
| isEditable?: boolean |
| tooltip?: string |
| } |
|
|
| |
| |
| |
| |
| const EditablePropertyRow = ({ |
| name, |
| value: initialValue, |
| onClick, |
| nodeId, |
| edgeId, |
| entityId, |
| dynamicId, |
| entityType, |
| sourceId, |
| targetId, |
| onValueChange, |
| isEditable = false, |
| tooltip |
| }: EditablePropertyRowProps) => { |
| const { t } = useTranslation() |
| const [isEditing, setIsEditing] = useState(false) |
| const [isSubmitting, setIsSubmitting] = useState(false) |
| const [currentValue, setCurrentValue] = useState(initialValue) |
| const [draftValue, setDraftValue] = useState(String(initialValue)) |
| const [draftAllowMerge, setDraftAllowMerge] = useState(false) |
| const [errorMessage, setErrorMessage] = useState<string | null>(null) |
| const [mergeDialogOpen, setMergeDialogOpen] = useState(false) |
| const [mergeDialogInfo, setMergeDialogInfo] = useState<{ |
| targetEntity: string |
| sourceEntity: string |
| } | null>(null) |
|
|
| useEffect(() => { |
| setCurrentValue(initialValue) |
| }, [initialValue]) |
|
|
| const handleEditClick = () => { |
| if (isEditable && !isEditing) { |
| setDraftValue(String(currentValue)) |
| setDraftAllowMerge(false) |
| setIsEditing(true) |
| setErrorMessage(null) |
| } |
| } |
|
|
| const handleCancel = () => { |
| setIsEditing(false) |
| setErrorMessage(null) |
| } |
|
|
| const handleSave = async () => { |
| const value = draftValue.trim() |
| const allowMerge = draftAllowMerge |
|
|
| if (value === '') { |
| return |
| } |
|
|
| if (isSubmitting || value === String(currentValue)) { |
| setIsEditing(false) |
| setErrorMessage(null) |
| return |
| } |
|
|
| setIsSubmitting(true) |
| setErrorMessage(null) |
|
|
| try { |
| if (entityType === 'node' && entityId && nodeId) { |
| let updatedData = { [name]: value } |
|
|
| if (name === 'entity_id') { |
| if (!allowMerge) { |
| const exists = await checkEntityNameExists(value) |
| if (exists) { |
| const errorMsg = t('graphPanel.propertiesView.errors.duplicateName') |
| setErrorMessage(errorMsg) |
| toast.error(errorMsg) |
| return |
| } |
| } |
| updatedData = { 'entity_name': value } |
| } |
|
|
| const response = await updateEntity(entityId, updatedData, true, allowMerge) |
| const operationSummary = response.operation_summary |
| const operationStatus = operationSummary?.operation_status || 'complete_success' |
| const finalValue = operationSummary?.final_entity ?? value |
|
|
| |
| if (operationStatus === 'success') { |
| if (operationSummary?.merged) { |
| |
| setMergeDialogInfo({ |
| targetEntity: finalValue, |
| sourceEntity: entityId, |
| }) |
| setMergeDialogOpen(true) |
|
|
| |
| SearchHistoryManager.removeLabel(entityId) |
|
|
| |
|
|
| toast.success(t('graphPanel.propertiesView.success.entityMerged')) |
| } else { |
| |
| try { |
| const graphValue = name === 'entity_id' ? finalValue : value |
| await useGraphStore |
| .getState() |
| .updateNodeAndSelect(nodeId, entityId, name, graphValue) |
| } catch (error) { |
| console.error('Error updating node in graph:', error) |
| throw createErrorWithCause('Failed to update node in graph', error) |
| } |
|
|
| |
| if (name === 'entity_id') { |
| const currentLabel = useSettingsStore.getState().queryLabel |
|
|
| SearchHistoryManager.removeLabel(entityId) |
| SearchHistoryManager.addToHistory(finalValue) |
|
|
| |
| useSettingsStore.getState().triggerSearchLabelDropdownRefresh() |
|
|
| |
| if (currentLabel === entityId) { |
| useSettingsStore.getState().setQueryLabel(finalValue) |
| } |
| } |
|
|
| toast.success(t('graphPanel.propertiesView.success.entityUpdated')) |
| } |
|
|
| |
| |
| |
| const valueToSet = name === 'entity_id' ? finalValue : value |
| setCurrentValue(valueToSet) |
| onValueChange?.(valueToSet) |
|
|
| } else if (operationStatus === 'partial_success') { |
| |
| |
| const mergeError = operationSummary?.merge_error || 'Unknown error' |
|
|
| const errorMsg = t('graphPanel.propertiesView.errors.updateSuccessButMergeFailed', { |
| error: mergeError |
| }) |
| setErrorMessage(errorMsg) |
| toast.error(errorMsg) |
| |
| return |
|
|
| } else { |
| |
| |
| if (operationSummary?.merge_status === 'failed') { |
| |
| const mergeError = operationSummary?.merge_error || 'Unknown error' |
| const errorMsg = t('graphPanel.propertiesView.errors.mergeFailed', { |
| error: mergeError |
| }) |
| setErrorMessage(errorMsg) |
| toast.error(errorMsg) |
| } else { |
| |
| const errorMsg = t('graphPanel.propertiesView.errors.updateFailed') |
| setErrorMessage(errorMsg) |
| toast.error(errorMsg) |
| } |
| |
| return |
| } |
| } else if (entityType === 'edge' && sourceId && targetId && edgeId && dynamicId) { |
| const updatedData = { [name]: value } |
| await updateRelation(sourceId, targetId, updatedData) |
| try { |
| await useGraphStore.getState().updateEdgeAndSelect(edgeId, dynamicId, sourceId, targetId, name, value) |
| } catch (error) { |
| console.error(`Error updating edge ${sourceId}->${targetId} in graph:`, error) |
| throw createErrorWithCause('Failed to update edge in graph', error) |
| } |
| toast.success(t('graphPanel.propertiesView.success.relationUpdated')) |
| setCurrentValue(value) |
| onValueChange?.(value) |
| } |
|
|
| setIsEditing(false) |
| } catch (error) { |
| console.error('Error updating property:', error) |
| const errorMsg = error instanceof Error ? error.message : t('graphPanel.propertiesView.errors.updateFailed') |
| setErrorMessage(errorMsg) |
| toast.error(errorMsg) |
| return |
| } finally { |
| setIsSubmitting(false) |
| } |
| } |
|
|
| const handleMergeRefresh = (useMergedStart: boolean) => { |
| const info = mergeDialogInfo |
| const graphState = useGraphStore.getState() |
| const settingsState = useSettingsStore.getState() |
| const currentLabel = settingsState.queryLabel |
|
|
| |
| graphState.clearSelection() |
| graphState.setGraphDataFetchAttempted(false) |
| graphState.setLastSuccessfulQueryLabel('') |
|
|
| if (useMergedStart && info?.targetEntity) { |
| |
| settingsState.setQueryLabel(info.targetEntity) |
| } else { |
| |
| |
| settingsState.setQueryLabel('') |
| setTimeout(() => { |
| settingsState.setQueryLabel(currentLabel) |
| }, 50) |
| } |
|
|
| |
| graphState.incrementGraphDataVersion() |
|
|
| setMergeDialogOpen(false) |
| setMergeDialogInfo(null) |
| toast.info(t('graphPanel.propertiesView.mergeDialog.refreshing')) |
| } |
|
|
| return ( |
| <div className="flex items-center gap-1 overflow-hidden"> |
| <PropertyName name={name} /> |
| <EditIcon onClick={handleEditClick} />: |
| <PropertyValue |
| value={currentValue} |
| onClick={onClick} |
| tooltip={tooltip || (typeof currentValue === 'string' ? currentValue : JSON.stringify(currentValue, null, 2))} |
| /> |
| <PropertyEditDialog |
| isOpen={isEditing} |
| onClose={handleCancel} |
| onSave={handleSave} |
| propertyName={name} |
| value={draftValue} |
| allowMerge={draftAllowMerge} |
| onValueChange={setDraftValue} |
| onAllowMergeChange={setDraftAllowMerge} |
| isSubmitting={isSubmitting} |
| errorMessage={errorMessage} |
| /> |
| |
| <MergeDialog |
| mergeDialogOpen={mergeDialogOpen} |
| mergeDialogInfo={mergeDialogInfo} |
| onOpenChange={(open) => { |
| setMergeDialogOpen(open) |
| if (!open) { |
| setMergeDialogInfo(null) |
| } |
| }} |
| onRefresh={handleMergeRefresh} |
| /> |
| </div> |
| ) |
| } |
|
|
| export default EditablePropertyRow |
|
|