mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 06:30:10 +08:00
feat(session-item): add in-place editing for session names
- Implement double-click to edit session names directly in the list - Add loading state during save operation - Update useInPlaceEdit hook to support async operations and saving state - Adjust styling to accommodate new edit input field
This commit is contained in:
parent
0e35224787
commit
6950b6f1e7
@ -1,7 +1,9 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { useTimer } from './useTimer'
|
||||
|
||||
export interface UseInPlaceEditOptions {
|
||||
onSave: (value: string) => void
|
||||
onSave: ((value: string) => void) | ((value: string) => Promise<void>)
|
||||
onCancel?: () => void
|
||||
autoSelectOnStart?: boolean
|
||||
trimOnSave?: boolean
|
||||
@ -9,6 +11,7 @@ export interface UseInPlaceEditOptions {
|
||||
|
||||
export interface UseInPlaceEditReturn {
|
||||
isEditing: boolean
|
||||
isSaving: boolean
|
||||
editValue: string
|
||||
inputRef: React.RefObject<HTMLInputElement | null>
|
||||
startEdit: (initialValue: string) => void
|
||||
@ -16,23 +19,27 @@ export interface UseInPlaceEditReturn {
|
||||
cancelEdit: () => void
|
||||
handleKeyDown: (e: React.KeyboardEvent) => void
|
||||
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
handleValueChange: (value: string) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* A React hook that provides in-place editing functionality for text inputs
|
||||
* @param options - Configuration options for the in-place edit behavior
|
||||
* @param options.onSave - Callback function called when edits are saved
|
||||
* @param options.onCancel - Optional callback function called when editing is cancelled
|
||||
* @param options.autoSelectOnStart - Whether to automatically select text when editing starts (default: true)
|
||||
* @param options.trimOnSave - Whether to trim whitespace when saving (default: true)
|
||||
* @returns An object containing the editing state and handler functions
|
||||
*/
|
||||
export function useInPlaceEdit(options: UseInPlaceEditOptions): UseInPlaceEditReturn {
|
||||
const { onSave, onCancel, autoSelectOnStart = true, trimOnSave = true } = options
|
||||
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [editValue, setEditValue] = useState('')
|
||||
const [originalValue, setOriginalValue] = useState('')
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const editTimerRef = useRef<NodeJS.Timeout>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearTimeout(editTimerRef.current)
|
||||
}
|
||||
}, [])
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
|
||||
const startEdit = useCallback(
|
||||
(initialValue: string) => {
|
||||
@ -40,26 +47,37 @@ export function useInPlaceEdit(options: UseInPlaceEditOptions): UseInPlaceEditRe
|
||||
setEditValue(initialValue)
|
||||
setOriginalValue(initialValue)
|
||||
|
||||
clearTimeout(editTimerRef.current)
|
||||
editTimerRef.current = setTimeout(() => {
|
||||
inputRef.current?.focus()
|
||||
if (autoSelectOnStart) {
|
||||
inputRef.current?.select()
|
||||
}
|
||||
}, 0)
|
||||
setTimeoutTimer(
|
||||
'startEdit',
|
||||
() => {
|
||||
inputRef.current?.focus()
|
||||
if (autoSelectOnStart) {
|
||||
inputRef.current?.select()
|
||||
}
|
||||
},
|
||||
0
|
||||
)
|
||||
},
|
||||
[autoSelectOnStart]
|
||||
[autoSelectOnStart, setTimeoutTimer]
|
||||
)
|
||||
|
||||
const saveEdit = useCallback(() => {
|
||||
const finalValue = trimOnSave ? editValue.trim() : editValue
|
||||
if (finalValue !== originalValue) {
|
||||
onSave(finalValue)
|
||||
const saveEdit = useCallback(async () => {
|
||||
if (isSaving) return
|
||||
|
||||
setIsSaving(true)
|
||||
|
||||
try {
|
||||
const finalValue = trimOnSave ? editValue.trim() : editValue
|
||||
if (finalValue !== originalValue) {
|
||||
await onSave(finalValue)
|
||||
}
|
||||
setIsEditing(false)
|
||||
setEditValue('')
|
||||
setOriginalValue('')
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
setIsEditing(false)
|
||||
setEditValue('')
|
||||
setOriginalValue('')
|
||||
}, [editValue, originalValue, onSave, trimOnSave])
|
||||
}, [isSaving, trimOnSave, editValue, originalValue, onSave])
|
||||
|
||||
const cancelEdit = useCallback(() => {
|
||||
setIsEditing(false)
|
||||
@ -86,6 +104,10 @@ export function useInPlaceEdit(options: UseInPlaceEditOptions): UseInPlaceEditRe
|
||||
setEditValue(e.target.value)
|
||||
}, [])
|
||||
|
||||
const handleValueChange = useCallback((value: string) => {
|
||||
setEditValue(value)
|
||||
}, [])
|
||||
|
||||
// Handle clicks outside the input to save
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
@ -105,12 +127,14 @@ export function useInPlaceEdit(options: UseInPlaceEditOptions): UseInPlaceEditRe
|
||||
|
||||
return {
|
||||
isEditing,
|
||||
isSaving,
|
||||
editValue,
|
||||
inputRef,
|
||||
startEdit,
|
||||
saveEdit,
|
||||
cancelEdit,
|
||||
handleKeyDown,
|
||||
handleInputChange
|
||||
handleInputChange,
|
||||
handleValueChange
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { Button, cn, useDisclosure } from '@heroui/react'
|
||||
import { Button, cn, Input, useDisclosure } from '@heroui/react'
|
||||
import { DeleteIcon, EditIcon } from '@renderer/components/Icons'
|
||||
import { SessionModal } from '@renderer/components/Popups/agent/SessionModal'
|
||||
import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
|
||||
import { useInPlaceEdit } from '@renderer/hooks/useInPlaceEdit'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { AgentSessionEntity } from '@renderer/types'
|
||||
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@renderer/ui/context-menu'
|
||||
@ -23,15 +25,24 @@ const SessionItem: FC<SessionItemProps> = ({ session, agentId, isDisabled, isLoa
|
||||
const { t } = useTranslation()
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const { chat } = useRuntime()
|
||||
const updateSession = useUpdateSession(agentId)
|
||||
const activeSessionId = chat.activeSessionId[agentId]
|
||||
|
||||
const { isEditing, isSaving, editValue, inputRef, startEdit, handleKeyDown, handleValueChange } = useInPlaceEdit({
|
||||
onSave: async (value) => {
|
||||
if (value !== session.name) {
|
||||
await updateSession({ id: session.id, name: value })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const isActive = activeSessionId === session.id
|
||||
|
||||
const SessionLabel = useCallback(() => {
|
||||
const displayName = session.name ?? session.id
|
||||
return (
|
||||
<>
|
||||
<span className="text-sm">{displayName}</span>
|
||||
<span className="px-2 text-sm">{displayName}</span>
|
||||
</>
|
||||
)
|
||||
}, [session.id, session.name])
|
||||
@ -44,9 +55,26 @@ const SessionItem: FC<SessionItemProps> = ({ session, agentId, isDisabled, isLoa
|
||||
isDisabled={isDisabled}
|
||||
isLoading={isLoading}
|
||||
onPress={onPress}
|
||||
className={isActive ? 'active' : ''}>
|
||||
<SessionLabelContainer className="name" title={session.name ?? session.id}>
|
||||
<SessionLabel />
|
||||
className={isActive ? 'active' : ''}
|
||||
onDoubleClick={() => startEdit(session.name ?? '')}>
|
||||
<SessionLabelContainer className="name h-full w-full" title={session.name ?? session.id}>
|
||||
{isEditing && (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
variant="bordered"
|
||||
value={editValue}
|
||||
onValueChange={handleValueChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
classNames={{
|
||||
base: 'h-full',
|
||||
mainWrapper: 'h-full',
|
||||
inputWrapper: 'h-full min-h-0 px-1.5',
|
||||
input: isSaving ? 'brightness-50' : undefined
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!isEditing && <SessionLabel />}
|
||||
</SessionLabelContainer>
|
||||
</ButtonContainer>
|
||||
</ContextMenuTrigger>
|
||||
@ -79,7 +107,7 @@ const ButtonContainer: React.FC<React.ComponentProps<typeof Button>> = ({ classN
|
||||
<Button
|
||||
{...props}
|
||||
className={cn(
|
||||
'relative mb-2 flex h-[37px] flex-row justify-between p-2.5',
|
||||
'relative mb-2 flex h-[37px] flex-row justify-between p-0',
|
||||
'rounded-[var(--list-item-border-radius)]',
|
||||
'border-[0.5px] border-transparent',
|
||||
'w-[calc(var(--assistants-width)_-_20px)]',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user