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 7a95365be8
4 changed files with 51 additions and 28 deletions
@@ -28,10 +28,8 @@ export const ExplorerTagNode = ({
operations: additionalOperations, operations: additionalOperations,
dropEffect, dropEffect,
canDrop, canDrop,
defaultRenaming,
}: { }: {
tagId: string; tagId: string;
defaultRenaming?: boolean;
} & GenericExplorerNode) => { } & GenericExplorerNode) => {
const t = useI18n(); const t = useI18n();
const { tagService, globalContextService } = useServices({ const { tagService, globalContextService } = useServices({
@@ -179,7 +177,6 @@ export const ExplorerTagNode = ({
setCollapsed={setCollapsed} setCollapsed={setCollapsed}
to={`/tag/${tagId}`} to={`/tag/${tagId}`}
active={active} active={active}
defaultRenaming={defaultRenaming}
reorderable={reorderable} reorderable={reorderable}
onRename={handleRename} onRename={handleRename}
canDrop={handleCanDrop} canDrop={handleCanDrop}
@@ -1,6 +1,5 @@
import { IconButton } from '@affine/component'; import { IconButton } from '@affine/component';
import { ExplorerTreeRoot } from '@affine/core/modules/explorer/views/tree'; 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 { TagService } from '@affine/core/modules/tag';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { track } from '@affine/track'; import { track } from '@affine/track';
@@ -11,6 +10,7 @@ import { useCallback, useEffect, useState } from 'react';
import { ExplorerService } from '../../../services/explorer'; import { ExplorerService } from '../../../services/explorer';
import { CollapsibleSection } from '../../layouts/collapsible-section'; import { CollapsibleSection } from '../../layouts/collapsible-section';
import { ExplorerTagNode } from '../../nodes/tag'; import { ExplorerTagNode } from '../../nodes/tag';
import { ExplorerTreeNodeRenameModal as CreateTagModal } from '../../tree/node';
import { RootEmpty } from './empty'; import { RootEmpty } from './empty';
import * as styles from './styles.css'; import * as styles from './styles.css';
@@ -21,25 +21,28 @@ export const ExplorerTags = () => {
}); });
const explorerSection = explorerService.sections.tags; const explorerSection = explorerService.sections.tags;
const collapsed = useLiveData(explorerSection.collapsed$); const collapsed = useLiveData(explorerSection.collapsed$);
const [createdTag, setCreatedTag] = useState<Tag | null>(null); const [creating, setCreating] = useState(false);
const tags = useLiveData(tagService.tagList.tags$); const tags = useLiveData(tagService.tagList.tags$);
const t = useI18n(); const t = useI18n();
const handleCreateNewFavoriteDoc = useCallback(() => { const handleCreateNewTag = useCallback(
const newTags = tagService.tagList.createTag( (name: string) => {
t['com.affine.rootAppSidebar.tags.new-tag'](), tagService.tagList.createTag(name, tagService.randomTagColor());
tagService.randomTagColor() track.$.navigationPanel.organize.createOrganizeItem({ type: 'tag' });
); explorerSection.setCollapsed(false);
setCreatedTag(newTags); },
track.$.navigationPanel.organize.createOrganizeItem({ type: 'tag' }); [explorerSection, tagService]
explorerSection.setCollapsed(false); );
}, [explorerSection, t, tagService]);
useEffect(() => { useEffect(() => {
if (collapsed) setCreatedTag(null); // reset created tag to clear the renaming state if (collapsed) setCreating(false);
}, [collapsed]); }, [collapsed]);
const handleOpenCreateModal = useCallback(() => {
setCreating(true);
}, []);
return ( return (
<CollapsibleSection <CollapsibleSection
name="tags" name="tags"
@@ -47,16 +50,26 @@ export const ExplorerTags = () => {
headerClassName={styles.draggedOverHighlight} headerClassName={styles.draggedOverHighlight}
title={t['com.affine.rootAppSidebar.tags']()} title={t['com.affine.rootAppSidebar.tags']()}
actions={ actions={
<IconButton <div className={styles.iconContainer}>
data-testid="explorer-bar-add-favorite-button" <IconButton
onClick={handleCreateNewFavoriteDoc} data-testid="explorer-bar-add-tag-button"
size="16" onClick={handleOpenCreateModal}
tooltip={t[ size="16"
'com.affine.rootAppSidebar.explorer.tag-section-add-tooltip' tooltip={t[
]()} 'com.affine.rootAppSidebar.explorer.tag-section-add-tooltip'
> ]()}
<AddTagIcon /> >
</IconButton> <AddTagIcon />
</IconButton>
{creating && (
<CreateTagModal
setRenaming={setCreating}
handleRename={handleCreateNewTag}
rawName={t['com.affine.rootAppSidebar.tags.new-tag']()}
className={styles.createModalAnchor}
/>
)}
</div>
} }
> >
<ExplorerTreeRoot placeholder={<RootEmpty />}> <ExplorerTreeRoot placeholder={<RootEmpty />}>
@@ -68,7 +81,6 @@ export const ExplorerTags = () => {
location={{ location={{
at: 'explorer:tags:list', at: 'explorer:tags:list',
}} }}
defaultRenaming={createdTag?.id === tag.id}
/> />
))} ))}
</ExplorerTreeRoot> </ExplorerTreeRoot>
@@ -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)',
});
@@ -98,14 +98,16 @@ interface WebExplorerTreeNodeProps extends BaseExplorerTreeNodeProps {
* specific rename modal for explorer tree node, * specific rename modal for explorer tree node,
* Separate it into a separate component to prevent re-rendering the entire component when width changes. * Separate it into a separate component to prevent re-rendering the entire component when width changes.
*/ */
const ExplorerTreeNodeRenameModal = ({ export const ExplorerTreeNodeRenameModal = ({
setRenaming, setRenaming,
handleRename, handleRename,
rawName, rawName,
className,
}: { }: {
setRenaming: (renaming: boolean) => void; setRenaming: (renaming: boolean) => void;
handleRename: (newName: string) => void; handleRename: (newName: string) => void;
rawName: string | undefined; rawName: string | undefined;
className?: string;
}) => { }) => {
const appSidebarService = useService(AppSidebarService).sidebar; const appSidebarService = useService(AppSidebarService).sidebar;
const sidebarWidth = useLiveData(appSidebarService.width$); const sidebarWidth = useLiveData(appSidebarService.width$);
@@ -117,7 +119,7 @@ const ExplorerTreeNodeRenameModal = ({
onRename={handleRename} onRename={handleRename}
currentName={rawName ?? ''} currentName={rawName ?? ''}
> >
<div className={styles.itemRenameAnchor} /> <div className={clsx(styles.itemRenameAnchor, className)} />
</RenameModal> </RenameModal>
); );
}; };