fix(core): confirm the tag name before creating a new tag (#11724)

close AF-1569

- Show rename modal below the "Add Tag" button instead of at the new tag node
- Tag is created only after the user confirms the name in the modal
- Improves sidebar tag creation flow and user experience
![CleanShot 2025-04-16 at 11 18 28@2x](https://github.com/user-attachments/assets/8b6ccd52-deef-45d7-b523-5f7b1c2cab96)
This commit is contained in:
JimmFly
2025-04-18 07:53:32 +00:00
parent 3264e65980
commit 9f59d5e941
4 changed files with 51 additions and 28 deletions

View File

@@ -28,10 +28,8 @@ export const ExplorerTagNode = ({
operations: additionalOperations,
dropEffect,
canDrop,
defaultRenaming,
}: {
tagId: string;
defaultRenaming?: boolean;
} & GenericExplorerNode) => {
const t = useI18n();
const { tagService, globalContextService } = useServices({
@@ -179,7 +177,6 @@ export const ExplorerTagNode = ({
setCollapsed={setCollapsed}
to={`/tag/${tagId}`}
active={active}
defaultRenaming={defaultRenaming}
reorderable={reorderable}
onRename={handleRename}
canDrop={handleCanDrop}

View File

@@ -1,6 +1,5 @@
import { IconButton } from '@affine/component';
import { ExplorerTreeRoot } from '@affine/core/modules/explorer/views/tree';
import type { Tag } from '@affine/core/modules/tag';
import { TagService } from '@affine/core/modules/tag';
import { useI18n } from '@affine/i18n';
import { track } from '@affine/track';
@@ -11,6 +10,7 @@ import { useCallback, useEffect, useState } from 'react';
import { ExplorerService } from '../../../services/explorer';
import { CollapsibleSection } from '../../layouts/collapsible-section';
import { ExplorerTagNode } from '../../nodes/tag';
import { ExplorerTreeNodeRenameModal as CreateTagModal } from '../../tree/node';
import { RootEmpty } from './empty';
import * as styles from './styles.css';
@@ -21,25 +21,28 @@ export const ExplorerTags = () => {
});
const explorerSection = explorerService.sections.tags;
const collapsed = useLiveData(explorerSection.collapsed$);
const [createdTag, setCreatedTag] = useState<Tag | null>(null);
const [creating, setCreating] = useState(false);
const tags = useLiveData(tagService.tagList.tags$);
const t = useI18n();
const handleCreateNewFavoriteDoc = useCallback(() => {
const newTags = tagService.tagList.createTag(
t['com.affine.rootAppSidebar.tags.new-tag'](),
tagService.randomTagColor()
);
setCreatedTag(newTags);
track.$.navigationPanel.organize.createOrganizeItem({ type: 'tag' });
explorerSection.setCollapsed(false);
}, [explorerSection, t, tagService]);
const handleCreateNewTag = useCallback(
(name: string) => {
tagService.tagList.createTag(name, tagService.randomTagColor());
track.$.navigationPanel.organize.createOrganizeItem({ type: 'tag' });
explorerSection.setCollapsed(false);
},
[explorerSection, tagService]
);
useEffect(() => {
if (collapsed) setCreatedTag(null); // reset created tag to clear the renaming state
if (collapsed) setCreating(false);
}, [collapsed]);
const handleOpenCreateModal = useCallback(() => {
setCreating(true);
}, []);
return (
<CollapsibleSection
name="tags"
@@ -47,16 +50,26 @@ export const ExplorerTags = () => {
headerClassName={styles.draggedOverHighlight}
title={t['com.affine.rootAppSidebar.tags']()}
actions={
<IconButton
data-testid="explorer-bar-add-favorite-button"
onClick={handleCreateNewFavoriteDoc}
size="16"
tooltip={t[
'com.affine.rootAppSidebar.explorer.tag-section-add-tooltip'
]()}
>
<AddTagIcon />
</IconButton>
<div className={styles.iconContainer}>
<IconButton
data-testid="explorer-bar-add-tag-button"
onClick={handleOpenCreateModal}
size="16"
tooltip={t[
'com.affine.rootAppSidebar.explorer.tag-section-add-tooltip'
]()}
>
<AddTagIcon />
</IconButton>
{creating && (
<CreateTagModal
setRenaming={setCreating}
handleRename={handleCreateNewTag}
rawName={t['com.affine.rootAppSidebar.tags.new-tag']()}
className={styles.createModalAnchor}
/>
)}
</div>
}
>
<ExplorerTreeRoot placeholder={<RootEmpty />}>
@@ -68,7 +81,6 @@ export const ExplorerTags = () => {
location={{
at: 'explorer:tags:list',
}}
defaultRenaming={createdTag?.id === tag.id}
/>
))}
</ExplorerTreeRoot>

View File

@@ -9,3 +9,15 @@ export const draggedOverHighlight = style({
},
},
});
export const iconContainer = style({
display: 'flex',
position: 'relative',
});
export const createModalAnchor = style({
top: 20,
left: 'auto',
right: 0,
transform: 'translateX(6px)',
});

View File

@@ -98,14 +98,16 @@ interface WebExplorerTreeNodeProps extends BaseExplorerTreeNodeProps {
* specific rename modal for explorer tree node,
* Separate it into a separate component to prevent re-rendering the entire component when width changes.
*/
const ExplorerTreeNodeRenameModal = ({
export const ExplorerTreeNodeRenameModal = ({
setRenaming,
handleRename,
rawName,
className,
}: {
setRenaming: (renaming: boolean) => void;
handleRename: (newName: string) => void;
rawName: string | undefined;
className?: string;
}) => {
const appSidebarService = useService(AppSidebarService).sidebar;
const sidebarWidth = useLiveData(appSidebarService.width$);
@@ -117,7 +119,7 @@ const ExplorerTreeNodeRenameModal = ({
onRename={handleRename}
currentName={rawName ?? ''}
>
<div className={styles.itemRenameAnchor} />
<div className={clsx(styles.itemRenameAnchor, className)} />
</RenameModal>
);
};