diff --git a/blocksuite/affine/blocks/embed/src/embed-linked-doc-block/configs/toolbar.ts b/blocksuite/affine/blocks/embed/src/embed-linked-doc-block/configs/toolbar.ts index e87b820fa6..6c4ebad8dd 100644 --- a/blocksuite/affine/blocks/embed/src/embed-linked-doc-block/configs/toolbar.ts +++ b/blocksuite/affine/blocks/embed/src/embed-linked-doc-block/configs/toolbar.ts @@ -12,6 +12,7 @@ import { import { ActionPlacement, DocDisplayMetaProvider, + EditorSettingProvider, type LinkEventType, type OpenDocMode, type ToolbarAction, @@ -33,7 +34,7 @@ import { ExpandFullIcon, OpenInNewIcon, } from '@blocksuite/icons/lit'; -import { BlockFlavourIdentifier } from '@blocksuite/std'; +import { BlockFlavourIdentifier, isGfxBlockComponent } from '@blocksuite/std'; import { type ExtensionType, Slice } from '@blocksuite/store'; import { computed, signal } from '@preact/signals-core'; import { html } from 'lit'; @@ -213,6 +214,15 @@ const conversionsActionGroup = { }, run(ctx) { const block = ctx.getCurrentBlockByType(EmbedLinkedDocBlockComponent); + + if (isGfxBlockComponent(block)) { + const editorSetting = ctx.std.getOptional(EditorSettingProvider); + editorSetting?.set?.( + 'docDropCanvasPreferView', + 'affine:embed-synced-doc' + ); + } + block?.convertToEmbed(); ctx.track('SelectedView', { diff --git a/blocksuite/affine/blocks/embed/src/embed-synced-doc-block/configs/toolbar.ts b/blocksuite/affine/blocks/embed/src/embed-synced-doc-block/configs/toolbar.ts index 1bceb29dc3..388c0a245f 100644 --- a/blocksuite/affine/blocks/embed/src/embed-synced-doc-block/configs/toolbar.ts +++ b/blocksuite/affine/blocks/embed/src/embed-synced-doc-block/configs/toolbar.ts @@ -3,6 +3,7 @@ import { EditorChevronDown } from '@blocksuite/affine-components/toolbar'; import { EmbedSyncedDocModel } from '@blocksuite/affine-model'; import { ActionPlacement, + EditorSettingProvider, type LinkEventType, type OpenDocMode, type ToolbarAction, @@ -21,7 +22,7 @@ import { ExpandFullIcon, OpenInNewIcon, } from '@blocksuite/icons/lit'; -import { BlockFlavourIdentifier } from '@blocksuite/std'; +import { BlockFlavourIdentifier, isGfxBlockComponent } from '@blocksuite/std'; import { type ExtensionType, Slice } from '@blocksuite/store'; import { computed, signal } from '@preact/signals-core'; import { html } from 'lit'; @@ -30,7 +31,6 @@ import { keyed } from 'lit/directives/keyed.js'; import { repeat } from 'lit/directives/repeat.js'; import { EmbedSyncedDocBlockComponent } from '../embed-synced-doc-block'; - const trackBaseProps = { category: 'linked doc', type: 'embed view', @@ -142,6 +142,14 @@ const conversionsActionGroup = { label: 'Card view', run(ctx) { const block = ctx.getCurrentBlockByType(EmbedSyncedDocBlockComponent); + if (isGfxBlockComponent(block)) { + const editorSetting = ctx.std.getOptional(EditorSettingProvider); + editorSetting?.set?.( + 'docDropCanvasPreferView', + 'affine:embed-linked-doc' + ); + } + block?.convertToCard(); ctx.track('SelectedView', { diff --git a/blocksuite/affine/blocks/embed/src/embed-synced-doc-block/embed-synced-doc-block.ts b/blocksuite/affine/blocks/embed/src/embed-synced-doc-block/embed-synced-doc-block.ts index c0b4c87420..c31d6e4dad 100644 --- a/blocksuite/affine/blocks/embed/src/embed-synced-doc-block/embed-synced-doc-block.ts +++ b/blocksuite/affine/blocks/embed/src/embed-synced-doc-block/embed-synced-doc-block.ts @@ -117,9 +117,9 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent { - const config = this.std.getOptional(EditorSettingProvider); + const config = this.std.getOptional(EditorSettingProvider)?.setting$; const state = ctx.get('defaultState'); const e = state.event as WheelEvent; const edgelessScrollZoom = config?.peek().edgelessScrollZoom ?? false; diff --git a/blocksuite/affine/blocks/root/src/edgeless/edgeless-root-preview-block.ts b/blocksuite/affine/blocks/root/src/edgeless/edgeless-root-preview-block.ts index e56c4bd873..a69115f4be 100644 --- a/blocksuite/affine/blocks/root/src/edgeless/edgeless-root-preview-block.ts +++ b/blocksuite/affine/blocks/root/src/edgeless/edgeless-root-preview-block.ts @@ -169,7 +169,7 @@ export class EdgelessRootPreviewBlockComponent extends BlockComponent< } private get _disableScheduleUpdate() { - const editorSetting = this.std.getOptional(EditorSettingProvider); + const editorSetting = this.std.getOptional(EditorSettingProvider)?.setting$; return editorSetting?.peek().edgelessDisableScheduleUpdate ?? false; } diff --git a/blocksuite/affine/shared/src/services/edit-props-store.ts b/blocksuite/affine/shared/src/services/edit-props-store.ts index cea07c55d4..36510ee68b 100644 --- a/blocksuite/affine/shared/src/services/edit-props-store.ts +++ b/blocksuite/affine/shared/src/services/edit-props-store.ts @@ -98,7 +98,9 @@ export class EditPropsStore extends LifeCycleWatcher { ); this.lastProps$ = computed(() => { - const editorSetting$ = this.std.getOptional(EditorSettingProvider); + const editorSetting$ = this.std.getOptional( + EditorSettingProvider + )?.setting$; const nextProps = mergeWith( clonedeep(initProps), editorSetting$?.value, diff --git a/blocksuite/affine/shared/src/services/editor-setting-service.ts b/blocksuite/affine/shared/src/services/editor-setting-service.ts index 4e95b19bcd..e01f399ea9 100644 --- a/blocksuite/affine/shared/src/services/editor-setting-service.ts +++ b/blocksuite/affine/shared/src/services/editor-setting-service.ts @@ -10,21 +10,32 @@ export const GeneralSettingSchema = z .object({ edgelessScrollZoom: z.boolean().default(false), edgelessDisableScheduleUpdate: z.boolean().default(false), + docDropCanvasPreferView: z + .enum(['affine:embed-linked-doc', 'affine:embed-synced-doc']) + .default('affine:embed-synced-doc'), }) .merge(NodePropsSchema); export type EditorSetting = z.infer; -export const EditorSettingProvider = createIdentifier< - Signal> ->('AffineEditorSettingProvider'); +export interface EditorSettingService { + setting$: Signal>; + set?: ( + key: keyof EditorSetting, + value: EditorSetting[keyof EditorSetting] + ) => void; +} + +export const EditorSettingProvider = createIdentifier( + 'AffineEditorSettingProvider' +); export function EditorSettingExtension( - signal: Signal> + service: EditorSettingService ): ExtensionType { return { setup: di => { - di.addImpl(EditorSettingProvider, () => signal); + di.addImpl(EditorSettingProvider, () => service); }, }; } diff --git a/blocksuite/affine/widgets/drag-handle/src/helpers/preview-helper.ts b/blocksuite/affine/widgets/drag-handle/src/helpers/preview-helper.ts index 50aceaeef6..f1037f675b 100644 --- a/blocksuite/affine/widgets/drag-handle/src/helpers/preview-helper.ts +++ b/blocksuite/affine/widgets/drag-handle/src/helpers/preview-helper.ts @@ -66,14 +66,14 @@ export class PreviewHelper { blockIds = blockIds.slice(); const docModeService = std.get(DocModeProvider); - const editorSetting = std.get(EditorSettingProvider).peek(); + const editorSetting = std.get(EditorSettingProvider); const query = this._calculateQuery(blockIds as string[]); const store = widget.doc.doc.getStore({ query }); const previewSpec = SpecProvider._.getSpec('preview:page'); - const settingSignal = signal({ ...editorSetting }); + const settingSignal = signal({ ...editorSetting.setting$.peek() }); const extensions = [ DocModeExtension(docModeService), - EditorSettingExtension(settingSignal), + EditorSettingExtension({ setting$: settingSignal }), { setup(di) { di.override( diff --git a/blocksuite/playground/apps/starter/utils/extensions.ts b/blocksuite/playground/apps/starter/utils/extensions.ts index e0a4bb21fd..8a44f24c38 100644 --- a/blocksuite/playground/apps/starter/utils/extensions.ts +++ b/blocksuite/playground/apps/starter/utils/extensions.ts @@ -25,7 +25,9 @@ export function getTestCommonExtensions( ): ExtensionType[] { return [ FontConfigExtension(CommunityCanvasTextFonts), - EditorSettingExtension(mockEditorSetting()), + EditorSettingExtension({ + setting$: mockEditorSetting(), + }), ParseDocUrlExtension(mockParseDocUrlService(editor.doc.workspace)), { setup: di => { diff --git a/packages/frontend/core/src/blocksuite/extensions/editor-config/index.ts b/packages/frontend/core/src/blocksuite/extensions/editor-config/index.ts index 145808b8e0..34af7241bb 100644 --- a/packages/frontend/core/src/blocksuite/extensions/editor-config/index.ts +++ b/packages/frontend/core/src/blocksuite/extensions/editor-config/index.ts @@ -22,7 +22,11 @@ export function getEditorConfigExtension( const baseUrl = workspaceServerService.server?.baseUrl ?? location.origin; return [ - EditorSettingExtension(editorSettingService.editorSetting.settingSignal), + EditorSettingExtension({ + // eslint-disable-next-line rxjs/finnish + setting$: editorSettingService.editorSetting.settingSignal, + set: (k, v) => editorSettingService.editorSetting.set(k, v), + }), DatabaseConfigExtension(createDatabaseOptionsConfig(framework)), LinkedWidgetConfigExtension(createLinkedWidgetConfig(framework)), ToolbarMoreMenuConfigExtension(createToolbarMoreMenuConfig(framework)), diff --git a/packages/frontend/core/src/modules/dnd/index.ts b/packages/frontend/core/src/modules/dnd/index.ts index fd9729ecef..8b6d798b23 100644 --- a/packages/frontend/core/src/modules/dnd/index.ts +++ b/packages/frontend/core/src/modules/dnd/index.ts @@ -1,11 +1,12 @@ import { type Framework } from '@toeverything/infra'; import { DocsService } from '../doc'; +import { EditorSettingService } from '../editor-setting'; import { WorkspaceScope, WorkspaceService } from '../workspace'; import { DndService } from './services'; export function configureDndModule(framework: Framework) { framework .scope(WorkspaceScope) - .service(DndService, [DocsService, WorkspaceService]); + .service(DndService, [DocsService, WorkspaceService, EditorSettingService]); } diff --git a/packages/frontend/core/src/modules/dnd/services/index.ts b/packages/frontend/core/src/modules/dnd/services/index.ts index 0c6170dd4d..77faa31b11 100644 --- a/packages/frontend/core/src/modules/dnd/services/index.ts +++ b/packages/frontend/core/src/modules/dnd/services/index.ts @@ -1,6 +1,7 @@ import { type ExternalGetDataFeedbackArgs, type fromExternalData, + type MonitorDragEvent, monitorForElements, type MonitorGetFeedback, type toExternalData, @@ -16,6 +17,7 @@ import type { DragBlockPayload } from '@blocksuite/affine/widgets/drag-handle'; import { Service } from '@toeverything/infra'; import type { DocsService } from '../../doc'; +import type { EditorSettingService } from '../../editor-setting'; import { resolveLinkToDoc } from '../../navigation'; import type { WorkspaceService } from '../../workspace'; @@ -31,7 +33,8 @@ type MixedDNDData = AffineDNDData & { export class DndService extends Service { constructor( private readonly docsService: DocsService, - private readonly workspaceService: WorkspaceService + private readonly workspaceService: WorkspaceService, + private readonly editorSettingService: EditorSettingService ) { super(); @@ -130,6 +133,43 @@ export class DndService extends Service { ); } + function getBSDropTarget(args: MonitorDragEvent) { + for (const target of args.location.current.dropTargets) { + const { tagName } = target.element; + if (['AFFINE-EDGELESS-NOTE', 'AFFINE-NOTE'].includes(tagName)) + return 'note'; + if (tagName === 'AFFINE-EDGELESS-ROOT') return 'canvas'; + } + return 'other'; + } + + const changeDocCardView = (args: MonitorDragEvent) => { + if (args.source.data.from?.at === 'blocksuite-editor') return; + + const dropTarget = getBSDropTarget(args); + if (dropTarget === 'other') return; + + const flavour = + dropTarget === 'canvas' + ? this.editorSettingService.editorSetting.docDropCanvasPreferView + .value + : 'affine:embed-linked-doc'; + + const { entity, bsEntity } = args.source.data; + if (!entity || !bsEntity) return; + + const dndAPI = this.getBlocksuiteDndAPI(); + if (!dndAPI) return; + + const snapshotSlice = dndAPI.fromEntity({ + docId: entity.id, + flavour, + }); + if (!snapshotSlice) return; + + bsEntity.snapshot = snapshotSlice; + }; + this.disposables.push( monitorForElements({ canMonitor: (args: MonitorGetFeedback) => { @@ -144,6 +184,9 @@ export class DndService extends Service { } return false; }, + onDropTargetChange: (args: MonitorDragEvent) => { + changeDocCardView(args); + }, }) ); } diff --git a/tests/affine-local/e2e/drag-page.spec.ts b/tests/affine-local/e2e/drag-page.spec.ts index 825da065ad..384d011d96 100644 --- a/tests/affine-local/e2e/drag-page.spec.ts +++ b/tests/affine-local/e2e/drag-page.spec.ts @@ -1,9 +1,15 @@ import { test } from '@affine-test/kit/playwright'; +import { + clickEdgelessModeButton, + locateToolbar, +} from '@affine-test/kit/utils/editor'; +import { pressBackspace, pressEnter } from '@affine-test/kit/utils/keyboard'; import { openHomePage } from '@affine-test/kit/utils/load-page'; import { clickNewPageButton, createLinkedPage, dragTo, + type, waitForEditorLoad, } from '@affine-test/kit/utils/page-logic'; import { clickSideBarAllPageButton } from '@affine-test/kit/utils/sidebar'; @@ -293,7 +299,7 @@ test('drag a page card block to another page', async ({ page }) => { ); }); -test('drag a favourite page into blocksuite', async ({ page }) => { +test('drag a favourite page into note on page mode', async ({ page }) => { await clickNewPageButton(page, 'hi from page'); await page.getByTestId('pin-button').click(); const pageId = getCurrentDocIdFromUrl(page); @@ -313,3 +319,93 @@ test('drag a favourite page into blocksuite', async ({ page }) => { 'hi from page' ); }); + +test('drag a favourite page into canvas on edgeless mode', async ({ page }) => { + await clickNewPageButton(page, 'hi from page'); + await pressEnter(page); + await type(page, 'Hello world'); + await page.getByTestId('pin-button').click(); + const pageId = getCurrentDocIdFromUrl(page); + const item = page + .getByTestId(`explorer-favorites`) + .locator(`[data-testid="explorer-doc-${pageId}"]`); + await expect(item).toBeVisible(); + + await clickNewPageButton(page, 'edgeless page'); + await clickEdgelessModeButton(page); + + // drag item into blocksuite editor + await dragTo(page, item, page.locator('affine-edgeless-root').first()); + + const embedDoc = page.locator('affine-embed-edgeless-synced-doc-block'); + await expect(embedDoc).toContainText('Hello world'); + await embedDoc.click(); + + const toolbar = locateToolbar(page); + const switchViewButton = toolbar.getByLabel('Switch view'); + await switchViewButton.click(); + await toolbar.getByLabel('Card view').click(); // this will record the view as preferred view + const cardDoc = page.locator('affine-embed-edgeless-linked-doc-block'); + await expect(embedDoc).toBeHidden(); + await expect(cardDoc).toBeVisible(); + + await pressBackspace(page); + await expect(embedDoc).toBeHidden(); + await expect(cardDoc).toBeHidden(); + + await dragTo(page, item, page.locator('affine-edgeless-root').first()); + await expect(cardDoc).toBeVisible(); + await expect(embedDoc).toBeHidden(); + + await page.reload(); + await cardDoc.click(); + await pressBackspace(page); + + await dragTo(page, item, page.locator('affine-edgeless-root').first()); + await expect(cardDoc, 'prefer view should be persistence').toBeVisible(); + await expect(embedDoc).toBeHidden(); +}); + +test('drag a favourite page into note on edgeless mode', async ({ page }) => { + await clickNewPageButton(page, 'hi from page'); + await pressEnter(page); + await type(page, 'Hello world'); + await page.getByTestId('pin-button').click(); + const pageId = getCurrentDocIdFromUrl(page); + const item = page + .getByTestId(`explorer-favorites`) + .locator(`[data-testid="explorer-doc-${pageId}"]`); + await expect(item).toBeVisible(); + + await clickNewPageButton(page, 'edgeless page'); + await clickEdgelessModeButton(page); + + const edgelessNote = page.locator('affine-edgeless-note'); + await dragTo(page, item, edgelessNote); + + const embedDocInNote = edgelessNote.locator('affine-embed-synced-doc-block'); + const cardDocInNote = edgelessNote.locator('affine-embed-linked-doc-block'); + + await expect(embedDocInNote).toBeHidden(); + await expect(cardDocInNote).toBeVisible(); + + await edgelessNote.click({ delay: 100 }); + await edgelessNote.click({ delay: 100 }); + await cardDocInNote.click(); + + const toolbar = locateToolbar(page); + const switchViewButton = toolbar.getByLabel('Switch view'); + await switchViewButton.click(); + await toolbar.getByLabel('Embed view').click(); // this should not record the view as preferred view + await expect(embedDocInNote).toBeVisible(); + await expect(cardDocInNote).toBeHidden(); + + await pressBackspace(page); + + await dragTo(page, item, edgelessNote); + await expect( + embedDocInNote, + 'prefer view should only be available in canvas' + ).toBeHidden(); + await expect(cardDocInNote).toBeVisible(); +});