mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-18 02:21:51 +08:00
feat(core): edit icon in navigation panel (#13595)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Rename dialog now edits per-item explorer icons (emoji or custom) and can skip name-change callbacks. Doc icon picker added to the editor with localized "Add icon" placeholder and readonly rendering. Icon editor supports fallbacks, trigger variants, and improved input/test-id wiring. - **Style** - Updated icon picker and trigger sizing and placeholder visuals; title/icon layout adjustments. - **Chores** - Explorer icon storage and module added to persist and serve icons across the app. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
+4
@@ -229,6 +229,10 @@ export const NavigationPanelCollectionNode = ({
|
||||
operations={finalOperations}
|
||||
dropEffect={handleDropEffectOnCollection}
|
||||
data-testid={`navigation-panel-collection-${collectionId}`}
|
||||
explorerIconConfig={{
|
||||
where: 'collection',
|
||||
id: collectionId,
|
||||
}}
|
||||
>
|
||||
<NavigationPanelCollectionNodeChildren
|
||||
collection={collection}
|
||||
|
||||
+4
-4
@@ -128,7 +128,7 @@ export const useNavigationPanelCollectionNodeOperations = (
|
||||
),
|
||||
},
|
||||
{
|
||||
index: 99,
|
||||
index: 103,
|
||||
view: (
|
||||
<MenuItem prefixIcon={<FilterIcon />} onClick={handleShowEdit}>
|
||||
{t['com.affine.collection.menu.edit']()}
|
||||
@@ -136,7 +136,7 @@ export const useNavigationPanelCollectionNodeOperations = (
|
||||
),
|
||||
},
|
||||
{
|
||||
index: 99,
|
||||
index: 102,
|
||||
view: (
|
||||
<MenuItem
|
||||
prefixIcon={<PlusIcon />}
|
||||
@@ -147,7 +147,7 @@ export const useNavigationPanelCollectionNodeOperations = (
|
||||
),
|
||||
},
|
||||
{
|
||||
index: 99,
|
||||
index: 101,
|
||||
view: (
|
||||
<MenuItem
|
||||
prefixIcon={<IsFavoriteIcon favorite={favorite} />}
|
||||
@@ -160,7 +160,7 @@ export const useNavigationPanelCollectionNodeOperations = (
|
||||
),
|
||||
},
|
||||
{
|
||||
index: 99,
|
||||
index: 100,
|
||||
view: (
|
||||
<MenuItem prefixIcon={<OpenInNewIcon />} onClick={handleOpenInNewTab}>
|
||||
{t['com.affine.workbench.tab.page-menu-open']()}
|
||||
|
||||
@@ -314,6 +314,10 @@ export const NavigationPanelDocNode = ({
|
||||
operations={finalOperations}
|
||||
dropEffect={handleDropEffectOnDoc}
|
||||
data-testid={`navigation-panel-doc-${docId}`}
|
||||
explorerIconConfig={{
|
||||
where: 'doc',
|
||||
id: docId,
|
||||
}}
|
||||
>
|
||||
{appSettings.showLinkedDocInSidebar ? (
|
||||
<Guard docId={docId} permission="Doc_Read">
|
||||
|
||||
@@ -811,6 +811,7 @@ const NavigationPanelFolderNodeFolder = ({
|
||||
}
|
||||
dropEffect={handleDropEffect}
|
||||
data-testid={`navigation-panel-folder-${node.id}`}
|
||||
explorerIconConfig={node.id ? { where: 'folder', id: node.id } : null}
|
||||
>
|
||||
{children.map(child => (
|
||||
<NavigationPanelFolderNode
|
||||
|
||||
@@ -200,6 +200,10 @@ export const NavigationPanelTagNode = ({
|
||||
operations={finalOperations}
|
||||
dropEffect={handleDropEffectOnTag}
|
||||
data-testid={`navigation-panel-tag-${tagId}`}
|
||||
explorerIconConfig={{
|
||||
where: 'tag',
|
||||
id: tagId,
|
||||
}}
|
||||
>
|
||||
<NavigationPanelTagNodeDocs tag={tagRecord} path={path} />
|
||||
</NavigationPanelTreeNode>
|
||||
|
||||
+6
-6
@@ -1,4 +1,5 @@
|
||||
import { IconButton } from '@affine/component';
|
||||
import { RenameModal } from '@affine/component/rename-modal';
|
||||
import { NavigationPanelService } from '@affine/core/modules/navigation-panel';
|
||||
import { TagService } from '@affine/core/modules/tag';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
@@ -10,7 +11,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { CollapsibleSection } from '../../layouts/collapsible-section';
|
||||
import { NavigationPanelTagNode } from '../../nodes/tag';
|
||||
import { NavigationPanelTreeRoot } from '../../tree';
|
||||
import { NavigationPanelTreeNodeRenameModal as CreateTagModal } from '../../tree/node';
|
||||
import { RootEmpty } from './empty';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
@@ -62,11 +62,11 @@ export const NavigationPanelTags = () => {
|
||||
<AddTagIcon />
|
||||
</IconButton>
|
||||
{creating && (
|
||||
<CreateTagModal
|
||||
setRenaming={setCreating}
|
||||
handleRename={handleCreateNewTag}
|
||||
rawName={t['com.affine.rootAppSidebar.tags.new-tag']()}
|
||||
className={styles.createModalAnchor}
|
||||
<RenameModal
|
||||
open
|
||||
onOpenChange={setCreating}
|
||||
onRename={handleCreateNewTag}
|
||||
currentName={t['com.affine.rootAppSidebar.tags.new-tag']()}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -4,19 +4,21 @@ import {
|
||||
type DropTargetDropEvent,
|
||||
type DropTargetOptions,
|
||||
type DropTargetTreeInstruction,
|
||||
IconAndNameEditorMenu,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
useDraggable,
|
||||
useDropTarget,
|
||||
} from '@affine/component';
|
||||
import { RenameModal } from '@affine/component/rename-modal';
|
||||
import { Guard } from '@affine/core/components/guard';
|
||||
import { AppSidebarService } from '@affine/core/modules/app-sidebar';
|
||||
import type { ExplorerIconType } from '@affine/core/modules/db/schema/schema';
|
||||
import { ExplorerIconService } from '@affine/core/modules/explorer-icon/services/explorer-icon';
|
||||
import type { ExplorerType } from '@affine/core/modules/explorer-icon/store/explorer-icon';
|
||||
import type { DocPermissionActions } from '@affine/core/modules/permissions';
|
||||
import { WorkbenchLink } from '@affine/core/modules/workbench';
|
||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||
import { extractEmojiIcon } from '@affine/core/utils';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import {
|
||||
ArrowDownSmallIcon,
|
||||
@@ -83,6 +85,10 @@ export interface BaseNavigationPanelTreeNodeProps {
|
||||
[key: `data-${string}`]: any;
|
||||
}
|
||||
|
||||
type ExplorerIconConfig = {
|
||||
where: ExplorerType;
|
||||
id: string;
|
||||
};
|
||||
interface WebNavigationPanelTreeNodeProps
|
||||
extends BaseNavigationPanelTreeNodeProps {
|
||||
renameable?: boolean;
|
||||
@@ -90,6 +96,8 @@ interface WebNavigationPanelTreeNodeProps
|
||||
renameableGuard?: { docId: string; action: DocPermissionActions };
|
||||
defaultRenaming?: boolean;
|
||||
|
||||
explorerIconConfig?: ExplorerIconConfig | null;
|
||||
|
||||
canDrop?: DropTargetOptions<AffineDNDData>['canDrop'];
|
||||
reorderable?: boolean;
|
||||
dndData?: AffineDNDData;
|
||||
@@ -105,25 +113,65 @@ export const NavigationPanelTreeNodeRenameModal = ({
|
||||
setRenaming,
|
||||
handleRename,
|
||||
rawName,
|
||||
explorerIconConfig,
|
||||
className,
|
||||
fallbackIcon,
|
||||
}: {
|
||||
setRenaming: (renaming: boolean) => void;
|
||||
handleRename: (newName: string) => void;
|
||||
rawName: string | undefined;
|
||||
className?: string;
|
||||
explorerIconConfig?: ExplorerIconConfig | null;
|
||||
fallbackIcon?: React.ReactNode;
|
||||
}) => {
|
||||
const explorerIconService = useService(ExplorerIconService);
|
||||
const appSidebarService = useService(AppSidebarService).sidebar;
|
||||
const sidebarWidth = useLiveData(appSidebarService.width$);
|
||||
|
||||
const explorerIcon = useLiveData(
|
||||
useMemo(
|
||||
() =>
|
||||
explorerIconConfig
|
||||
? explorerIconService.icon$(
|
||||
explorerIconConfig.where,
|
||||
explorerIconConfig.id
|
||||
)
|
||||
: null,
|
||||
[explorerIconConfig, explorerIconService]
|
||||
)
|
||||
);
|
||||
|
||||
const onIconChange = useCallback(
|
||||
(type?: ExplorerIconType, icon?: string) => {
|
||||
if (!explorerIconConfig) return;
|
||||
explorerIconService.setIcon({
|
||||
where: explorerIconConfig.where,
|
||||
id: explorerIconConfig.id,
|
||||
type,
|
||||
icon,
|
||||
});
|
||||
},
|
||||
[explorerIconConfig, explorerIconService]
|
||||
);
|
||||
|
||||
return (
|
||||
<RenameModal
|
||||
<IconAndNameEditorMenu
|
||||
open
|
||||
width={sidebarWidth - 32}
|
||||
onOpenChange={setRenaming}
|
||||
onRename={handleRename}
|
||||
currentName={rawName ?? ''}
|
||||
onIconChange={onIconChange}
|
||||
onNameChange={handleRename}
|
||||
name={rawName ?? ''}
|
||||
iconType={explorerIcon?.type ?? 'emoji'}
|
||||
icon={explorerIcon?.icon ?? ''}
|
||||
width={sidebarWidth - 16}
|
||||
contentOptions={{
|
||||
sideOffset: 36,
|
||||
}}
|
||||
iconPlaceholder={fallbackIcon}
|
||||
inputTestId="rename-modal-input"
|
||||
>
|
||||
<div className={clsx(styles.itemRenameAnchor, className)} />
|
||||
</RenameModal>
|
||||
</IconAndNameEditorMenu>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -140,7 +188,6 @@ export const NavigationPanelTreeNode = ({
|
||||
onRename,
|
||||
disabled,
|
||||
collapsed,
|
||||
extractEmojiAsIcon,
|
||||
setCollapsed,
|
||||
collapsible = true,
|
||||
canDrop,
|
||||
@@ -151,10 +198,12 @@ export const NavigationPanelTreeNode = ({
|
||||
childrenPlaceholder,
|
||||
linkComponent: LinkComponent = WorkbenchLink,
|
||||
dndData,
|
||||
explorerIconConfig,
|
||||
onDrop,
|
||||
dropEffect,
|
||||
...otherProps
|
||||
}: WebNavigationPanelTreeNodeProps) => {
|
||||
const explorerIconService = useService(ExplorerIconService);
|
||||
const t = useI18n();
|
||||
const cid = useId();
|
||||
const context = useContext(NavigationPanelTreeContext);
|
||||
@@ -165,20 +214,19 @@ export const NavigationPanelTreeNode = ({
|
||||
const [renaming, setRenaming] = useState(defaultRenaming);
|
||||
const [lastInGroup, setLastInGroup] = useState(false);
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const explorerIcon = useLiveData(
|
||||
useMemo(
|
||||
() =>
|
||||
explorerIconConfig
|
||||
? explorerIconService.icon$(
|
||||
explorerIconConfig?.where,
|
||||
explorerIconConfig?.id
|
||||
)
|
||||
: null,
|
||||
[explorerIconConfig, explorerIconService]
|
||||
)
|
||||
);
|
||||
|
||||
const { emoji, name } = useMemo(() => {
|
||||
if (!extractEmojiAsIcon || !rawName) {
|
||||
return {
|
||||
emoji: null,
|
||||
name: rawName,
|
||||
};
|
||||
}
|
||||
const { emoji, rest } = extractEmojiIcon(rawName);
|
||||
return {
|
||||
emoji,
|
||||
name: rest,
|
||||
};
|
||||
}, [extractEmojiAsIcon, rawName]);
|
||||
const { dragRef, dragging, CustomDragPreview } = useDraggable<
|
||||
AffineDNDData & { draggable: { __cid: string } }
|
||||
>(
|
||||
@@ -384,6 +432,14 @@ export const NavigationPanelTreeNode = ({
|
||||
[clickForCollapse, collapsed, collapsible, onClick, setCollapsed]
|
||||
);
|
||||
|
||||
const fallbackIcon = Icon && (
|
||||
<Icon
|
||||
draggedOver={draggedOver && !isSelfDraggedOver}
|
||||
treeInstruction={treeInstruction}
|
||||
collapsed={collapsed}
|
||||
/>
|
||||
);
|
||||
|
||||
const content = (
|
||||
<div
|
||||
onClick={handleClick}
|
||||
@@ -405,19 +461,15 @@ export const NavigationPanelTreeNode = ({
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.iconContainer}>
|
||||
{emoji ??
|
||||
(Icon && (
|
||||
<Icon
|
||||
draggedOver={draggedOver && !isSelfDraggedOver}
|
||||
treeInstruction={treeInstruction}
|
||||
collapsed={collapsed}
|
||||
/>
|
||||
))}
|
||||
{/* Only emoji icon is supported for now */}
|
||||
{explorerIcon && explorerIcon.type === 'emoji'
|
||||
? explorerIcon.icon
|
||||
: fallbackIcon}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.itemMain}>
|
||||
<div className={styles.itemContent}>{name}</div>
|
||||
<div className={styles.itemContent}>{rawName}</div>
|
||||
{postfix}
|
||||
<div
|
||||
className={styles.postfix}
|
||||
@@ -452,6 +504,8 @@ export const NavigationPanelTreeNode = ({
|
||||
setRenaming={setRenaming}
|
||||
handleRename={handleRename}
|
||||
rawName={rawName}
|
||||
explorerIconConfig={explorerIconConfig}
|
||||
fallbackIcon={fallbackIcon}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user