mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-18 06:47:02 +08:00
refactor(web): insert blew action (#10722)
### TL;DR Refactor the insert below functionality to work with page mode and edgeless mode * Page Mode - Insert content below the current selection - If nothing selected, insert content below the last block * EdgeLess Mode - If no note block is currently selected, create the content as a new note block. - Otherwise, insert content into the selected note Close BS-2760 ### What changed? - Created separate insert handlers for page and edgeless modes with context-aware behavior - Added support for inserting content when nothing is selected by targeting the last content block - Added special handling for edgeless mode to support inserting below selected note blocks - Removed the "Replace selection" action and consolidated insert functionality - Optimized the clickable area of the action button
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import { ChatHistoryOrder } from '@affine/graphql';
|
||||
import {
|
||||
BlockSelection,
|
||||
type BlockComponent,
|
||||
type BlockSelection,
|
||||
type BlockStdScope,
|
||||
type EditorHost,
|
||||
TextSelection,
|
||||
type TextSelection,
|
||||
} from '@blocksuite/affine/block-std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/affine/block-std/gfx';
|
||||
import { EdgelessCRUDIdentifier } from '@blocksuite/affine/blocks/surface';
|
||||
@@ -14,11 +15,15 @@ import {
|
||||
} from '@blocksuite/affine/global/gfx';
|
||||
import {
|
||||
type DocMode,
|
||||
NoteBlockModel,
|
||||
NoteDisplayMode,
|
||||
ParagraphBlockModel,
|
||||
} from '@blocksuite/affine/model';
|
||||
import { RefNodeSlotsProvider } from '@blocksuite/affine/rich-text';
|
||||
import { getSelectedBlocksCommand } from '@blocksuite/affine/shared/commands';
|
||||
import {
|
||||
getFirstBlockCommand,
|
||||
getLastBlockCommand,
|
||||
getSelectedBlocksCommand,
|
||||
} from '@blocksuite/affine/shared/commands';
|
||||
import type { ImageSelection } from '@blocksuite/affine/shared/selection';
|
||||
import {
|
||||
DocModeProvider,
|
||||
@@ -26,14 +31,12 @@ import {
|
||||
NotificationProvider,
|
||||
TelemetryProvider,
|
||||
} from '@blocksuite/affine/shared/services';
|
||||
import { matchModels } from '@blocksuite/affine/shared/utils';
|
||||
import type { Store } from '@blocksuite/affine/store';
|
||||
import {
|
||||
BlockIcon,
|
||||
InsertBleowIcon as InsertBelowIcon,
|
||||
LinkedPageIcon,
|
||||
PageIcon,
|
||||
ReplaceIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import type { TemplateResult } from 'lit';
|
||||
|
||||
@@ -41,7 +44,7 @@ import { insertFromMarkdown } from '../../utils';
|
||||
import type { ChatMessage } from '../blocks';
|
||||
import { AIProvider, type AIUserInfo } from '../provider';
|
||||
import { reportResponse } from '../utils/action-reporter';
|
||||
import { insertBelow, replace } from '../utils/editor-actions';
|
||||
import { insertBelow } from '../utils/editor-actions';
|
||||
|
||||
type Selections = {
|
||||
text?: TextSelection;
|
||||
@@ -201,70 +204,63 @@ export function promptDocTitle(host: EditorHost, autofill?: string) {
|
||||
});
|
||||
}
|
||||
|
||||
const REPLACE_SELECTION = {
|
||||
icon: ReplaceIcon({ width: '20px', height: '20px' }),
|
||||
title: 'Replace selection',
|
||||
showWhen: (host: EditorHost) => {
|
||||
if (host.std.store.readonly$.value) {
|
||||
return false;
|
||||
}
|
||||
const textSelection = host.selection.find(TextSelection);
|
||||
const blockSelections = host.selection.filter(BlockSelection);
|
||||
if (
|
||||
(!textSelection || textSelection.from.length === 0) &&
|
||||
blockSelections?.length === 0
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
toast: 'Successfully replaced',
|
||||
handler: async (
|
||||
host: EditorHost,
|
||||
content: string,
|
||||
currentSelections: Selections
|
||||
) => {
|
||||
const currentTextSelection = currentSelections.text;
|
||||
const currentBlockSelections = currentSelections.blocks;
|
||||
const [_, data] = host.command.exec(getSelectedBlocksCommand, {
|
||||
/**
|
||||
* Get insert below block based on current selections
|
||||
* @param host Editor host
|
||||
* @param currentSelections Current selections
|
||||
* @returns Selected blocks and selection state
|
||||
*/
|
||||
async function getInsertBelowBlock(
|
||||
host: EditorHost,
|
||||
currentSelections: Selections
|
||||
): Promise<BlockComponent | null> {
|
||||
const currentTextSelection = currentSelections.text;
|
||||
const currentBlockSelections = currentSelections.blocks;
|
||||
const currentImageSelections = currentSelections.images;
|
||||
|
||||
const [_, { selectedBlocks: blocks }] = host.command.exec(
|
||||
getSelectedBlocksCommand,
|
||||
{
|
||||
currentTextSelection,
|
||||
currentBlockSelections,
|
||||
});
|
||||
if (!data.selectedBlocks) return false;
|
||||
|
||||
reportResponse('result:replace');
|
||||
|
||||
if (currentTextSelection) {
|
||||
const { doc } = host;
|
||||
const block = doc.getBlock(currentTextSelection.blockId);
|
||||
if (matchModels(block?.model ?? null, [ParagraphBlockModel])) {
|
||||
block?.model.text?.replace(
|
||||
currentTextSelection.from.index,
|
||||
currentTextSelection.from.length,
|
||||
content
|
||||
);
|
||||
return true;
|
||||
}
|
||||
currentImageSelections,
|
||||
}
|
||||
);
|
||||
|
||||
await replace(
|
||||
host,
|
||||
content,
|
||||
data.selectedBlocks[0],
|
||||
data.selectedBlocks.map(block => block.model),
|
||||
currentTextSelection
|
||||
);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
if (blocks && blocks.length) {
|
||||
return blocks[blocks.length - 1];
|
||||
}
|
||||
|
||||
const INSERT_BELOW = {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base handler for inserting content below the block
|
||||
* @param host Editor host
|
||||
* @param content Content to insert
|
||||
* @param block block
|
||||
* @returns Whether insertion was successful
|
||||
*/
|
||||
async function insertBelowBlock(
|
||||
host: EditorHost,
|
||||
content: string,
|
||||
block: BlockComponent | null
|
||||
): Promise<boolean> {
|
||||
if (!block) return false;
|
||||
|
||||
reportResponse('result:insert');
|
||||
await insertBelow(host, content, block);
|
||||
return true;
|
||||
}
|
||||
|
||||
const PAGE_INSERT = {
|
||||
icon: InsertBelowIcon({ width: '20px', height: '20px' }),
|
||||
title: 'Insert below',
|
||||
title: 'Insert',
|
||||
showWhen: (host: EditorHost) => {
|
||||
if (host.std.store.readonly$.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
toast: 'Successfully inserted',
|
||||
@@ -273,22 +269,68 @@ const INSERT_BELOW = {
|
||||
content: string,
|
||||
currentSelections: Selections
|
||||
) => {
|
||||
const currentTextSelection = currentSelections.text;
|
||||
const currentBlockSelections = currentSelections.blocks;
|
||||
const currentImageSelections = currentSelections.images;
|
||||
const [_, data] = host.command.exec(getSelectedBlocksCommand, {
|
||||
currentTextSelection,
|
||||
currentBlockSelections,
|
||||
currentImageSelections,
|
||||
});
|
||||
if (!data.selectedBlocks) return false;
|
||||
reportResponse('result:insert');
|
||||
await insertBelow(
|
||||
host,
|
||||
content,
|
||||
data.selectedBlocks[data.selectedBlocks?.length - 1]
|
||||
);
|
||||
return true;
|
||||
const block = await getInsertBelowBlock(host, currentSelections);
|
||||
|
||||
const isNothingSelected = !block;
|
||||
|
||||
// In page mode, if nothing is selected, use the last content block
|
||||
if (isNothingSelected) {
|
||||
const [_, { firstBlock: noteBlock }] = host.command.exec(
|
||||
getFirstBlockCommand,
|
||||
{
|
||||
flavour: 'affine:note',
|
||||
}
|
||||
);
|
||||
|
||||
const lastChild = noteBlock?.lastChild();
|
||||
const lastBlock = lastChild ? host.std.view.getBlock(lastChild.id) : null;
|
||||
|
||||
return insertBelowBlock(host, content, lastBlock);
|
||||
}
|
||||
|
||||
return insertBelowBlock(host, content, block);
|
||||
},
|
||||
};
|
||||
|
||||
const EDGELESS_INSERT = {
|
||||
...PAGE_INSERT,
|
||||
handler: async (
|
||||
host: EditorHost,
|
||||
content: string,
|
||||
currentSelections: Selections
|
||||
): Promise<boolean> => {
|
||||
const block = await getInsertBelowBlock(host, currentSelections);
|
||||
|
||||
const isNothingSelected = !block;
|
||||
|
||||
// In edgeless mode, handle special cases
|
||||
if (isNothingSelected) {
|
||||
const gfx = host.std.get(GfxControllerIdentifier);
|
||||
const selectedElements = gfx.selection.selectedElements;
|
||||
const isOnlyOneNoteSelected =
|
||||
selectedElements.length === 1 &&
|
||||
selectedElements[0] instanceof NoteBlockModel;
|
||||
|
||||
if (isOnlyOneNoteSelected) {
|
||||
// Insert into selected note
|
||||
const [_, { lastBlock: lastBlockModel }] = host.command.exec(
|
||||
getLastBlockCommand,
|
||||
{
|
||||
root: selectedElements[0] as NoteBlockModel,
|
||||
}
|
||||
);
|
||||
|
||||
const lastBlock = lastBlockModel
|
||||
? host.std.view.getBlock(lastBlockModel.id)
|
||||
: null;
|
||||
return insertBelowBlock(host, content, lastBlock);
|
||||
} else {
|
||||
// Create a new note
|
||||
return !!(await ADD_TO_EDGELESS_AS_NOTE.handler(host, content));
|
||||
}
|
||||
}
|
||||
|
||||
return insertBelowBlock(host, content, block);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -397,7 +439,7 @@ const ADD_TO_EDGELESS_AS_NOTE = {
|
||||
return true;
|
||||
},
|
||||
toast: 'New note created',
|
||||
handler: async (host: EditorHost, content: string) => {
|
||||
handler: async (host: EditorHost, content: string): Promise<boolean> => {
|
||||
reportResponse('result:add-note');
|
||||
const { doc } = host;
|
||||
|
||||
@@ -535,16 +577,14 @@ const CREATE_AS_LINKED_DOC = {
|
||||
},
|
||||
};
|
||||
|
||||
const CommonActions: ChatAction[] = [REPLACE_SELECTION, INSERT_BELOW];
|
||||
|
||||
export const PageEditorActions = [
|
||||
...CommonActions,
|
||||
PAGE_INSERT,
|
||||
CREATE_AS_DOC,
|
||||
SAVE_CHAT_TO_BLOCK_ACTION,
|
||||
];
|
||||
|
||||
export const EdgelessEditorActions = [
|
||||
...CommonActions,
|
||||
EDGELESS_INSERT,
|
||||
ADD_TO_EDGELESS_AS_NOTE,
|
||||
SAVE_CHAT_TO_BLOCK_ACTION,
|
||||
];
|
||||
|
||||
@@ -115,43 +115,45 @@ export class ChatActionList extends LitElement {
|
||||
actions.filter(action => action.showWhen(host)),
|
||||
action => action.title,
|
||||
action => {
|
||||
return html`<div class="action">
|
||||
return html`<div
|
||||
class="action"
|
||||
@click=${async () => {
|
||||
if (
|
||||
action.title === 'Insert below' &&
|
||||
this._selectionValue.length === 1 &&
|
||||
this._selectionValue[0].type === 'database'
|
||||
) {
|
||||
const element = this.host.view.getBlock(
|
||||
this._selectionValue[0].blockId
|
||||
);
|
||||
if (!element) return;
|
||||
await insertBelow(host, content, element);
|
||||
return;
|
||||
}
|
||||
const currentSelections = {
|
||||
text: this._currentTextSelection,
|
||||
blocks: this._currentBlockSelections,
|
||||
images: this._currentImageSelections,
|
||||
};
|
||||
const sessionId = await this.getSessionId();
|
||||
const success = await action.handler(
|
||||
host,
|
||||
content,
|
||||
currentSelections,
|
||||
sessionId,
|
||||
messageId
|
||||
);
|
||||
if (success) {
|
||||
this.host.std.getOptional(NotificationProvider)?.notify({
|
||||
title: action.toast,
|
||||
accent: 'success',
|
||||
onClose: function (): void {},
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
${action.icon}
|
||||
<div
|
||||
@click=${async () => {
|
||||
if (
|
||||
action.title === 'Insert below' &&
|
||||
this._selectionValue.length === 1 &&
|
||||
this._selectionValue[0].type === 'database'
|
||||
) {
|
||||
const element = this.host.view.getBlock(
|
||||
this._selectionValue[0].blockId
|
||||
);
|
||||
if (!element) return;
|
||||
await insertBelow(host, content, element);
|
||||
return;
|
||||
}
|
||||
const currentSelections = {
|
||||
text: this._currentTextSelection,
|
||||
blocks: this._currentBlockSelections,
|
||||
images: this._currentImageSelections,
|
||||
};
|
||||
const sessionId = await this.getSessionId();
|
||||
const success = await action.handler(
|
||||
host,
|
||||
content,
|
||||
currentSelections,
|
||||
sessionId,
|
||||
messageId
|
||||
);
|
||||
if (success) {
|
||||
this.host.std.getOptional(NotificationProvider)?.notify({
|
||||
title: action.toast,
|
||||
accent: 'success',
|
||||
onClose: function (): void {},
|
||||
});
|
||||
}
|
||||
}}
|
||||
data-testid="action-${action.title
|
||||
.toLowerCase()
|
||||
.replaceAll(' ', '-')}"
|
||||
|
||||
Reference in New Issue
Block a user