feat(editor): simple table block (#9740)

close: BS-2122, BS-2125, BS-2124, BS-2420, PD-2073, BS-2126, BS-2469, BS-2470, BS-2478, BS-2471
This commit is contained in:
zzj3720
2025-01-24 10:07:57 +00:00
parent 3f4311ff1c
commit 5a5779c05a
61 changed files with 3577 additions and 381 deletions
@@ -2,7 +2,11 @@ import {
InlineDeltaToMarkdownAdapterExtensions,
MarkdownInlineToDeltaAdapterExtensions,
} from '@blocksuite/affine-components/rich-text';
import { DefaultTheme, NoteDisplayMode } from '@blocksuite/affine-model';
import {
DefaultTheme,
NoteDisplayMode,
TableModelFlavour,
} from '@blocksuite/affine-model';
import { MarkdownAdapter } from '@blocksuite/affine-shared/adapters';
import { Container } from '@blocksuite/global/di';
import type {
@@ -3529,91 +3533,76 @@ bbb
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
background: {
dark: '#000000',
light: '#ffffff',
},
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
displayMode: 'both',
},
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:database',
flavour: TableModelFlavour,
props: {
views: [
{
id: 'matchesReplaceMap[2]',
name: 'Table View',
mode: 'table',
columns: [],
filter: {
type: 'group',
op: 'and',
conditions: [],
},
header: {
titleColumn: 'matchesReplaceMap[9]',
iconColumn: 'type',
},
columns: {
'matchesReplaceMap[3]': {
columnId: 'matchesReplaceMap[3]',
order: 'matchesReplaceMap[4]',
},
'matchesReplaceMap[6]': {
columnId: 'matchesReplaceMap[6]',
order: 'matchesReplaceMap[7]',
},
'matchesReplaceMap[9]': {
columnId: 'matchesReplaceMap[9]',
order: 'matchesReplaceMap[10]',
},
},
rows: {
'matchesReplaceMap[12]': {
rowId: 'matchesReplaceMap[12]',
order: 'matchesReplaceMap[13]',
},
'matchesReplaceMap[15]': {
rowId: 'matchesReplaceMap[15]',
order: 'matchesReplaceMap[16]',
},
],
title: {
'$blocksuite:internal:text$': true,
delta: [],
},
cells: {
'matchesReplaceMap[12]': {
'matchesReplaceMap[10]': {
columnId: 'matchesReplaceMap[10]',
value: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'eee',
},
],
},
},
'matchesReplaceMap[11]': {
columnId: 'matchesReplaceMap[11]',
value: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'fff',
},
],
},
'matchesReplaceMap[17]': {
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'aaa',
},
],
},
},
},
columns: [
{
type: 'title',
name: 'aaa',
data: {},
id: 'matchesReplaceMap[9]',
'matchesReplaceMap[18]': {
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'bbb',
},
],
},
},
{
type: 'rich-text',
name: 'bbb',
data: {},
id: 'matchesReplaceMap[10]',
'matchesReplaceMap[19]': {
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'ccc',
},
],
},
},
{
type: 'rich-text',
name: 'ccc',
data: {},
id: 'matchesReplaceMap[11]',
},
],
},
children: [
{
type: 'block',
id: 'matchesReplaceMap[12]',
flavour: 'affine:paragraph',
props: {
'matchesReplaceMap[20]': {
text: {
'$blocksuite:internal:text$': true,
delta: [
@@ -3622,11 +3611,30 @@ bbb
},
],
},
type: 'text',
},
children: [],
'matchesReplaceMap[21]': {
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'eee',
},
],
},
},
'matchesReplaceMap[22]': {
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'fff',
},
],
},
},
},
],
},
children: [],
},
],
};
@@ -13,6 +13,7 @@ import {
import { ImageBlockHtmlAdapterExtension } from '@blocksuite/affine-block-image';
import { ListBlockHtmlAdapterExtension } from '@blocksuite/affine-block-list';
import { ParagraphBlockHtmlAdapterExtension } from '@blocksuite/affine-block-paragraph';
import { TableBlockHtmlAdapterExtension } from '@blocksuite/affine-block-table';
import { RootBlockHtmlAdapterExtension } from '../../../root-block/adapters/html.js';
@@ -29,6 +30,7 @@ export const defaultBlockHtmlAdapterMatchers = [
EmbedGithubBlockHtmlAdapterExtension,
BookmarkBlockHtmlAdapterExtension,
DatabaseBlockHtmlAdapterExtension,
TableBlockHtmlAdapterExtension,
EmbedLinkedDocHtmlAdapterExtension,
EmbedSyncedDocBlockHtmlAdapterExtension,
];
@@ -15,6 +15,7 @@ import { LatexBlockMarkdownAdapterExtension } from '@blocksuite/affine-block-lat
import { ListBlockMarkdownAdapterExtension } from '@blocksuite/affine-block-list';
import { DocNoteBlockMarkdownAdapterExtension } from '@blocksuite/affine-block-note';
import { ParagraphBlockMarkdownAdapterExtension } from '@blocksuite/affine-block-paragraph';
import { TableBlockMarkdownAdapterExtension } from '@blocksuite/affine-block-table';
import { RootBlockMarkdownAdapterExtension } from '../../../root-block/adapters/markdown.js';
@@ -32,6 +33,7 @@ export const defaultBlockMarkdownAdapterMatchers = [
BookmarkBlockMarkdownAdapterExtension,
CodeBlockMarkdownAdapterExtension,
DatabaseBlockMarkdownAdapterExtension,
TableBlockMarkdownAdapterExtension,
DividerBlockMarkdownAdapterExtension,
ImageBlockMarkdownAdapterExtension,
LatexBlockMarkdownAdapterExtension,
@@ -18,7 +18,6 @@ export class TestUtils {
export function nanoidReplacement(snapshot: BlockSnapshot | SliceSnapshot) {
return JSON.parse(nanoidReplacementString(JSON.stringify(snapshot)));
}
const escapedSnapshotAttributes = new Set([
'"attributes"',
'"conditions"',
@@ -31,7 +30,7 @@ const escapedSnapshotAttributes = new Set([
function nanoidReplacementString(snapshotString: string) {
const re =
/("block:[A-Za-z0-9-_]{10}")|("[A-Za-z0-9-_]{10}")|("var\(--affine-v2-chip-label-[a-z]{3,10}\)")|("[A-Za-z0-9-_=]{44}")/g;
/("block:[A-Za-z0-9-_]{10}")|("[A-Za-z0-9-_]{10}")|("[A-Za-z0-9-_]{35}")|("[A-Za-z0-9-_]{10}:[A-Za-z0-9-_]{10}")|("var\(--affine-v2-chip-label-[a-z]{3,10}\)")|("[A-Za-z0-9-_=]{44}")/g;
const matches = snapshotString.matchAll(re);
const matchesReplaceMap = new Map();
let escapedNumber = 0;
+6
View File
@@ -26,6 +26,10 @@ import {
EdgelessSurfaceRefBlockSpec,
PageSurfaceRefBlockSpec,
} from '@blocksuite/affine-block-surface-ref';
import {
TableBlockSpec,
TableSelectionExtension,
} from '@blocksuite/affine-block-table';
import {
RefNodeSlotsExtension,
RichTextExtensions,
@@ -60,6 +64,7 @@ export const CommonBlockSpecs: ExtensionType[] = [
LatexBlockSpec,
ListBlockSpec,
DatabaseBlockSpec,
TableBlockSpec,
DataViewBlockSpec,
DividerBlockSpec,
BookmarkBlockSpec,
@@ -100,5 +105,6 @@ export const StoreExtensions: ExtensionType[] = [
HighlightSelectionExtension,
ImageSelectionExtension,
DatabaseSelectionExtension,
TableSelectionExtension,
LinkPreviewerService,
];
+2
View File
@@ -14,6 +14,7 @@ import { effects as blockNoteEffects } from '@blocksuite/affine-block-note/effec
import { effects as blockParagraphEffects } from '@blocksuite/affine-block-paragraph/effects';
import { effects as blockSurfaceEffects } from '@blocksuite/affine-block-surface/effects';
import { effects as blockSurfaceRefEffects } from '@blocksuite/affine-block-surface-ref/effects';
import { effects as blockTableEffects } from '@blocksuite/affine-block-table/effects';
import { effects as componentAiItemEffects } from '@blocksuite/affine-components/ai-item';
import { BlockSelection } from '@blocksuite/affine-components/block-selection';
import { BlockZeroWidth } from '@blocksuite/affine-components/block-zero-width';
@@ -204,6 +205,7 @@ export function effects() {
blockDividerEffects();
blockDataViewEffects();
blockCodeEffects();
blockTableEffects();
componentCaptionEffects();
componentContextMenuEffects();
+1
View File
@@ -48,6 +48,7 @@ export * from '@blocksuite/affine-block-note';
export * from '@blocksuite/affine-block-paragraph';
export * from '@blocksuite/affine-block-surface';
export * from '@blocksuite/affine-block-surface-ref';
export * from '@blocksuite/affine-block-table';
export {
type AIError,
type AIItemConfig,
@@ -439,6 +439,8 @@ export const BUILT_IN_GROUPS: MenuItemGroup<FormatBarContext>[] = [
];
export function toolbarMoreButton(toolbar: AffineFormatBarWidget) {
const richText = getRichText();
if (richText?.dataset.disableAskAi !== undefined) return null;
const context = new FormatBarContext(toolbar);
const actions = renderGroups(toolbar.moreGroups, context);
@@ -455,3 +457,15 @@ export function toolbarMoreButton(toolbar: AffineFormatBarWidget) {
</editor-menu-button>
`;
}
const getRichText = () => {
const selection = getSelection();
if (!selection) return null;
if (selection.rangeCount === 0) return null;
const range = selection.getRangeAt(0);
const commonAncestorContainer =
range.commonAncestorContainer instanceof Element
? range.commonAncestorContainer
: range.commonAncestorContainer.parentElement;
if (!commonAncestorContainer) return null;
return commonAncestorContainer.closest('rich-text');
};
@@ -1,7 +1,8 @@
import { DatabaseSelection } from '@blocksuite/affine-block-database';
import { HoverController } from '@blocksuite/affine-components/hover';
import type { RichText } from '@blocksuite/affine-components/rich-text';
import { isFormatSupported } from '@blocksuite/affine-components/rich-text';
import {
isFormatSupported,
type RichText,
} from '@blocksuite/affine-components/rich-text';
import {
cloneGroups,
getMoreMenuConfig,
@@ -205,34 +206,20 @@ export class AffineFormatBarWidget extends WidgetComponent {
);
this.disposables.addFromEvent(document, 'selectionchange', () => {
if (!this.host.event.active) return;
const databaseSelection = this.host.selection.find(DatabaseSelection);
if (!databaseSelection) {
return;
}
const reset = () => {
this.reset();
this.requestUpdate();
};
const viewSelection = databaseSelection.viewSelection;
// check table selection
if (
viewSelection.type === 'table' &&
(viewSelection.selectionType !== 'area' || !viewSelection.isEditing)
)
return reset();
// check kanban selection
if (
(viewSelection.type === 'kanban' &&
viewSelection.selectionType !== 'cell') ||
!viewSelection.isEditing
)
return reset();
const range = this.nativeRange;
if (!range || range.collapsed) return reset();
if (!range) return;
const container =
range.commonAncestorContainer instanceof Element
? range.commonAncestorContainer
: range.commonAncestorContainer.parentElement;
if (!container) return;
const notBlockText = container.closest('rich-text')?.dataset.notBlockText;
if (notBlockText == null) return;
if (range.collapsed) return reset();
this._displayType = 'native';
this.requestUpdate();
});
@@ -551,12 +538,16 @@ export class AffineFormatBarWidget extends WidgetComponent {
}
const items = ConfigRenderer(this);
const moreButton = toolbarMoreButton(this);
return html`
<editor-toolbar class="${AFFINE_FORMAT_BAR_WIDGET}">
${items}
<editor-toolbar-separator></editor-toolbar-separator>
${toolbarMoreButton(this)}
${moreButton
? html`
<editor-toolbar-separator></editor-toolbar-separator>
${moreButton}
`
: nothing}
</editor-toolbar>
`;
}
@@ -246,6 +246,23 @@ export const defaultSlashMenuConfig: SlashMenuConfig = {
// ---------------------------------------------------------
{ groupName: 'Content & Media' },
{
name: 'Table',
description: 'Create a table block.',
icon: DatabaseTableViewIcon20,
tooltip: slashMenuToolTips['Table View'],
showWhen: ({ model }) => !insideEdgelessText(model),
action: ({ rootComponent }) => {
rootComponent.std.command
.chain()
.getSelectedModels()
.insertTableBlock({
place: 'after',
removeEmptyLine: true,
})
.run();
},
},
{
name: 'Image',
description: 'Insert an image.',
+2
View File
@@ -23,6 +23,7 @@ import {
ParagraphBlockSchema,
RootBlockSchema,
SurfaceRefBlockSchema,
TableBlockSchema,
} from '@blocksuite/affine-model';
import type { BlockSchema } from '@blocksuite/store';
import type { z } from 'zod';
@@ -52,4 +53,5 @@ export const AffineSchemas: z.infer<typeof BlockSchema>[] = [
EmbedLoomBlockSchema,
EdgelessTextBlockSchema,
LatexBlockSchema,
TableBlockSchema,
];