mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-07 05:39:05 +08:00
feat: disable mask closing for various popups across the application (#8832)
Some checks failed
Nightly Build / cleanup-artifacts (push) Has been cancelled
Nightly Build / check-repository (push) Has been cancelled
Nightly Build / nightly-build (macos-latest) (push) Has been cancelled
Nightly Build / nightly-build (ubuntu-latest) (push) Has been cancelled
Nightly Build / nightly-build (windows-latest) (push) Has been cancelled
Nightly Build / Build-Summary (push) Has been cancelled
Some checks failed
Nightly Build / cleanup-artifacts (push) Has been cancelled
Nightly Build / check-repository (push) Has been cancelled
Nightly Build / nightly-build (macos-latest) (push) Has been cancelled
Nightly Build / nightly-build (ubuntu-latest) (push) Has been cancelled
Nightly Build / nightly-build (windows-latest) (push) Has been cancelled
Nightly Build / Build-Summary (push) Has been cancelled
* feat: disable mask closing for various popups across the application - Updated multiple popup components to prevent closing when clicking outside, enhancing user experience and preventing accidental dismissals. - Affected components include ImportAgentPopup, QuickPhrasesButton, NewAppButton, EditMcpJsonPopup, TopicNamingModalPopup, CustomHeaderPopup, and QuickPhraseSettings. This change aims to improve the usability of modal dialogs by ensuring users must explicitly confirm or cancel their actions. * feat: implement click outside to save edits in TopicsTab - Added a useEffect hook to handle clicks outside the editing input, triggering save on blur. - Updated onClick behavior for topic items to prevent switching while editing. - Enhanced cursor style for better user experience during editing. This change improves the editing experience by ensuring that edits are saved when the user clicks outside the input field. * feat: integrate in-place editing for topic names in TopicsTab - Added useInPlaceEdit hook to manage topic name editing, improving user experience. - Removed previous editing logic and integrated new editing flow with save and cancel functionalities. - Updated UI interactions to reflect the new editing state, ensuring a smoother editing process. This change enhances the editing experience by allowing users to edit topic names directly within the list, streamlining the workflow.
This commit is contained in:
parent
4f0638ac4f
commit
aac4adea1a
106
src/renderer/src/hooks/useInPlaceEdit.ts
Normal file
106
src/renderer/src/hooks/useInPlaceEdit.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
export interface UseInPlaceEditOptions {
|
||||
onSave: (value: string) => void
|
||||
onCancel?: () => void
|
||||
autoSelectOnStart?: boolean
|
||||
trimOnSave?: boolean
|
||||
}
|
||||
|
||||
export interface UseInPlaceEditReturn {
|
||||
isEditing: boolean
|
||||
editValue: string
|
||||
inputRef: React.RefObject<HTMLInputElement | null>
|
||||
startEdit: (initialValue: string) => void
|
||||
saveEdit: () => void
|
||||
cancelEdit: () => void
|
||||
handleKeyDown: (e: React.KeyboardEvent) => void
|
||||
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
export function useInPlaceEdit(options: UseInPlaceEditOptions): UseInPlaceEditReturn {
|
||||
const { onSave, onCancel, autoSelectOnStart = true, trimOnSave = true } = options
|
||||
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [editValue, setEditValue] = useState('')
|
||||
const [originalValue, setOriginalValue] = useState('')
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const startEdit = useCallback(
|
||||
(initialValue: string) => {
|
||||
setIsEditing(true)
|
||||
setEditValue(initialValue)
|
||||
setOriginalValue(initialValue)
|
||||
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus()
|
||||
if (autoSelectOnStart) {
|
||||
inputRef.current?.select()
|
||||
}
|
||||
}, 0)
|
||||
},
|
||||
[autoSelectOnStart]
|
||||
)
|
||||
|
||||
const saveEdit = useCallback(() => {
|
||||
const finalValue = trimOnSave ? editValue.trim() : editValue
|
||||
if (finalValue !== originalValue) {
|
||||
onSave(finalValue)
|
||||
}
|
||||
setIsEditing(false)
|
||||
setEditValue('')
|
||||
setOriginalValue('')
|
||||
}, [editValue, originalValue, onSave, trimOnSave])
|
||||
|
||||
const cancelEdit = useCallback(() => {
|
||||
setIsEditing(false)
|
||||
setEditValue('')
|
||||
setOriginalValue('')
|
||||
onCancel?.()
|
||||
}, [onCancel])
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
saveEdit()
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
cancelEdit()
|
||||
}
|
||||
},
|
||||
[saveEdit, cancelEdit]
|
||||
)
|
||||
|
||||
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setEditValue(e.target.value)
|
||||
}, [])
|
||||
|
||||
// Handle clicks outside the input to save
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (isEditing && inputRef.current && !inputRef.current.contains(event.target as Node)) {
|
||||
saveEdit()
|
||||
}
|
||||
}
|
||||
|
||||
if (isEditing) {
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}
|
||||
return
|
||||
}, [isEditing, saveEdit])
|
||||
|
||||
return {
|
||||
isEditing,
|
||||
editValue,
|
||||
inputRef,
|
||||
startEdit,
|
||||
saveEdit,
|
||||
cancelEdit,
|
||||
handleKeyDown,
|
||||
handleInputChange
|
||||
}
|
||||
}
|
||||
@ -98,6 +98,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
title={t('agents.import.title')}
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
maskClosable={false}
|
||||
footer={
|
||||
<Flex justify="end" gap={8}>
|
||||
<Button onClick={onCancel}>{t('common.cancel')}</Button>
|
||||
|
||||
@ -158,6 +158,7 @@ const QuickPhrasesButton = ({ ref, setInputValue, resizeTextArea, ToolbarButton,
|
||||
title={t('settings.quickPhrase.add')}
|
||||
open={isModalOpen}
|
||||
onOk={handleModalOk}
|
||||
maskClosable={false}
|
||||
onCancel={() => {
|
||||
setIsModalOpen(false)
|
||||
setFormData({ title: '', content: '', location: 'global' })
|
||||
|
||||
@ -4,6 +4,7 @@ import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup
|
||||
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import { useInPlaceEdit } from '@renderer/hooks/useInPlaceEdit'
|
||||
import { modelGenerating } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { finishTopicRenaming, startTopicRenaming, TopicManager } from '@renderer/hooks/useTopic'
|
||||
@ -70,6 +71,22 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic,
|
||||
|
||||
const [deletingTopicId, setDeletingTopicId] = useState<string | null>(null)
|
||||
const deleteTimerRef = useRef<NodeJS.Timeout>(null)
|
||||
const [editingTopicId, setEditingTopicId] = useState<string | null>(null)
|
||||
|
||||
const topicEdit = useInPlaceEdit({
|
||||
onSave: (name: string) => {
|
||||
const topic = assistant.topics.find((t) => t.id === editingTopicId)
|
||||
if (topic && name !== topic.name) {
|
||||
const updatedTopic = { ...topic, name, isNameManuallyEdited: true }
|
||||
updateTopic(updatedTopic)
|
||||
window.message.success(t('common.saved'))
|
||||
}
|
||||
setEditingTopicId(null)
|
||||
},
|
||||
onCancel: () => {
|
||||
setEditingTopicId(null)
|
||||
}
|
||||
})
|
||||
|
||||
const isPending = useCallback((topicId: string) => topicLoadingQuery[topicId], [topicLoadingQuery])
|
||||
const isFulfilled = useCallback((topicId: string) => topicFulfilledQuery[topicId], [topicFulfilledQuery])
|
||||
@ -203,16 +220,9 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic,
|
||||
key: 'rename',
|
||||
icon: <EditIcon size={14} />,
|
||||
disabled: isRenaming(topic.id),
|
||||
async onClick() {
|
||||
const name = await PromptPopup.show({
|
||||
title: t('chat.topics.edit.title'),
|
||||
message: '',
|
||||
defaultValue: topic?.name || ''
|
||||
})
|
||||
if (name && topic?.name !== name) {
|
||||
const updatedTopic = { ...topic, name, isNameManuallyEdited: true }
|
||||
updateTopic(updatedTopic)
|
||||
}
|
||||
onClick() {
|
||||
setEditingTopicId(topic.id)
|
||||
topicEdit.startEdit(topic.name)
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -415,6 +425,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic,
|
||||
assistants,
|
||||
assistant,
|
||||
updateTopic,
|
||||
topicEdit,
|
||||
activeTopic.id,
|
||||
setActiveTopic,
|
||||
onPinTopic,
|
||||
@ -468,14 +479,27 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic,
|
||||
<TopicListItem
|
||||
onContextMenu={() => setTargetTopic(topic)}
|
||||
className={classNames(isActive ? 'active' : '', singlealone ? 'singlealone' : '')}
|
||||
onClick={() => onSwitchTopic(topic)}
|
||||
style={{ borderRadius }}>
|
||||
onClick={editingTopicId === topic.id && topicEdit.isEditing ? undefined : () => onSwitchTopic(topic)}
|
||||
style={{
|
||||
borderRadius,
|
||||
cursor: editingTopicId === topic.id && topicEdit.isEditing ? 'default' : 'pointer'
|
||||
}}>
|
||||
{isPending(topic.id) && !isActive && <PendingIndicator />}
|
||||
{isFulfilled(topic.id) && !isActive && <FulfilledIndicator />}
|
||||
<TopicNameContainer>
|
||||
<TopicName className={getTopicNameClassName()} title={topicName}>
|
||||
{topicName}
|
||||
</TopicName>
|
||||
{editingTopicId === topic.id && topicEdit.isEditing ? (
|
||||
<TopicEditInput
|
||||
ref={topicEdit.inputRef}
|
||||
value={topicEdit.editValue}
|
||||
onChange={topicEdit.handleInputChange}
|
||||
onKeyDown={topicEdit.handleKeyDown}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
) : (
|
||||
<TopicName className={getTopicNameClassName()} title={topicName}>
|
||||
{topicName}
|
||||
</TopicName>
|
||||
)}
|
||||
{!topic.pinned && (
|
||||
<Tooltip
|
||||
placement="bottom"
|
||||
@ -626,6 +650,23 @@ const TopicName = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const TopicEditInput = styled.input`
|
||||
background: var(--color-background);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
color: var(--color-text-1);
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
padding: 2px 6px;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
|
||||
&:focus {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px var(--color-primary-alpha);
|
||||
}
|
||||
`
|
||||
|
||||
const PendingIndicator = styled.div.attrs({
|
||||
className: 'animation-pulse'
|
||||
})`
|
||||
|
||||
@ -104,6 +104,7 @@ const NewAppButton: FC<Props> = ({ size = 60 }) => {
|
||||
setIsModalVisible(false)
|
||||
setFileList([])
|
||||
}}
|
||||
maskClosable={false}
|
||||
footer={null}
|
||||
transitionName="animation-move-down"
|
||||
centered>
|
||||
|
||||
@ -94,6 +94,7 @@ const AssistantSettingPopupContainer: React.FC<Props> = ({ resolve, tab, ...prop
|
||||
onOk={onOk}
|
||||
onCancel={onCancel}
|
||||
afterClose={afterClose}
|
||||
maskClosable={false}
|
||||
footer={null}
|
||||
title={assistant.name}
|
||||
transitionName="animation-move-down"
|
||||
|
||||
@ -115,6 +115,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
onOk={onOk}
|
||||
onCancel={onCancel}
|
||||
afterClose={onClose}
|
||||
maskClosable={false}
|
||||
width={800}
|
||||
height="80vh"
|
||||
loading={jsonSaving}
|
||||
|
||||
@ -46,6 +46,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
onOk={onOk}
|
||||
onCancel={onCancel}
|
||||
afterClose={onClose}
|
||||
maskClosable={false}
|
||||
transitionName="animation-move-down"
|
||||
footer={null}
|
||||
centered>
|
||||
|
||||
@ -68,6 +68,7 @@ const PopupContainer: React.FC<Props> = ({ provider, resolve }) => {
|
||||
onOk={onOk}
|
||||
onCancel={onCancel}
|
||||
afterClose={onClose}
|
||||
maskClosable={false}
|
||||
transitionName="animation-move-down"
|
||||
centered>
|
||||
<Space.Compact direction="vertical" style={{ width: '100%', marginTop: 5 }}>
|
||||
|
||||
@ -133,7 +133,8 @@ const QuickPhraseSettings: FC = () => {
|
||||
onCancel={() => setIsModalOpen(false)}
|
||||
width={520}
|
||||
transitionName="animation-move-down"
|
||||
centered>
|
||||
centered
|
||||
maskClosable={false}>
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
||||
<div>
|
||||
<Label>{t('settings.quickPhrase.titleLabel')}</Label>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user