File size: 11,226 Bytes
979853c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 | 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 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 [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
// 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 createErrorWithCause('Failed to update node in graph', error)
}
// 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 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
// 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 (
<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
|