mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-23 13:29:02 +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:
@@ -46,6 +46,14 @@ export const AFFiNE_WORKSPACE_DB_SCHEMA = {
|
||||
collectionId: f.string().primaryKey(),
|
||||
index: f.string(),
|
||||
},
|
||||
explorerIcon: {
|
||||
/**
|
||||
* ${doc|collection|folder|tag}:${id}
|
||||
*/
|
||||
id: f.string().primaryKey(),
|
||||
type: f.enum('emoji', 'affine-icon', 'blob'),
|
||||
icon: f.string(),
|
||||
},
|
||||
} as const satisfies DBSchemaBuilder;
|
||||
export type AFFiNEWorkspaceDbSchema = typeof AFFiNE_WORKSPACE_DB_SCHEMA;
|
||||
|
||||
@@ -53,6 +61,10 @@ 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: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type Framework } from '@toeverything/infra';
|
||||
|
||||
import { DocsService } from '../doc';
|
||||
import { FeatureFlagService } from '../feature-flag';
|
||||
import { ExplorerIconService } from '../explorer-icon/services/explorer-icon';
|
||||
import { I18nService } from '../i18n';
|
||||
import { JournalService } from '../journal';
|
||||
import { WorkspaceScope } from '../workspace';
|
||||
@@ -15,7 +15,7 @@ export function configureDocDisplayMetaModule(framework: Framework) {
|
||||
.service(DocDisplayMetaService, [
|
||||
JournalService,
|
||||
DocsService,
|
||||
FeatureFlagService,
|
||||
I18nService,
|
||||
ExplorerIconService,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { extractEmojiIcon } from '@affine/core/utils';
|
||||
import { i18nTime } from '@affine/i18n';
|
||||
import {
|
||||
AliasIcon as LitAliasIcon,
|
||||
@@ -27,7 +26,7 @@ import type { Dayjs } from 'dayjs';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import type { DocRecord, DocsService } from '../../doc';
|
||||
import type { FeatureFlagService } from '../../feature-flag';
|
||||
import type { ExplorerIconService } from '../../explorer-icon/services/explorer-icon';
|
||||
import type { I18nService } from '../../i18n';
|
||||
import type { JournalService } from '../../journal';
|
||||
|
||||
@@ -87,8 +86,8 @@ export class DocDisplayMetaService extends Service {
|
||||
constructor(
|
||||
private readonly journalService: JournalService,
|
||||
private readonly docsService: DocsService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly i18nService: I18nService
|
||||
private readonly i18nService: I18nService,
|
||||
private readonly explorerIconService: ExplorerIconService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -128,30 +127,30 @@ export class DocDisplayMetaService extends Service {
|
||||
const iconSet = icons[options?.type ?? 'rc'];
|
||||
|
||||
return LiveData.computed(get => {
|
||||
const enableEmojiIcon =
|
||||
get(this.featureFlagService.flags.enable_emoji_doc_icon.$) &&
|
||||
options?.enableEmojiIcon !== false;
|
||||
const enableEmojiIcon = options?.enableEmojiIcon !== false;
|
||||
const doc = get(this.docsService.list.doc$(docId));
|
||||
const referenced = !!options?.reference;
|
||||
const titleAlias = referenced ? options?.title : undefined;
|
||||
const originalTitle = doc ? get(doc.title$) : '';
|
||||
// const originalTitle = doc ? get(doc.title$) : '';
|
||||
// link to journal doc
|
||||
const journalDateString = get(this.journalService.journalDate$(docId));
|
||||
const journalIcon = journalDateString
|
||||
? this.getJournalIcon(journalDateString, options)
|
||||
: undefined;
|
||||
const journalTitle = journalDateString
|
||||
? i18nTime(journalDateString, { absolute: { accuracy: 'day' } })
|
||||
: undefined;
|
||||
const title = titleAlias ?? journalTitle ?? originalTitle;
|
||||
// const journalTitle = journalDateString
|
||||
// ? i18nTime(journalDateString, { absolute: { accuracy: 'day' } })
|
||||
// : undefined;
|
||||
// const title = titleAlias ?? journalTitle ?? originalTitle;
|
||||
const mode = doc ? get(doc.primaryMode$) : undefined;
|
||||
const finalMode = options?.mode ?? mode ?? 'page';
|
||||
const referenceToNode = !!(referenced && options.referenceToNode);
|
||||
|
||||
// emoji title
|
||||
if (enableEmojiIcon && title) {
|
||||
const { emoji } = extractEmojiIcon(title);
|
||||
if (emoji) return () => emoji;
|
||||
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;
|
||||
}
|
||||
|
||||
// title alias
|
||||
@@ -176,10 +175,6 @@ export class DocDisplayMetaService extends Service {
|
||||
|
||||
title$(docId: string, options?: DocDisplayTitleOptions) {
|
||||
return LiveData.computed(get => {
|
||||
const enableEmojiIcon =
|
||||
get(this.featureFlagService.flags.enable_emoji_doc_icon.$) &&
|
||||
options?.enableEmojiIcon !== false;
|
||||
|
||||
const lng = get(this.i18nService.i18n.currentLanguageKey$);
|
||||
const doc = get(this.docsService.list.doc$(docId));
|
||||
const referenced = !!options?.reference;
|
||||
@@ -190,20 +185,17 @@ export class DocDisplayMetaService extends Service {
|
||||
const journalTitle = journalDateString
|
||||
? i18nTime(journalDateString, { absolute: { accuracy: 'day' } })
|
||||
: undefined;
|
||||
const title = titleAlias ?? journalTitle ?? originalTitle;
|
||||
// const title = titleAlias ?? journalTitle ?? originalTitle;
|
||||
|
||||
// emoji title
|
||||
if (enableEmojiIcon && title) {
|
||||
const { rest } = extractEmojiIcon(title);
|
||||
if (rest) return rest;
|
||||
|
||||
// When the title has only one emoji character,
|
||||
// if it's a journal document, the date should be displayed.
|
||||
const journalDateString = get(this.journalService.journalDate$(docId));
|
||||
if (journalDateString) {
|
||||
return i18nTime(journalDateString, { absolute: { accuracy: 'day' } });
|
||||
}
|
||||
}
|
||||
// if (enableEmojiIcon && title) {
|
||||
// // When the title has only one emoji character,
|
||||
// // if it's a journal document, the date should be displayed.
|
||||
// const journalDateString = get(this.journalService.journalDate$(docId));
|
||||
// if (journalDateString) {
|
||||
// return i18nTime(journalDateString, { absolute: { accuracy: 'day' } });
|
||||
// }
|
||||
// }
|
||||
|
||||
// title alias
|
||||
if (titleAlias) return titleAlias;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { Framework } from '@toeverything/infra';
|
||||
|
||||
import { WorkspaceDBService } from '../db';
|
||||
import { WorkspaceScope } from '../workspace';
|
||||
import { ExplorerIconService } from './services/explorer-icon';
|
||||
import { ExplorerIconStore } from './store/explorer-icon';
|
||||
|
||||
export function configureExplorerIconModule(framework: Framework) {
|
||||
framework
|
||||
.scope(WorkspaceScope)
|
||||
.store(ExplorerIconStore, [WorkspaceDBService])
|
||||
.service(ExplorerIconService, [ExplorerIconStore]);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { LiveData, Service } from '@toeverything/infra';
|
||||
|
||||
import type { ExplorerIconStore, ExplorerType } from '../store/explorer-icon';
|
||||
|
||||
export class ExplorerIconService extends Service {
|
||||
constructor(private readonly store: ExplorerIconStore) {
|
||||
super();
|
||||
}
|
||||
|
||||
getIcon(type: ExplorerType, id: string) {
|
||||
return this.store.getIcon(type, id);
|
||||
}
|
||||
|
||||
setIcon(options: Parameters<ExplorerIconStore['setIcon']>[0]) {
|
||||
return this.store.setIcon(options);
|
||||
}
|
||||
|
||||
icon$(type: ExplorerType, id: string) {
|
||||
return LiveData.from(this.store.watchIcon(type, id), null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Store } from '@toeverything/infra';
|
||||
|
||||
import type { WorkspaceDBService } from '../../db';
|
||||
import type { ExplorerIconType } from '../../db/schema/schema';
|
||||
|
||||
export type ExplorerType = 'doc' | 'collection' | 'folder' | 'tag';
|
||||
|
||||
export class ExplorerIconStore extends Store {
|
||||
constructor(private readonly dbService: WorkspaceDBService) {
|
||||
super();
|
||||
}
|
||||
|
||||
watchIcon(type: ExplorerType, id: string) {
|
||||
return this.dbService.db.explorerIcon.get$(`${type}:${id}`);
|
||||
}
|
||||
|
||||
getIcon(type: ExplorerType, id: string) {
|
||||
return this.dbService.db.explorerIcon.get(`${type}:${id}`);
|
||||
}
|
||||
|
||||
setIcon(options: {
|
||||
where: ExplorerType;
|
||||
id: string;
|
||||
type?: ExplorerIconType;
|
||||
icon?: string;
|
||||
}) {
|
||||
const { where, id, type, icon } = options;
|
||||
// remove icon
|
||||
if (!type || !icon) {
|
||||
return this.dbService.db.explorerIcon.delete(`${where}:${id}`);
|
||||
}
|
||||
// upsert icon
|
||||
return this.dbService.db.explorerIcon.create({
|
||||
id: `${where}:${id}`,
|
||||
type,
|
||||
icon,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import { configureDocSummaryModule } from './doc-summary';
|
||||
import { configureDocsSearchModule } from './docs-search';
|
||||
import { configureEditorModule } from './editor';
|
||||
import { configureEditorSettingModule } from './editor-setting';
|
||||
import { configureExplorerIconModule } from './explorer-icon';
|
||||
import { configureFavoriteModule } from './favorite';
|
||||
import { configureFeatureFlagModule } from './feature-flag';
|
||||
import { configureGlobalContextModule } from './global-context';
|
||||
@@ -85,6 +86,7 @@ export function configureCommonModules(framework: Framework) {
|
||||
configureTelemetryModule(framework);
|
||||
configurePDFModule(framework);
|
||||
configurePeekViewModule(framework);
|
||||
configureExplorerIconModule(framework);
|
||||
configureDocDisplayMetaModule(framework);
|
||||
configureQuickSearchModule(framework);
|
||||
configureDocsSearchModule(framework);
|
||||
|
||||
Reference in New Issue
Block a user