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

* 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:
SuYao 2025-08-05 10:55:28 +08:00 committed by GitHub
parent 4f0638ac4f
commit aac4adea1a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 171 additions and 16 deletions

View 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
}
}

View File

@ -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>

View File

@ -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' })

View File

@ -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'
})`

View File

@ -104,6 +104,7 @@ const NewAppButton: FC<Props> = ({ size = 60 }) => {
setIsModalVisible(false)
setFileList([])
}}
maskClosable={false}
footer={null}
transitionName="animation-move-down"
centered>

View File

@ -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"

View File

@ -115,6 +115,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
onOk={onOk}
onCancel={onCancel}
afterClose={onClose}
maskClosable={false}
width={800}
height="80vh"
loading={jsonSaving}

View File

@ -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>

View File

@ -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 }}>

View File

@ -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>