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' /** * Interface for the EditablePropertyRow component props */ interface EditablePropertyRowProps { name: string // Property name to display and edit value: any // Initial value of the property onClick?: () => void // Optional click handler for the property value nodeId?: string // ID of the node (for node type) entityId?: string // ID of the entity (for node type) edgeId?: string // ID of the edge (for edge type) dynamicId?: string entityType?: 'node' | 'edge' // Type of graph entity sourceId?: string // Source node ID (for edge type) targetId?: string // Target node ID (for edge type) onValueChange?: (newValue: any) => void // Optional callback when value changes isEditable?: boolean // Whether this property can be edited tooltip?: string // Optional tooltip to display on hover } /** * EditablePropertyRow component that supports editing property values * This component is used in the graph properties panel to display and edit entity properties */ 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 [errorMessage, setErrorMessage] = useState(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) { setIsEditing(true) setErrorMessage(null) } } const handleCancel = () => { setIsEditing(false) setErrorMessage(null) } const handleSave = async (value: string, options?: { allowMerge?: boolean }) => { if (isSubmitting || value === String(currentValue)) { setIsEditing(false) setErrorMessage(null) return } setIsSubmitting(true) setErrorMessage(null) try { if (entityType === 'node' && entityId && nodeId) { let updatedData = { [name]: value } const allowMerge = options?.allowMerge ?? false 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 // Handle different operation statuses if (operationStatus === 'success') { if (operationSummary?.merged) { // Node was successfully merged into an existing entity setMergeDialogInfo({ targetEntity: finalValue, 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 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 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')) } // Update local state and notify parent component // For entity_id updates, use finalValue (which may be different due to merging) // For other properties, use the original value the user entered const valueToSet = name === 'entity_id' ? finalValue : value setCurrentValue(valueToSet) onValueChange?.(valueToSet) } else if (operationStatus === 'partial_success') { // Partial success: update succeeded but merge failed // Do NOT update graph data to keep frontend in sync with backend const mergeError = operationSummary?.merge_error || 'Unknown error' const errorMsg = t('graphPanel.propertiesView.errors.updateSuccessButMergeFailed', { error: mergeError }) setErrorMessage(errorMsg) toast.error(errorMsg) // Do not update currentValue or call onValueChange return } else { // Complete failure or unknown status // Check if this was a merge attempt or just a regular update if (operationSummary?.merge_status === 'failed') { // Merge operation was attempted but failed const mergeError = operationSummary?.merge_error || 'Unknown error' const errorMsg = t('graphPanel.propertiesView.errors.mergeFailed', { error: mergeError }) setErrorMessage(errorMsg) toast.error(errorMsg) } else { // Regular update failed (no merge involved) const errorMsg = t('graphPanel.propertiesView.errors.updateFailed') setErrorMessage(errorMsg) toast.error(errorMsg) } // Do not update currentValue or call onValueChange 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 new Error('Failed to update edge in graph') } 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 // 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 { // 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')) } return (
: { setMergeDialogOpen(open) if (!open) { setMergeDialogInfo(null) } }} onRefresh={handleMergeRefresh} />
) } export default EditablePropertyRow