mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-25 18:26:05 +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-hitbox": "^1.0.3",
|
||||
"@blocksuite/icons": "^2.2.17",
|
||||
"@emoji-mart/data": "^1.2.1",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@radix-ui/react-avatar": "^1.1.2",
|
||||
|
||||
@@ -15,6 +15,7 @@ export const contentRoot = style({
|
||||
|
||||
export const iconPicker = style({
|
||||
padding: 0,
|
||||
lineHeight: 1,
|
||||
});
|
||||
globalStyle(`${iconPicker} span:has(svg)`, {
|
||||
lineHeight: 0,
|
||||
|
||||
@@ -2,12 +2,12 @@ import type { Meta, StoryFn } from '@storybook/react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { Button } from '../button';
|
||||
import { type IconData, IconType } from '../icon-picker';
|
||||
import { ResizePanel } from '../resize-panel/resize-panel';
|
||||
import {
|
||||
IconAndNameEditorMenu,
|
||||
type IconAndNameEditorMenuProps,
|
||||
IconEditor,
|
||||
type IconType,
|
||||
} from './icon-name-editor';
|
||||
|
||||
export default {
|
||||
@@ -16,10 +16,13 @@ export default {
|
||||
} satisfies Meta<typeof IconAndNameEditorMenu>;
|
||||
|
||||
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 handleIconChange = useCallback((_?: IconType, icon?: string) => {
|
||||
const handleIconChange = useCallback((icon?: IconData) => {
|
||||
setIcon(icon);
|
||||
}, []);
|
||||
const handleNameChange = useCallback((name: string) => {
|
||||
@@ -28,7 +31,7 @@ export const Basic: StoryFn<IconAndNameEditorMenuProps> = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Icon: {icon}</p>
|
||||
<p>Icon: {JSON.stringify(icon)}</p>
|
||||
<p>Name: {name}</p>
|
||||
|
||||
<ResizePanel
|
||||
@@ -44,17 +47,16 @@ export const Basic: StoryFn<IconAndNameEditorMenuProps> = () => {
|
||||
}}
|
||||
>
|
||||
<IconAndNameEditorMenu
|
||||
iconType="emoji"
|
||||
icon={icon}
|
||||
name={name}
|
||||
onIconChange={handleIconChange}
|
||||
onNameChange={handleNameChange}
|
||||
closeAfterSelect
|
||||
>
|
||||
<Button>Edit Name and Icon</Button>
|
||||
</IconAndNameEditorMenu>
|
||||
|
||||
<IconEditor
|
||||
iconType="emoji"
|
||||
icon={icon}
|
||||
onIconChange={handleIconChange}
|
||||
closeAfterSelect
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
import data from '@emoji-mart/data';
|
||||
import Picker from '@emoji-mart/react';
|
||||
import clsx from 'clsx';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { type ReactNode, useCallback, useState } from 'react';
|
||||
|
||||
import { Button, type ButtonProps } from '../button';
|
||||
import { type IconData, IconPicker } from '../icon-picker';
|
||||
import { IconRenderer } from '../icon-picker/renderer';
|
||||
import Input from '../input';
|
||||
import { Menu, type MenuProps } from '../menu';
|
||||
import * as styles from './icon-name-editor.css';
|
||||
|
||||
export type IconType = 'emoji' | 'affine-icon' | 'blob';
|
||||
|
||||
export interface IconEditorProps {
|
||||
iconType?: IconType;
|
||||
icon?: string;
|
||||
icon?: IconData;
|
||||
closeAfterSelect?: boolean;
|
||||
iconPlaceholder?: ReactNode;
|
||||
onIconChange?: (type?: IconType, icon?: string) => void;
|
||||
onIconChange?: (data?: IconData) => void;
|
||||
triggerClassName?: string;
|
||||
}
|
||||
|
||||
@@ -38,25 +34,7 @@ export interface IconAndNameEditorMenuProps
|
||||
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 = ({
|
||||
iconType,
|
||||
icon,
|
||||
closeAfterSelect,
|
||||
iconPlaceholder,
|
||||
@@ -71,16 +49,17 @@ export const IconEditor = ({
|
||||
triggerVariant?: ButtonProps['variant'];
|
||||
}) => {
|
||||
const [isPickerOpen, setIsPickerOpen] = useState(false);
|
||||
const { resolvedTheme } = useTheme();
|
||||
const handleEmojiClick = useCallback(
|
||||
(emoji: any) => {
|
||||
onIconChange?.('emoji', emoji.native);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(data?: IconData) => {
|
||||
onIconChange?.(data);
|
||||
if (closeAfterSelect) {
|
||||
setIsPickerOpen(false);
|
||||
}
|
||||
},
|
||||
[closeAfterSelect, onIconChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
rootOptions={{
|
||||
@@ -97,30 +76,18 @@ export const IconEditor = ({
|
||||
}}
|
||||
items={
|
||||
<div onWheel={e => e.stopPropagation()}>
|
||||
<Picker
|
||||
data={data}
|
||||
theme={resolvedTheme}
|
||||
onEmojiSelect={handleEmojiClick}
|
||||
/>
|
||||
<IconPicker onSelect={handleSelect} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant={triggerVariant}
|
||||
className={clsx(styles.iconPicker, triggerClassName)}
|
||||
data-icon-type={iconType}
|
||||
data-icon-type={icon?.type}
|
||||
aria-label={icon ? 'Change Icon' : 'Select Icon'}
|
||||
title={icon ? 'Change Icon' : 'Select Icon'}
|
||||
>
|
||||
{icon && iconType ? (
|
||||
<IconRenderer
|
||||
iconType={iconType}
|
||||
icon={icon}
|
||||
fallback={iconPlaceholder}
|
||||
/>
|
||||
) : (
|
||||
iconPlaceholder
|
||||
)}
|
||||
<IconRenderer data={icon} fallback={iconPlaceholder} />
|
||||
</Button>
|
||||
</Menu>
|
||||
);
|
||||
@@ -160,7 +127,6 @@ export const IconAndNameEditorMenu = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
width = 300,
|
||||
iconType: initialIconType,
|
||||
icon: initialIcon,
|
||||
name: initialName,
|
||||
onIconChange,
|
||||
@@ -169,15 +135,15 @@ export const IconAndNameEditorMenu = ({
|
||||
iconPlaceholder,
|
||||
skipIfNotChanged = true,
|
||||
inputTestId,
|
||||
closeAfterSelect,
|
||||
...menuProps
|
||||
}: IconAndNameEditorMenuProps) => {
|
||||
const [iconType, setIconType] = useState(initialIconType);
|
||||
const [icon, setIcon] = useState(initialIcon);
|
||||
const [name, setName] = useState(initialName);
|
||||
|
||||
const commit = useCallback(() => {
|
||||
if (iconType !== initialIconType || icon !== initialIcon) {
|
||||
onIconChange?.(iconType, icon);
|
||||
if (icon !== initialIcon) {
|
||||
onIconChange?.(icon);
|
||||
}
|
||||
if (skipIfNotChanged) {
|
||||
if (name !== initialName) onNameChange?.(name);
|
||||
@@ -186,9 +152,7 @@ export const IconAndNameEditorMenu = ({
|
||||
}
|
||||
}, [
|
||||
icon,
|
||||
iconType,
|
||||
initialIcon,
|
||||
initialIconType,
|
||||
initialName,
|
||||
name,
|
||||
onIconChange,
|
||||
@@ -196,13 +160,11 @@ export const IconAndNameEditorMenu = ({
|
||||
skipIfNotChanged,
|
||||
]);
|
||||
const abort = useCallback(() => {
|
||||
setIconType(initialIconType);
|
||||
setIcon(initialIcon);
|
||||
setName(initialName);
|
||||
}, [initialIcon, initialIconType, initialName]);
|
||||
const handleIconChange = useCallback((type?: IconType, icon?: string) => {
|
||||
setIconType(type);
|
||||
setIcon(icon);
|
||||
}, [initialIcon, initialName]);
|
||||
const handleIconChange = useCallback((data?: IconData) => {
|
||||
setIcon(data);
|
||||
}, []);
|
||||
const handleNameChange = useCallback((name: string) => {
|
||||
setName(name);
|
||||
@@ -210,13 +172,12 @@ export const IconAndNameEditorMenu = ({
|
||||
const handleMenuOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
if (open) {
|
||||
setIconType(initialIconType);
|
||||
setIcon(initialIcon);
|
||||
setName(initialName);
|
||||
}
|
||||
onOpenChange?.(open);
|
||||
},
|
||||
[initialIcon, initialIconType, initialName, onOpenChange]
|
||||
[initialIcon, initialName, onOpenChange]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -243,10 +204,10 @@ export const IconAndNameEditorMenu = ({
|
||||
{...menuProps}
|
||||
items={
|
||||
<IconAndNameEditorContent
|
||||
iconType={iconType}
|
||||
icon={icon}
|
||||
name={name}
|
||||
iconPlaceholder={iconPlaceholder}
|
||||
closeAfterSelect={closeAfterSelect}
|
||||
onIconChange={handleIconChange}
|
||||
onNameChange={handleNameChange}
|
||||
inputTestId={inputTestId}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { RadioGroup, type RadioItem } from '../radio';
|
||||
import * as styles from './icon-picker.css';
|
||||
import { AffineIconPicker } from './picker/affine-icon/affine-icon-picker';
|
||||
import { EmojiPicker } from './picker/emoji/emoji-picker';
|
||||
import { type IconData, IconType } from './type';
|
||||
|
||||
const panels: Array<RadioItem> = [
|
||||
{ value: 'Emoji', className: styles.headerNavItem },
|
||||
@@ -16,17 +17,11 @@ const panels: Array<RadioItem> = [
|
||||
export const IconPicker = ({
|
||||
className,
|
||||
style,
|
||||
}: HTMLAttributes<HTMLDivElement> & {
|
||||
onSelect?: (
|
||||
type: 'emoji' | 'affine-icon',
|
||||
data: { icon?: string; color?: string }
|
||||
) => void;
|
||||
onSelect,
|
||||
}: Omit<HTMLAttributes<HTMLDivElement>, 'onSelect'> & {
|
||||
onSelect?: (data?: IconData) => void;
|
||||
}) => {
|
||||
const [activePanel, setActivePanel] = useState<string>('Icons');
|
||||
|
||||
// const ActivePanel = panels.find(
|
||||
// panel => panel.value === activePanel
|
||||
// )?.component;
|
||||
const [activePanel, setActivePanel] = useState<string>('Emoji');
|
||||
|
||||
return (
|
||||
<div className={clsx(styles.container, className)} style={{ ...style }}>
|
||||
@@ -53,7 +48,7 @@ export const IconPicker = ({
|
||||
<Button
|
||||
variant="plain"
|
||||
style={{ color: cssVarV2.text.secondary, fontWeight: 500 }}
|
||||
onClick={() => void 0}
|
||||
onClick={() => onSelect?.()}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
@@ -61,9 +56,17 @@ export const IconPicker = ({
|
||||
</header>
|
||||
<main className={styles.main}>
|
||||
{activePanel === 'Emoji' ? (
|
||||
<EmojiPicker />
|
||||
<EmojiPicker
|
||||
onSelect={emoji => {
|
||||
onSelect?.({ type: IconType.Emoji, unicode: emoji });
|
||||
}}
|
||||
/>
|
||||
) : activePanel === 'Icons' ? (
|
||||
<AffineIconPicker />
|
||||
<AffineIconPicker
|
||||
onSelect={(icon, color) => {
|
||||
onSelect?.({ type: IconType.AffineIcon, name: icon, color });
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
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 { type IconData, IconType } from './type';
|
||||
|
||||
export const IconRenderer = ({
|
||||
iconType,
|
||||
icon,
|
||||
data,
|
||||
fallback,
|
||||
}: {
|
||||
iconType: 'emoji' | 'affine-icon';
|
||||
icon: string;
|
||||
data?: IconData;
|
||||
fallback?: ReactNode;
|
||||
}) => {
|
||||
if (iconType === 'emoji') {
|
||||
return icon;
|
||||
}
|
||||
if (iconType === 'affine-icon') {
|
||||
return <AffineIconRenderer name={icon} />;
|
||||
if (!data) {
|
||||
return fallback ?? null;
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
20
packages/frontend/component/src/ui/icon-picker/type.ts
Normal file
20
packages/frontend/component/src/ui/icon-picker/type.ts
Normal file
@@ -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,
|
||||
padding: 2,
|
||||
selectors: {
|
||||
'&[data-icon-type="emoji"]': {
|
||||
'&[data-icon-type="emoji"], &[data-icon-type="affine-icon"]': {
|
||||
fontSize: 60,
|
||||
lineHeight: 1,
|
||||
},
|
||||
|
||||
@@ -40,12 +40,15 @@ export const DocIconPicker = ({
|
||||
|
||||
const icon = useLiveData(explorerIconService.icon$('doc', docId));
|
||||
|
||||
const isPlaceholder = !icon?.type || !icon?.icon;
|
||||
const isPlaceholder = !icon?.icon;
|
||||
|
||||
if (readonly) {
|
||||
return isPlaceholder ? null : (
|
||||
<div className={styles.docIconPickerTrigger} data-icon-type={icon?.type}>
|
||||
<IconRenderer iconType={icon.type} icon={icon.icon} />
|
||||
<div
|
||||
className={styles.docIconPickerTrigger}
|
||||
data-icon-type={icon?.icon?.type}
|
||||
>
|
||||
<IconRenderer data={icon.icon} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -53,14 +56,12 @@ export const DocIconPicker = ({
|
||||
return (
|
||||
<TitleContainer isPlaceholder={isPlaceholder}>
|
||||
<IconEditor
|
||||
iconType={icon?.type}
|
||||
icon={icon?.icon}
|
||||
onIconChange={(type, icon) => {
|
||||
onIconChange={data => {
|
||||
explorerIconService.setIcon({
|
||||
where: 'doc',
|
||||
id: docId,
|
||||
type,
|
||||
icon,
|
||||
icon: data,
|
||||
});
|
||||
}}
|
||||
closeAfterSelect={true}
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
type DropTargetTreeInstruction,
|
||||
IconAndNameEditorMenu,
|
||||
IconButton,
|
||||
type IconData,
|
||||
IconRenderer,
|
||||
Menu,
|
||||
MenuItem,
|
||||
useDraggable,
|
||||
@@ -13,7 +15,6 @@ import {
|
||||
} from '@affine/component';
|
||||
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';
|
||||
@@ -142,13 +143,12 @@ export const NavigationPanelTreeNodeRenameModal = ({
|
||||
);
|
||||
|
||||
const onIconChange = useCallback(
|
||||
(type?: ExplorerIconType, icon?: string) => {
|
||||
(data?: IconData) => {
|
||||
if (!explorerIconConfig) return;
|
||||
explorerIconService.setIcon({
|
||||
where: explorerIconConfig.where,
|
||||
id: explorerIconConfig.id,
|
||||
type,
|
||||
icon,
|
||||
icon: data,
|
||||
});
|
||||
},
|
||||
[explorerIconConfig, explorerIconService]
|
||||
@@ -161,8 +161,7 @@ export const NavigationPanelTreeNodeRenameModal = ({
|
||||
onIconChange={onIconChange}
|
||||
onNameChange={handleRename}
|
||||
name={rawName ?? ''}
|
||||
iconType={explorerIcon?.type ?? 'emoji'}
|
||||
icon={explorerIcon?.icon ?? ''}
|
||||
icon={explorerIcon?.icon}
|
||||
width={sidebarWidth - 16}
|
||||
contentOptions={{
|
||||
sideOffset: 36,
|
||||
@@ -461,10 +460,7 @@ export const NavigationPanelTreeNode = ({
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.iconContainer}>
|
||||
{/* Only emoji icon is supported for now */}
|
||||
{explorerIcon && explorerIcon.type === 'emoji'
|
||||
? explorerIcon.icon
|
||||
: fallbackIcon}
|
||||
<IconRenderer data={explorerIcon?.icon} fallback={fallbackIcon} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { IconData } from '@affine/component';
|
||||
import {
|
||||
type DBSchemaBuilder,
|
||||
f,
|
||||
@@ -51,8 +52,7 @@ export const AFFiNE_WORKSPACE_DB_SCHEMA = {
|
||||
* ${doc|collection|folder|tag}:${id}
|
||||
*/
|
||||
id: f.string().primaryKey(),
|
||||
type: f.enum('emoji', 'affine-icon', 'blob'),
|
||||
icon: f.string(),
|
||||
icon: f.json<IconData>(),
|
||||
},
|
||||
} as const satisfies DBSchemaBuilder;
|
||||
export type AFFiNEWorkspaceDbSchema = typeof AFFiNE_WORKSPACE_DB_SCHEMA;
|
||||
@@ -61,10 +61,6 @@ export type DocProperties = ORMEntity<AFFiNEWorkspaceDbSchema['docProperties']>;
|
||||
export type DocCustomPropertyInfo = ORMEntity<
|
||||
AFFiNEWorkspaceDbSchema['docCustomPropertyInfo']
|
||||
>;
|
||||
export type ExplorerIcon = ORMEntity<AFFiNEWorkspaceDbSchema['explorerIcon']>;
|
||||
export type ExplorerIconType = ORMEntity<
|
||||
AFFiNEWorkspaceDbSchema['explorerIcon']
|
||||
>['type'];
|
||||
|
||||
export const AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA = {
|
||||
favorite: {
|
||||
|
||||
@@ -29,6 +29,7 @@ import type { DocRecord, DocsService } from '../../doc';
|
||||
import type { ExplorerIconService } from '../../explorer-icon/services/explorer-icon';
|
||||
import type { I18nService } from '../../i18n';
|
||||
import type { JournalService } from '../../journal';
|
||||
import { getDocIconComponent } from './icon';
|
||||
|
||||
type IconType = 'rc' | 'lit';
|
||||
interface DocDisplayIconOptions<T extends IconType> {
|
||||
@@ -149,8 +150,10 @@ export class DocDisplayMetaService extends Service {
|
||||
if (enableEmojiIcon) {
|
||||
// const { emoji } = extractEmojiIcon(title);
|
||||
// if (emoji) return () => emoji;
|
||||
const icon = get(this.explorerIconService.icon$('doc', docId));
|
||||
if (icon && icon.type === 'emoji') return () => icon.icon;
|
||||
const icon = get(this.explorerIconService.icon$('doc', docId))?.icon;
|
||||
if (icon) {
|
||||
return getDocIconComponent(icon);
|
||||
}
|
||||
}
|
||||
|
||||
// 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 type { WorkspaceDBService } from '../../db';
|
||||
import type { ExplorerIconType } from '../../db/schema/schema';
|
||||
|
||||
export type ExplorerType = 'doc' | 'collection' | 'folder' | 'tag';
|
||||
|
||||
@@ -18,21 +18,15 @@ export class ExplorerIconStore extends Store {
|
||||
return this.dbService.db.explorerIcon.get(`${type}:${id}`);
|
||||
}
|
||||
|
||||
setIcon(options: {
|
||||
where: ExplorerType;
|
||||
id: string;
|
||||
type?: ExplorerIconType;
|
||||
icon?: string;
|
||||
}) {
|
||||
const { where, id, type, icon } = options;
|
||||
setIcon(options: { where: ExplorerType; id: string; icon?: IconData }) {
|
||||
const { where, id, icon } = options;
|
||||
// remove icon
|
||||
if (!type || !icon) {
|
||||
if (!icon) {
|
||||
return this.dbService.db.explorerIcon.delete(`${where}:${id}`);
|
||||
}
|
||||
// upsert icon
|
||||
return this.dbService.db.explorerIcon.create({
|
||||
id: `${where}:${id}`,
|
||||
type,
|
||||
icon,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user