mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-23 04:04:27 +08:00
feat(core): replace emoji-mart with affine icon picker (#13644)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - New Features - Unified icon picker with consistent rendering across the app. - Picker can auto-close after selection. - “Remove” now clears the icon selection. - Refactor - Icon handling consolidated across editors, navigation, and document titles for consistent behavior. - Picker now opens on the Emoji panel by default. - Style - Adjusted line-height and selectors for icon picker visuals. - Chores - Removed unused emoji-mart dependencies. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -28,8 +28,6 @@
|
|||||||
"@atlaskit/pragmatic-drag-and-drop": "^1.4.0",
|
"@atlaskit/pragmatic-drag-and-drop": "^1.4.0",
|
||||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
|
||||||
"@blocksuite/icons": "^2.2.17",
|
"@blocksuite/icons": "^2.2.17",
|
||||||
"@emoji-mart/data": "^1.2.1",
|
|
||||||
"@emoji-mart/react": "^1.1.1",
|
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
"@radix-ui/react-avatar": "^1.1.2",
|
"@radix-ui/react-avatar": "^1.1.2",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export const contentRoot = style({
|
|||||||
|
|
||||||
export const iconPicker = style({
|
export const iconPicker = style({
|
||||||
padding: 0,
|
padding: 0,
|
||||||
|
lineHeight: 1,
|
||||||
});
|
});
|
||||||
globalStyle(`${iconPicker} span:has(svg)`, {
|
globalStyle(`${iconPicker} span:has(svg)`, {
|
||||||
lineHeight: 0,
|
lineHeight: 0,
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import type { Meta, StoryFn } from '@storybook/react';
|
|||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { Button } from '../button';
|
import { Button } from '../button';
|
||||||
|
import { type IconData, IconType } from '../icon-picker';
|
||||||
import { ResizePanel } from '../resize-panel/resize-panel';
|
import { ResizePanel } from '../resize-panel/resize-panel';
|
||||||
import {
|
import {
|
||||||
IconAndNameEditorMenu,
|
IconAndNameEditorMenu,
|
||||||
type IconAndNameEditorMenuProps,
|
type IconAndNameEditorMenuProps,
|
||||||
IconEditor,
|
IconEditor,
|
||||||
type IconType,
|
|
||||||
} from './icon-name-editor';
|
} from './icon-name-editor';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -16,10 +16,13 @@ export default {
|
|||||||
} satisfies Meta<typeof IconAndNameEditorMenu>;
|
} satisfies Meta<typeof IconAndNameEditorMenu>;
|
||||||
|
|
||||||
export const Basic: StoryFn<IconAndNameEditorMenuProps> = () => {
|
export const Basic: StoryFn<IconAndNameEditorMenuProps> = () => {
|
||||||
const [icon, setIcon] = useState<string | undefined>('👋');
|
const [icon, setIcon] = useState<IconData | undefined>({
|
||||||
|
type: IconType.Emoji,
|
||||||
|
unicode: '👋',
|
||||||
|
});
|
||||||
const [name, setName] = useState<string>('Hello');
|
const [name, setName] = useState<string>('Hello');
|
||||||
|
|
||||||
const handleIconChange = useCallback((_?: IconType, icon?: string) => {
|
const handleIconChange = useCallback((icon?: IconData) => {
|
||||||
setIcon(icon);
|
setIcon(icon);
|
||||||
}, []);
|
}, []);
|
||||||
const handleNameChange = useCallback((name: string) => {
|
const handleNameChange = useCallback((name: string) => {
|
||||||
@@ -28,7 +31,7 @@ export const Basic: StoryFn<IconAndNameEditorMenuProps> = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p>Icon: {icon}</p>
|
<p>Icon: {JSON.stringify(icon)}</p>
|
||||||
<p>Name: {name}</p>
|
<p>Name: {name}</p>
|
||||||
|
|
||||||
<ResizePanel
|
<ResizePanel
|
||||||
@@ -44,17 +47,16 @@ export const Basic: StoryFn<IconAndNameEditorMenuProps> = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconAndNameEditorMenu
|
<IconAndNameEditorMenu
|
||||||
iconType="emoji"
|
|
||||||
icon={icon}
|
icon={icon}
|
||||||
name={name}
|
name={name}
|
||||||
onIconChange={handleIconChange}
|
onIconChange={handleIconChange}
|
||||||
onNameChange={handleNameChange}
|
onNameChange={handleNameChange}
|
||||||
|
closeAfterSelect
|
||||||
>
|
>
|
||||||
<Button>Edit Name and Icon</Button>
|
<Button>Edit Name and Icon</Button>
|
||||||
</IconAndNameEditorMenu>
|
</IconAndNameEditorMenu>
|
||||||
|
|
||||||
<IconEditor
|
<IconEditor
|
||||||
iconType="emoji"
|
|
||||||
icon={icon}
|
icon={icon}
|
||||||
onIconChange={handleIconChange}
|
onIconChange={handleIconChange}
|
||||||
closeAfterSelect
|
closeAfterSelect
|
||||||
|
|||||||
@@ -1,22 +1,18 @@
|
|||||||
import data from '@emoji-mart/data';
|
|
||||||
import Picker from '@emoji-mart/react';
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useTheme } from 'next-themes';
|
|
||||||
import { type ReactNode, useCallback, useState } from 'react';
|
import { type ReactNode, useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { Button, type ButtonProps } from '../button';
|
import { Button, type ButtonProps } from '../button';
|
||||||
|
import { type IconData, IconPicker } from '../icon-picker';
|
||||||
|
import { IconRenderer } from '../icon-picker/renderer';
|
||||||
import Input from '../input';
|
import Input from '../input';
|
||||||
import { Menu, type MenuProps } from '../menu';
|
import { Menu, type MenuProps } from '../menu';
|
||||||
import * as styles from './icon-name-editor.css';
|
import * as styles from './icon-name-editor.css';
|
||||||
|
|
||||||
export type IconType = 'emoji' | 'affine-icon' | 'blob';
|
|
||||||
|
|
||||||
export interface IconEditorProps {
|
export interface IconEditorProps {
|
||||||
iconType?: IconType;
|
icon?: IconData;
|
||||||
icon?: string;
|
|
||||||
closeAfterSelect?: boolean;
|
closeAfterSelect?: boolean;
|
||||||
iconPlaceholder?: ReactNode;
|
iconPlaceholder?: ReactNode;
|
||||||
onIconChange?: (type?: IconType, icon?: string) => void;
|
onIconChange?: (data?: IconData) => void;
|
||||||
triggerClassName?: string;
|
triggerClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,25 +34,7 @@ export interface IconAndNameEditorMenuProps
|
|||||||
skipIfNotChanged?: boolean;
|
skipIfNotChanged?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IconRenderer = ({
|
|
||||||
iconType,
|
|
||||||
icon,
|
|
||||||
fallback,
|
|
||||||
}: {
|
|
||||||
iconType: IconType;
|
|
||||||
icon: string;
|
|
||||||
fallback?: ReactNode;
|
|
||||||
}) => {
|
|
||||||
switch (iconType) {
|
|
||||||
case 'emoji':
|
|
||||||
return <div>{icon ?? fallback}</div>;
|
|
||||||
default:
|
|
||||||
return <div>{fallback}</div>;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const IconEditor = ({
|
export const IconEditor = ({
|
||||||
iconType,
|
|
||||||
icon,
|
icon,
|
||||||
closeAfterSelect,
|
closeAfterSelect,
|
||||||
iconPlaceholder,
|
iconPlaceholder,
|
||||||
@@ -71,16 +49,17 @@ export const IconEditor = ({
|
|||||||
triggerVariant?: ButtonProps['variant'];
|
triggerVariant?: ButtonProps['variant'];
|
||||||
}) => {
|
}) => {
|
||||||
const [isPickerOpen, setIsPickerOpen] = useState(false);
|
const [isPickerOpen, setIsPickerOpen] = useState(false);
|
||||||
const { resolvedTheme } = useTheme();
|
|
||||||
const handleEmojiClick = useCallback(
|
const handleSelect = useCallback(
|
||||||
(emoji: any) => {
|
(data?: IconData) => {
|
||||||
onIconChange?.('emoji', emoji.native);
|
onIconChange?.(data);
|
||||||
if (closeAfterSelect) {
|
if (closeAfterSelect) {
|
||||||
setIsPickerOpen(false);
|
setIsPickerOpen(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[closeAfterSelect, onIconChange]
|
[closeAfterSelect, onIconChange]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
rootOptions={{
|
rootOptions={{
|
||||||
@@ -97,30 +76,18 @@ export const IconEditor = ({
|
|||||||
}}
|
}}
|
||||||
items={
|
items={
|
||||||
<div onWheel={e => e.stopPropagation()}>
|
<div onWheel={e => e.stopPropagation()}>
|
||||||
<Picker
|
<IconPicker onSelect={handleSelect} />
|
||||||
data={data}
|
|
||||||
theme={resolvedTheme}
|
|
||||||
onEmojiSelect={handleEmojiClick}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant={triggerVariant}
|
variant={triggerVariant}
|
||||||
className={clsx(styles.iconPicker, triggerClassName)}
|
className={clsx(styles.iconPicker, triggerClassName)}
|
||||||
data-icon-type={iconType}
|
data-icon-type={icon?.type}
|
||||||
aria-label={icon ? 'Change Icon' : 'Select Icon'}
|
aria-label={icon ? 'Change Icon' : 'Select Icon'}
|
||||||
title={icon ? 'Change Icon' : 'Select Icon'}
|
title={icon ? 'Change Icon' : 'Select Icon'}
|
||||||
>
|
>
|
||||||
{icon && iconType ? (
|
<IconRenderer data={icon} fallback={iconPlaceholder} />
|
||||||
<IconRenderer
|
|
||||||
iconType={iconType}
|
|
||||||
icon={icon}
|
|
||||||
fallback={iconPlaceholder}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
iconPlaceholder
|
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
@@ -160,7 +127,6 @@ export const IconAndNameEditorMenu = ({
|
|||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
width = 300,
|
width = 300,
|
||||||
iconType: initialIconType,
|
|
||||||
icon: initialIcon,
|
icon: initialIcon,
|
||||||
name: initialName,
|
name: initialName,
|
||||||
onIconChange,
|
onIconChange,
|
||||||
@@ -169,15 +135,15 @@ export const IconAndNameEditorMenu = ({
|
|||||||
iconPlaceholder,
|
iconPlaceholder,
|
||||||
skipIfNotChanged = true,
|
skipIfNotChanged = true,
|
||||||
inputTestId,
|
inputTestId,
|
||||||
|
closeAfterSelect,
|
||||||
...menuProps
|
...menuProps
|
||||||
}: IconAndNameEditorMenuProps) => {
|
}: IconAndNameEditorMenuProps) => {
|
||||||
const [iconType, setIconType] = useState(initialIconType);
|
|
||||||
const [icon, setIcon] = useState(initialIcon);
|
const [icon, setIcon] = useState(initialIcon);
|
||||||
const [name, setName] = useState(initialName);
|
const [name, setName] = useState(initialName);
|
||||||
|
|
||||||
const commit = useCallback(() => {
|
const commit = useCallback(() => {
|
||||||
if (iconType !== initialIconType || icon !== initialIcon) {
|
if (icon !== initialIcon) {
|
||||||
onIconChange?.(iconType, icon);
|
onIconChange?.(icon);
|
||||||
}
|
}
|
||||||
if (skipIfNotChanged) {
|
if (skipIfNotChanged) {
|
||||||
if (name !== initialName) onNameChange?.(name);
|
if (name !== initialName) onNameChange?.(name);
|
||||||
@@ -186,9 +152,7 @@ export const IconAndNameEditorMenu = ({
|
|||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
icon,
|
icon,
|
||||||
iconType,
|
|
||||||
initialIcon,
|
initialIcon,
|
||||||
initialIconType,
|
|
||||||
initialName,
|
initialName,
|
||||||
name,
|
name,
|
||||||
onIconChange,
|
onIconChange,
|
||||||
@@ -196,13 +160,11 @@ export const IconAndNameEditorMenu = ({
|
|||||||
skipIfNotChanged,
|
skipIfNotChanged,
|
||||||
]);
|
]);
|
||||||
const abort = useCallback(() => {
|
const abort = useCallback(() => {
|
||||||
setIconType(initialIconType);
|
|
||||||
setIcon(initialIcon);
|
setIcon(initialIcon);
|
||||||
setName(initialName);
|
setName(initialName);
|
||||||
}, [initialIcon, initialIconType, initialName]);
|
}, [initialIcon, initialName]);
|
||||||
const handleIconChange = useCallback((type?: IconType, icon?: string) => {
|
const handleIconChange = useCallback((data?: IconData) => {
|
||||||
setIconType(type);
|
setIcon(data);
|
||||||
setIcon(icon);
|
|
||||||
}, []);
|
}, []);
|
||||||
const handleNameChange = useCallback((name: string) => {
|
const handleNameChange = useCallback((name: string) => {
|
||||||
setName(name);
|
setName(name);
|
||||||
@@ -210,13 +172,12 @@ export const IconAndNameEditorMenu = ({
|
|||||||
const handleMenuOpenChange = useCallback(
|
const handleMenuOpenChange = useCallback(
|
||||||
(open: boolean) => {
|
(open: boolean) => {
|
||||||
if (open) {
|
if (open) {
|
||||||
setIconType(initialIconType);
|
|
||||||
setIcon(initialIcon);
|
setIcon(initialIcon);
|
||||||
setName(initialName);
|
setName(initialName);
|
||||||
}
|
}
|
||||||
onOpenChange?.(open);
|
onOpenChange?.(open);
|
||||||
},
|
},
|
||||||
[initialIcon, initialIconType, initialName, onOpenChange]
|
[initialIcon, initialName, onOpenChange]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -243,10 +204,10 @@ export const IconAndNameEditorMenu = ({
|
|||||||
{...menuProps}
|
{...menuProps}
|
||||||
items={
|
items={
|
||||||
<IconAndNameEditorContent
|
<IconAndNameEditorContent
|
||||||
iconType={iconType}
|
|
||||||
icon={icon}
|
icon={icon}
|
||||||
name={name}
|
name={name}
|
||||||
iconPlaceholder={iconPlaceholder}
|
iconPlaceholder={iconPlaceholder}
|
||||||
|
closeAfterSelect={closeAfterSelect}
|
||||||
onIconChange={handleIconChange}
|
onIconChange={handleIconChange}
|
||||||
onNameChange={handleNameChange}
|
onNameChange={handleNameChange}
|
||||||
inputTestId={inputTestId}
|
inputTestId={inputTestId}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { RadioGroup, type RadioItem } from '../radio';
|
|||||||
import * as styles from './icon-picker.css';
|
import * as styles from './icon-picker.css';
|
||||||
import { AffineIconPicker } from './picker/affine-icon/affine-icon-picker';
|
import { AffineIconPicker } from './picker/affine-icon/affine-icon-picker';
|
||||||
import { EmojiPicker } from './picker/emoji/emoji-picker';
|
import { EmojiPicker } from './picker/emoji/emoji-picker';
|
||||||
|
import { type IconData, IconType } from './type';
|
||||||
|
|
||||||
const panels: Array<RadioItem> = [
|
const panels: Array<RadioItem> = [
|
||||||
{ value: 'Emoji', className: styles.headerNavItem },
|
{ value: 'Emoji', className: styles.headerNavItem },
|
||||||
@@ -16,17 +17,11 @@ const panels: Array<RadioItem> = [
|
|||||||
export const IconPicker = ({
|
export const IconPicker = ({
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
}: HTMLAttributes<HTMLDivElement> & {
|
onSelect,
|
||||||
onSelect?: (
|
}: Omit<HTMLAttributes<HTMLDivElement>, 'onSelect'> & {
|
||||||
type: 'emoji' | 'affine-icon',
|
onSelect?: (data?: IconData) => void;
|
||||||
data: { icon?: string; color?: string }
|
|
||||||
) => void;
|
|
||||||
}) => {
|
}) => {
|
||||||
const [activePanel, setActivePanel] = useState<string>('Icons');
|
const [activePanel, setActivePanel] = useState<string>('Emoji');
|
||||||
|
|
||||||
// const ActivePanel = panels.find(
|
|
||||||
// panel => panel.value === activePanel
|
|
||||||
// )?.component;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx(styles.container, className)} style={{ ...style }}>
|
<div className={clsx(styles.container, className)} style={{ ...style }}>
|
||||||
@@ -53,7 +48,7 @@ export const IconPicker = ({
|
|||||||
<Button
|
<Button
|
||||||
variant="plain"
|
variant="plain"
|
||||||
style={{ color: cssVarV2.text.secondary, fontWeight: 500 }}
|
style={{ color: cssVarV2.text.secondary, fontWeight: 500 }}
|
||||||
onClick={() => void 0}
|
onClick={() => onSelect?.()}
|
||||||
>
|
>
|
||||||
Remove
|
Remove
|
||||||
</Button>
|
</Button>
|
||||||
@@ -61,9 +56,17 @@ export const IconPicker = ({
|
|||||||
</header>
|
</header>
|
||||||
<main className={styles.main}>
|
<main className={styles.main}>
|
||||||
{activePanel === 'Emoji' ? (
|
{activePanel === 'Emoji' ? (
|
||||||
<EmojiPicker />
|
<EmojiPicker
|
||||||
|
onSelect={emoji => {
|
||||||
|
onSelect?.({ type: IconType.Emoji, unicode: emoji });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
) : activePanel === 'Icons' ? (
|
) : activePanel === 'Icons' ? (
|
||||||
<AffineIconPicker />
|
<AffineIconPicker
|
||||||
|
onSelect={(icon, color) => {
|
||||||
|
onSelect?.({ type: IconType.AffineIcon, name: icon, color });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
export * from './icon-picker';
|
export * from './icon-picker';
|
||||||
|
export * from './renderer';
|
||||||
|
export * from './type';
|
||||||
|
|||||||
@@ -1,18 +1,29 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
import { AffineIconRenderer } from './renderer/affine-icon';
|
import { AffineIconRenderer } from './renderer/affine-icon';
|
||||||
|
import { type IconData, IconType } from './type';
|
||||||
|
|
||||||
export const IconRenderer = ({
|
export const IconRenderer = ({
|
||||||
iconType,
|
data,
|
||||||
icon,
|
fallback,
|
||||||
}: {
|
}: {
|
||||||
iconType: 'emoji' | 'affine-icon';
|
data?: IconData;
|
||||||
icon: string;
|
fallback?: ReactNode;
|
||||||
}) => {
|
}) => {
|
||||||
if (iconType === 'emoji') {
|
if (!data) {
|
||||||
return icon;
|
return fallback ?? null;
|
||||||
}
|
|
||||||
if (iconType === 'affine-icon') {
|
|
||||||
return <AffineIconRenderer name={icon} />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
if (data.type === IconType.Emoji && data.unicode) {
|
||||||
|
return data.unicode;
|
||||||
|
}
|
||||||
|
if (data.type === IconType.AffineIcon && data.name) {
|
||||||
|
return <AffineIconRenderer name={data.name} color={data.color} />;
|
||||||
|
}
|
||||||
|
if (data.type === IconType.Blob) {
|
||||||
|
// Not supported yet
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback ?? null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
export enum IconType {
|
||||||
|
Emoji = 'emoji',
|
||||||
|
AffineIcon = 'affine-icon',
|
||||||
|
Blob = 'blob',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IconData =
|
||||||
|
| {
|
||||||
|
type: IconType.Emoji;
|
||||||
|
unicode: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: IconType.AffineIcon;
|
||||||
|
name: string;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: IconType.Blob;
|
||||||
|
blob: Blob;
|
||||||
|
};
|
||||||
@@ -6,7 +6,7 @@ export const docIconPickerTrigger = style({
|
|||||||
height: 64,
|
height: 64,
|
||||||
padding: 2,
|
padding: 2,
|
||||||
selectors: {
|
selectors: {
|
||||||
'&[data-icon-type="emoji"]': {
|
'&[data-icon-type="emoji"], &[data-icon-type="affine-icon"]': {
|
||||||
fontSize: 60,
|
fontSize: 60,
|
||||||
lineHeight: 1,
|
lineHeight: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -40,12 +40,15 @@ export const DocIconPicker = ({
|
|||||||
|
|
||||||
const icon = useLiveData(explorerIconService.icon$('doc', docId));
|
const icon = useLiveData(explorerIconService.icon$('doc', docId));
|
||||||
|
|
||||||
const isPlaceholder = !icon?.type || !icon?.icon;
|
const isPlaceholder = !icon?.icon;
|
||||||
|
|
||||||
if (readonly) {
|
if (readonly) {
|
||||||
return isPlaceholder ? null : (
|
return isPlaceholder ? null : (
|
||||||
<div className={styles.docIconPickerTrigger} data-icon-type={icon?.type}>
|
<div
|
||||||
<IconRenderer iconType={icon.type} icon={icon.icon} />
|
className={styles.docIconPickerTrigger}
|
||||||
|
data-icon-type={icon?.icon?.type}
|
||||||
|
>
|
||||||
|
<IconRenderer data={icon.icon} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -53,14 +56,12 @@ export const DocIconPicker = ({
|
|||||||
return (
|
return (
|
||||||
<TitleContainer isPlaceholder={isPlaceholder}>
|
<TitleContainer isPlaceholder={isPlaceholder}>
|
||||||
<IconEditor
|
<IconEditor
|
||||||
iconType={icon?.type}
|
|
||||||
icon={icon?.icon}
|
icon={icon?.icon}
|
||||||
onIconChange={(type, icon) => {
|
onIconChange={data => {
|
||||||
explorerIconService.setIcon({
|
explorerIconService.setIcon({
|
||||||
where: 'doc',
|
where: 'doc',
|
||||||
id: docId,
|
id: docId,
|
||||||
type,
|
icon: data,
|
||||||
icon,
|
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
closeAfterSelect={true}
|
closeAfterSelect={true}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import {
|
|||||||
type DropTargetTreeInstruction,
|
type DropTargetTreeInstruction,
|
||||||
IconAndNameEditorMenu,
|
IconAndNameEditorMenu,
|
||||||
IconButton,
|
IconButton,
|
||||||
|
type IconData,
|
||||||
|
IconRenderer,
|
||||||
Menu,
|
Menu,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
useDraggable,
|
useDraggable,
|
||||||
@@ -13,7 +15,6 @@ import {
|
|||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
import { Guard } from '@affine/core/components/guard';
|
import { Guard } from '@affine/core/components/guard';
|
||||||
import { AppSidebarService } from '@affine/core/modules/app-sidebar';
|
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 { ExplorerIconService } from '@affine/core/modules/explorer-icon/services/explorer-icon';
|
||||||
import type { ExplorerType } from '@affine/core/modules/explorer-icon/store/explorer-icon';
|
import type { ExplorerType } from '@affine/core/modules/explorer-icon/store/explorer-icon';
|
||||||
import type { DocPermissionActions } from '@affine/core/modules/permissions';
|
import type { DocPermissionActions } from '@affine/core/modules/permissions';
|
||||||
@@ -142,13 +143,12 @@ export const NavigationPanelTreeNodeRenameModal = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const onIconChange = useCallback(
|
const onIconChange = useCallback(
|
||||||
(type?: ExplorerIconType, icon?: string) => {
|
(data?: IconData) => {
|
||||||
if (!explorerIconConfig) return;
|
if (!explorerIconConfig) return;
|
||||||
explorerIconService.setIcon({
|
explorerIconService.setIcon({
|
||||||
where: explorerIconConfig.where,
|
where: explorerIconConfig.where,
|
||||||
id: explorerIconConfig.id,
|
id: explorerIconConfig.id,
|
||||||
type,
|
icon: data,
|
||||||
icon,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[explorerIconConfig, explorerIconService]
|
[explorerIconConfig, explorerIconService]
|
||||||
@@ -161,8 +161,7 @@ export const NavigationPanelTreeNodeRenameModal = ({
|
|||||||
onIconChange={onIconChange}
|
onIconChange={onIconChange}
|
||||||
onNameChange={handleRename}
|
onNameChange={handleRename}
|
||||||
name={rawName ?? ''}
|
name={rawName ?? ''}
|
||||||
iconType={explorerIcon?.type ?? 'emoji'}
|
icon={explorerIcon?.icon}
|
||||||
icon={explorerIcon?.icon ?? ''}
|
|
||||||
width={sidebarWidth - 16}
|
width={sidebarWidth - 16}
|
||||||
contentOptions={{
|
contentOptions={{
|
||||||
sideOffset: 36,
|
sideOffset: 36,
|
||||||
@@ -461,10 +460,7 @@ export const NavigationPanelTreeNode = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.iconContainer}>
|
<div className={styles.iconContainer}>
|
||||||
{/* Only emoji icon is supported for now */}
|
<IconRenderer data={explorerIcon?.icon} fallback={fallbackIcon} />
|
||||||
{explorerIcon && explorerIcon.type === 'emoji'
|
|
||||||
? explorerIcon.icon
|
|
||||||
: fallbackIcon}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { IconData } from '@affine/component';
|
||||||
import {
|
import {
|
||||||
type DBSchemaBuilder,
|
type DBSchemaBuilder,
|
||||||
f,
|
f,
|
||||||
@@ -51,8 +52,7 @@ export const AFFiNE_WORKSPACE_DB_SCHEMA = {
|
|||||||
* ${doc|collection|folder|tag}:${id}
|
* ${doc|collection|folder|tag}:${id}
|
||||||
*/
|
*/
|
||||||
id: f.string().primaryKey(),
|
id: f.string().primaryKey(),
|
||||||
type: f.enum('emoji', 'affine-icon', 'blob'),
|
icon: f.json<IconData>(),
|
||||||
icon: f.string(),
|
|
||||||
},
|
},
|
||||||
} as const satisfies DBSchemaBuilder;
|
} as const satisfies DBSchemaBuilder;
|
||||||
export type AFFiNEWorkspaceDbSchema = typeof AFFiNE_WORKSPACE_DB_SCHEMA;
|
export type AFFiNEWorkspaceDbSchema = typeof AFFiNE_WORKSPACE_DB_SCHEMA;
|
||||||
@@ -61,10 +61,6 @@ export type DocProperties = ORMEntity<AFFiNEWorkspaceDbSchema['docProperties']>;
|
|||||||
export type DocCustomPropertyInfo = ORMEntity<
|
export type DocCustomPropertyInfo = ORMEntity<
|
||||||
AFFiNEWorkspaceDbSchema['docCustomPropertyInfo']
|
AFFiNEWorkspaceDbSchema['docCustomPropertyInfo']
|
||||||
>;
|
>;
|
||||||
export type ExplorerIcon = ORMEntity<AFFiNEWorkspaceDbSchema['explorerIcon']>;
|
|
||||||
export type ExplorerIconType = ORMEntity<
|
|
||||||
AFFiNEWorkspaceDbSchema['explorerIcon']
|
|
||||||
>['type'];
|
|
||||||
|
|
||||||
export const AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA = {
|
export const AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA = {
|
||||||
favorite: {
|
favorite: {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import type { DocRecord, DocsService } from '../../doc';
|
|||||||
import type { ExplorerIconService } from '../../explorer-icon/services/explorer-icon';
|
import type { ExplorerIconService } from '../../explorer-icon/services/explorer-icon';
|
||||||
import type { I18nService } from '../../i18n';
|
import type { I18nService } from '../../i18n';
|
||||||
import type { JournalService } from '../../journal';
|
import type { JournalService } from '../../journal';
|
||||||
|
import { getDocIconComponent } from './icon';
|
||||||
|
|
||||||
type IconType = 'rc' | 'lit';
|
type IconType = 'rc' | 'lit';
|
||||||
interface DocDisplayIconOptions<T extends IconType> {
|
interface DocDisplayIconOptions<T extends IconType> {
|
||||||
@@ -149,8 +150,10 @@ export class DocDisplayMetaService extends Service {
|
|||||||
if (enableEmojiIcon) {
|
if (enableEmojiIcon) {
|
||||||
// const { emoji } = extractEmojiIcon(title);
|
// const { emoji } = extractEmojiIcon(title);
|
||||||
// if (emoji) return () => emoji;
|
// if (emoji) return () => emoji;
|
||||||
const icon = get(this.explorerIconService.icon$('doc', docId));
|
const icon = get(this.explorerIconService.icon$('doc', docId))?.icon;
|
||||||
if (icon && icon.type === 'emoji') return () => icon.icon;
|
if (icon) {
|
||||||
|
return getDocIconComponent(icon);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// title alias
|
// title alias
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { type IconData, IconRenderer } from '@affine/component';
|
||||||
|
|
||||||
|
export const getDocIconComponent = (icon: IconData) => {
|
||||||
|
const Icon = () => <IconRenderer data={icon} />;
|
||||||
|
Icon.displayName = 'DocIcon';
|
||||||
|
return Icon;
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import type { IconData } from '@affine/component';
|
||||||
import { Store } from '@toeverything/infra';
|
import { Store } from '@toeverything/infra';
|
||||||
|
|
||||||
import type { WorkspaceDBService } from '../../db';
|
import type { WorkspaceDBService } from '../../db';
|
||||||
import type { ExplorerIconType } from '../../db/schema/schema';
|
|
||||||
|
|
||||||
export type ExplorerType = 'doc' | 'collection' | 'folder' | 'tag';
|
export type ExplorerType = 'doc' | 'collection' | 'folder' | 'tag';
|
||||||
|
|
||||||
@@ -18,21 +18,15 @@ export class ExplorerIconStore extends Store {
|
|||||||
return this.dbService.db.explorerIcon.get(`${type}:${id}`);
|
return this.dbService.db.explorerIcon.get(`${type}:${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
setIcon(options: {
|
setIcon(options: { where: ExplorerType; id: string; icon?: IconData }) {
|
||||||
where: ExplorerType;
|
const { where, id, icon } = options;
|
||||||
id: string;
|
|
||||||
type?: ExplorerIconType;
|
|
||||||
icon?: string;
|
|
||||||
}) {
|
|
||||||
const { where, id, type, icon } = options;
|
|
||||||
// remove icon
|
// remove icon
|
||||||
if (!type || !icon) {
|
if (!icon) {
|
||||||
return this.dbService.db.explorerIcon.delete(`${where}:${id}`);
|
return this.dbService.db.explorerIcon.delete(`${where}:${id}`);
|
||||||
}
|
}
|
||||||
// upsert icon
|
// upsert icon
|
||||||
return this.dbService.db.explorerIcon.create({
|
return this.dbService.db.explorerIcon.create({
|
||||||
id: `${where}:${id}`,
|
id: `${where}:${id}`,
|
||||||
type,
|
|
||||||
icon,
|
icon,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -314,8 +314,6 @@ __metadata:
|
|||||||
"@blocksuite/affine": "workspace:*"
|
"@blocksuite/affine": "workspace:*"
|
||||||
"@blocksuite/icons": "npm:^2.2.17"
|
"@blocksuite/icons": "npm:^2.2.17"
|
||||||
"@chromatic-com/storybook": "npm:^4.0.0"
|
"@chromatic-com/storybook": "npm:^4.0.0"
|
||||||
"@emoji-mart/data": "npm:^1.2.1"
|
|
||||||
"@emoji-mart/react": "npm:^1.1.1"
|
|
||||||
"@emotion/react": "npm:^11.14.0"
|
"@emotion/react": "npm:^11.14.0"
|
||||||
"@emotion/styled": "npm:^11.14.0"
|
"@emotion/styled": "npm:^11.14.0"
|
||||||
"@radix-ui/react-avatar": "npm:^1.1.2"
|
"@radix-ui/react-avatar": "npm:^1.1.2"
|
||||||
@@ -5532,16 +5530,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@emoji-mart/react@npm:^1.1.1":
|
|
||||||
version: 1.1.1
|
|
||||||
resolution: "@emoji-mart/react@npm:1.1.1"
|
|
||||||
peerDependencies:
|
|
||||||
emoji-mart: ^5.2
|
|
||||||
react: ^16.8 || ^17 || ^18
|
|
||||||
checksum: 10/def4dddaa01ce88c396510d84d1878b881fe0f9c484a836a50e3db784a91ab98edf94816cff503b4d1cab7b00400a01c87e32d19dee2d950f64ef9243f79101e
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@emotion/babel-plugin@npm:^11.13.5":
|
"@emotion/babel-plugin@npm:^11.13.5":
|
||||||
version: 11.13.5
|
version: 11.13.5
|
||||||
resolution: "@emotion/babel-plugin@npm:11.13.5"
|
resolution: "@emotion/babel-plugin@npm:11.13.5"
|
||||||
|
|||||||
Reference in New Issue
Block a user