feat(editor): unify block props api (#10888)

Closes: [BS-2707](https://linear.app/affine-design/issue/BS-2707/统一使用props获取和更新block-prop)
This commit is contained in:
Saul-Mirone
2025-03-16 05:48:34 +00:00
parent 8f9e5bf0aa
commit 26285f7dcb
193 changed files with 1019 additions and 891 deletions
@@ -287,7 +287,7 @@ const GenerateWithAIGroup: AIItemGroupConfig = {
return selectedModels.every(
model =>
matchModels(model, [ParagraphBlockModel, ListBlockModel]) &&
!model.type.startsWith('h')
!model.props.type.startsWith('h')
);
},
},
@@ -9,12 +9,13 @@ import {
EdgelessTextBlockModel,
EmbedSyncedDocModel,
ImageBlockModel,
type ImageBlockProps,
NoteBlockModel,
ShapeElementModel,
TextElementModel,
} from '@blocksuite/affine/model';
import { matchModels } from '@blocksuite/affine/shared/utils';
import { Slice } from '@blocksuite/affine/store';
import { type BlockModel, Slice } from '@blocksuite/affine/store';
import type { TemplateResult } from 'lit';
import { getContentFromSlice } from '../../utils';
@@ -87,8 +88,9 @@ export async function getContentFromSelected(
function isImageWithCaption(
el: ImageBlockModel
): el is RemoveUndefinedKey<ImageBlockModel, 'caption'> {
return el.caption !== undefined && el.caption.length !== 0;
): el is ImageBlockModel &
BlockModel<RemoveUndefinedKey<ImageBlockProps, 'caption'>> {
return el.props.caption !== undefined && el.props.caption.length !== 0;
}
const { notes, texts, shapes, images, edgelessTexts, embedSyncedDocs } =
@@ -96,7 +98,7 @@ export async function getContentFromSelected(
notes: NoteBlockModel[];
texts: TextElementModel[];
shapes: RemoveUndefinedKey<ShapeElementModel, 'text'>[];
images: RemoveUndefinedKey<ImageBlockModel, 'caption'>[];
images: RemoveUndefinedKey<ImageBlockProps, 'caption'>[];
edgelessTexts: EdgelessTextBlockModel[];
embedSyncedDocs: EmbedSyncedDocModel[];
}>(
@@ -108,7 +110,7 @@ export async function getContentFromSelected(
} else if (cur instanceof ShapeElementModel && isShapeWithText(cur)) {
pre.shapes.push(cur);
} else if (cur instanceof ImageBlockModel && isImageWithCaption(cur)) {
pre.images.push(cur);
pre.images.push(cur.props);
} else if (cur instanceof EdgelessTextBlockModel) {
pre.edgelessTexts.push(cur);
} else if (cur instanceof EmbedSyncedDocModel) {
@@ -18,7 +18,7 @@ export class AIChatBlockComponent extends BlockComponent<AIChatBlockModel> {
// Deserialize messages from JSON string and verify the type using zod
private readonly _deserializeChatMessages = computed(() => {
const messages = this.model.messages$.value;
const messages = this.model.props.messages$.value;
try {
const result = ChatMessagesSchema.safeParse(JSON.parse(messages));
if (result.success) {
@@ -9,8 +9,8 @@ export class EdgelessAIChatBlockComponent extends toGfxBlockComponent(
AIChatBlockComponent
) {
override renderGfxBlock() {
const bound = Bound.deserialize(this.model.xywh$.value);
const scale = this.model.scale$.value;
const bound = Bound.deserialize(this.model.props.xywh$.value);
const scale = this.model.props.scale$.value;
const width = bound.w / scale;
const height = bound.h / scale;
const style = {
@@ -12,7 +12,7 @@ class AIParagraphBlockWatcher extends LifeCycleWatcher {
super.mounted();
const service = this.std.get(ParagraphBlockService);
service.placeholderGenerator = model => {
if (model.type === 'text') {
if (model.props.type === 'text') {
return "Type '/' for commands, 'space' for AI";
}
@@ -25,7 +25,7 @@ class AIParagraphBlockWatcher extends LifeCycleWatcher {
h6: 'Heading 6',
quote: '',
};
return placeholders[model.type];
return placeholders[model.props.type];
};
}
}
@@ -44,11 +44,11 @@ export class AIChatBlockPeekView extends LitElement {
}
private get parentSessionId() {
return this.parentModel.sessionId;
return this.parentModel.props.sessionId;
}
private get historyMessagesString() {
return this.parentModel.messages;
return this.parentModel.props.messages;
}
private get parentChatBlockId() {
@@ -56,11 +56,11 @@ export class AIChatBlockPeekView extends LitElement {
}
private get parentRootDocId() {
return this.parentModel.rootDocId;
return this.parentModel.props.rootDocId;
}
private get parentRootWorkspaceId() {
return this.parentModel.rootWorkspaceId;
return this.parentModel.props.rootWorkspaceId;
}
private _textRendererOptions: TextRendererOptions = {};
@@ -145,7 +145,7 @@ export async function extractPageAll(
const blobs = await Promise.all(
blockModels.map(async s => {
if (s.flavour !== 'affine:image') return null;
const sourceId = (s as ImageBlockModel)?.sourceId;
const sourceId = (s as ImageBlockModel)?.props.sourceId;
if (!sourceId) return null;
const blob = await (sourceId ? host.doc.blobSync.get(sourceId) : null);
if (!blob) return null;
@@ -190,7 +190,7 @@ function getNoteBlockModels(doc: Store) {
.getBlocksByFlavour('affine:note')
.filter(
note =>
(note.model as NoteBlockModel).displayMode !==
(note.model as NoteBlockModel).props.displayMode !==
NoteDisplayMode.EdgelessOnly
)
.map(note => note.model as NoteBlockModel);
@@ -263,7 +263,7 @@ export const getSelectedImagesAsBlobs = async (host: EditorHost) => {
const blobs = await Promise.all(
data.selectedBlocks?.map(async b => {
const sourceId = (b.model as ImageBlockModel).sourceId;
const sourceId = (b.model as ImageBlockModel).props.sourceId;
if (!sourceId) return null;
const blob = await host.doc.blobSync.get(sourceId);
if (!blob) return null;
@@ -295,9 +295,9 @@ export const imageCustomInput = async (host: EditorHost) => {
const imageBlock = selectedElements[0];
if (!(imageBlock instanceof ImageBlockModel)) return;
if (!imageBlock.sourceId) return;
if (!imageBlock.props.sourceId) return;
const blob = await host.doc.blobSync.get(imageBlock.sourceId);
const blob = await host.doc.blobSync.get(imageBlock.props.sourceId);
if (!blob) return;
return {
@@ -16,9 +16,10 @@ export function patchForAttachmentEmbedViews(
di.override(AttachmentEmbedConfigIdentifier('pdf'), () => ({
name: 'pdf',
check: (model, maxFileSize) =>
model.type === 'application/pdf' && model.size <= maxFileSize,
model.props.type === 'application/pdf' &&
model.props.size <= maxFileSize,
action: model => {
const bound = Bound.deserialize(model.xywh);
const bound = Bound.deserialize(model.props.xywh);
bound.w = 537 + 24 + 2;
bound.h = 759 + 46 + 24 + 2;
model.doc.updateBlock(model, {
@@ -410,7 +410,7 @@ function createExternalLinkableToolbarConfig(
)?.model;
if (!model) return;
const { url } = model;
const { url } = model.props;
navigator.clipboard.writeText(url).catch(console.error);
toast(ctx.host, 'Copied link to clipboard');
@@ -523,7 +523,7 @@ function createOpenDocActionGroup(
return {
...action,
disabled: shouldOpenInActiveView
? component.model.pageId === ctx.store.id
? component.model.props.pageId === ctx.store.id
: false,
when:
allowed &&
@@ -590,7 +590,7 @@ const embedLinkedDocToolbarConfig = {
);
if (!model) return;
const { pageId, params } = model;
const { pageId, params } = model.props;
const url = ctx.std
.getOptional(GenerateDocUrlProvider)
@@ -625,7 +625,7 @@ const embedLinkedDocToolbarConfig = {
ctx.hide();
const model = component.model;
const doc = ctx.workspace.getDoc(model.pageId);
const doc = ctx.workspace.getDoc(model.props.pageId);
const abortController = new AbortController();
abortController.signal.onabort = () => ctx.show();
@@ -683,7 +683,7 @@ const embedSyncedDocToolbarConfig = {
);
if (!model) return;
const { pageId, params } = model;
const { pageId, params } = model.props;
const url = ctx.std
.getOptional(GenerateDocUrlProvider)
@@ -718,7 +718,7 @@ const embedSyncedDocToolbarConfig = {
ctx.hide();
const model = component.model;
const doc = ctx.workspace.getDoc(model.pageId);
const doc = ctx.workspace.getDoc(model.props.pageId);
const abortController = new AbortController();
abortController.signal.onabort = () => ctx.show();
@@ -920,7 +920,7 @@ const embedIframeToolbarConfig = {
)?.model;
if (!model) return;
const { url } = model;
const { url } = model.props;
navigator.clipboard.writeText(url).catch(console.error);
toast(ctx.host, 'Copied link to clipboard');
@@ -86,7 +86,7 @@ class MobileSpecsPatches extends LifeCycleWatcher {
h6: 'Heading 6',
quote: '',
};
return placeholders[model.type];
return placeholders[model.props.type];
};
}
}
@@ -25,7 +25,7 @@ import * as styles from './edgeless-note-header.css';
const EdgelessNoteToggleButton = ({ note }: { note: NoteBlockModel }) => {
const t = useI18n();
const [collapsed, setCollapsed] = useState(note.edgeless.collapse);
const [collapsed, setCollapsed] = useState(note.props.edgeless.collapse);
const editor = useService(EditorService).editor;
const editorContainer = useLiveData(editor.editorContainer$);
const gfx = editorContainer?.std.get(GfxControllerIdentifier);
@@ -39,7 +39,7 @@ const EdgelessNoteToggleButton = ({ note }: { note: NoteBlockModel }) => {
);
useEffect(() => {
return note.edgeless$.subscribe(({ collapse, collapsedHeight }) => {
return note.props.edgeless$.subscribe(({ collapse, collapsedHeight }) => {
if (
collapse &&
collapsedHeight &&
@@ -50,7 +50,7 @@ const EdgelessNoteToggleButton = ({ note }: { note: NoteBlockModel }) => {
setCollapsed(false);
}
});
}, [note.edgeless$]);
}, [note.props.edgeless$]);
useEffect(() => {
if (!gfx) return;
@@ -60,7 +60,7 @@ const EdgelessNoteToggleButton = ({ note }: { note: NoteBlockModel }) => {
const dispose = selection.slots.updated.subscribe(() => {
if (selection.has(note.id) && selection.editing) {
note.doc.transact(() => {
note.edgeless.collapse = false;
note.props.edgeless.collapse = false;
});
}
});
@@ -74,13 +74,13 @@ const EdgelessNoteToggleButton = ({ note }: { note: NoteBlockModel }) => {
});
note.doc.transact(() => {
if (collapsed) {
note.edgeless.collapse = false;
note.props.edgeless.collapse = false;
} else {
const bound = Bound.deserialize(note.xywh);
bound.h = styles.headerHeight * (note.edgeless.scale ?? 1);
note.xywh = bound.serialize();
note.edgeless.collapse = true;
note.edgeless.collapsedHeight = styles.headerHeight;
const bound = Bound.deserialize(note.props.xywh);
bound.h = styles.headerHeight * (note.props.edgeless.scale ?? 1);
note.props.xywh = bound.serialize();
note.props.edgeless.collapse = true;
note.props.edgeless.collapsedHeight = styles.headerHeight;
gfx?.selection.clear();
}
});