feat: add icon picker functionality to callout block

- Add IconData type import to callout-model.ts
- Implement icon picker component in callout-block.ts
- Copy renderUniLit function to avoid external dependencies
- Integrate icon picker directly in renderBlock for testing
- Remove unused IconPickerServiceIdentifier import
This commit is contained in:
zzj3720
2025-09-26 23:59:30 +08:00
parent 5147e2c62d
commit 06b84330a9
17 changed files with 297 additions and 78 deletions
@@ -175,6 +175,7 @@ const usePreviewExtensions = () => {
.ai(enableAI, framework)
.theme(framework)
.database(framework)
.iconPicker(framework)
.linkedDoc(framework)
.paragraph(enableAI)
.linkPreview(framework)
@@ -117,6 +117,7 @@ const usePatchSpecs = (mode: DocMode, shared?: boolean) => {
.electron(framework)
.linkPreview(framework)
.codeBlockPreview(framework)
.iconPicker(framework)
.comment(enableComment, framework).value;
if (BUILD_CONFIG.isMobileEdition) {
@@ -16,6 +16,7 @@ import {
type AffineEditorViewOptions,
} from '@affine/core/blocksuite/view-extensions/editor-view/editor-view';
import { ElectronViewExtension } from '@affine/core/blocksuite/view-extensions/electron';
import { AffineIconPickerExtension } from '@affine/core/blocksuite/view-extensions/icon-picker';
import { AffineLinkPreviewExtension } from '@affine/core/blocksuite/view-extensions/link-preview-service';
import { MobileViewExtension } from '@affine/core/blocksuite/view-extensions/mobile';
import { PdfViewExtension } from '@affine/core/blocksuite/view-extensions/pdf';
@@ -58,6 +59,7 @@ type Configure = {
electron: (framework?: FrameworkProvider) => Configure;
linkPreview: (framework?: FrameworkProvider) => Configure;
codeBlockPreview: (framework?: FrameworkProvider) => Configure;
iconPicker: (framework?: FrameworkProvider) => Configure;
comment: (
enableComment?: boolean,
framework?: FrameworkProvider
@@ -86,6 +88,7 @@ class ViewProvider {
AffineThemeViewExtension,
AffineEditorViewExtension,
AffineEditorConfigViewExtension,
AffineIconPickerExtension,
CodeBlockPreviewViewExtension,
EdgelessBlockHeaderConfigViewExtension,
TurboRendererViewExtension,
@@ -123,6 +126,7 @@ class ViewProvider {
electron: this._configureElectron,
linkPreview: this._configureLinkPreview,
codeBlockPreview: this._configureCodeBlockHtmlPreview,
iconPicker: this._configureIconPicker,
comment: this._configureComment,
value: this._manager,
};
@@ -146,6 +150,7 @@ class ViewProvider {
.electron()
.linkPreview()
.codeBlockPreview()
.iconPicker()
.comment();
return this.config;
@@ -333,6 +338,11 @@ class ViewProvider {
return this.config;
};
private readonly _configureIconPicker = (framework?: FrameworkProvider) => {
this._manager.configure(AffineIconPickerExtension, { framework });
return this.config;
};
private readonly _configureComment = (
enableComment?: boolean,
framework?: FrameworkProvider
@@ -0,0 +1,23 @@
import { IconPickerServiceIdentifier } from '@blocksuite/affine/shared/services';
import { type ExtensionType } from '@blocksuite/affine/store';
import type { Container } from '@blocksuite/global/di';
import type { FrameworkProvider } from '@toeverything/infra';
import { IconPickerService } from '../../../modules/icon-picker/services/icon-picker';
/**
* Patch the icon picker service to make it available in BlockSuite
* @param framework
* @returns
*/
export function patchIconPickerService(
framework: FrameworkProvider
): ExtensionType {
return {
setup: (di: Container) => {
di.override(IconPickerServiceIdentifier, () => {
return framework.get(IconPickerService);
});
},
};
}
@@ -0,0 +1,32 @@
import {
type ViewExtensionContext,
ViewExtensionProvider,
} from '@blocksuite/affine/ext-loader';
import { FrameworkProvider } from '@toeverything/infra';
import { z } from 'zod';
import { patchIconPickerService } from './icon-picker-service';
const optionsSchema = z.object({
framework: z.instanceof(FrameworkProvider).optional(),
});
type AffineIconPickerViewOptions = z.infer<typeof optionsSchema>;
export class AffineIconPickerExtension extends ViewExtensionProvider<AffineIconPickerViewOptions> {
override name = 'affine-icon-picker-extension';
override schema = optionsSchema;
override setup(
context: ViewExtensionContext,
options?: AffineIconPickerViewOptions
) {
super.setup(context, options);
if (!options?.framework) {
return;
}
const { framework } = options;
context.register(patchIconPickerService(framework));
}
}
@@ -44,6 +44,7 @@ export const useAISpecs = () => {
.mobile(framework)
.electron(framework)
.linkPreview(framework)
.iconPicker(framework)
.codeBlockPreview(framework).value;
return manager.get('page');
@@ -0,0 +1,9 @@
import { type Framework } from '@toeverything/infra';
import { IconPickerService } from './services/icon-picker';
export { IconPickerService } from './services/icon-picker';
export function configureIconPickerModule(framework: Framework) {
framework.service(IconPickerService);
}
@@ -0,0 +1,69 @@
import {
type IconData as ComponentIconData,
IconPicker,
IconType,
uniReactRoot,
} from '@affine/component';
// Import the identifier for internal use
import {
type IconData,
type IconPickerOptions,
type IconPickerService as IIconPickerService,
} from '@blocksuite/affine-shared/services';
import { Service } from '@toeverything/infra';
import { html, type TemplateResult } from 'lit';
// Re-export types from BlockSuite shared services
export type {
IconData,
IconPickerOptions,
IconPickerService as IIconPickerService,
} from '@blocksuite/affine-shared/services';
export { IconPickerServiceIdentifier } from '@blocksuite/affine-shared/services';
// Convert between BlockSuite IconData and Component IconData
function convertToBlockSuiteIconData(
componentIconData: ComponentIconData
): IconData {
if (componentIconData.type === IconType.Emoji) {
return {
type: 'emoji',
value: componentIconData.unicode,
};
} else if (componentIconData.type === IconType.AffineIcon) {
return {
type: 'icon',
value: componentIconData.name,
};
}
// For other types, default to icon type
return {
type: 'icon',
value: 'default',
};
}
export class IconPickerService extends Service implements IIconPickerService {
public readonly iconPickerComponent =
uniReactRoot.createUniComponent(IconPicker);
renderIconPicker(options: IconPickerOptions): TemplateResult {
const element = document.createElement('div');
// Adapt the options to match IconPicker component's expected interface
const adaptedOptions = {
onSelect: options.onSelect
? (data?: ComponentIconData) => {
if (data && options.onSelect) {
const blockSuiteIconData = convertToBlockSuiteIconData(data);
options.onSelect(blockSuiteIconData);
}
}
: undefined,
onClose: options.onClose,
};
this.iconPickerComponent(element, adaptedOptions, () => {});
return html`${element}`;
}
}
@@ -33,6 +33,7 @@ import { configureFavoriteModule } from './favorite';
import { configureFeatureFlagModule } from './feature-flag';
import { configureGlobalContextModule } from './global-context';
import { configureI18nModule } from './i18n';
import { configureIconPickerModule } from './icon-picker';
import { configureImportClipperModule } from './import-clipper';
import { configureImportTemplateModule } from './import-template';
import { configureIntegrationModule } from './integration';
@@ -132,4 +133,5 @@ export function configureCommonModules(framework: Framework) {
configureCommentModule(framework);
configureDocSummaryModule(framework);
configurePaywallModule(framework);
configureIconPickerModule(framework);
}