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