Saul-Mirone
2024-12-30 12:59:57 +00:00
parent e526106f45
commit c28f918527
63 changed files with 268 additions and 127 deletions

View File

@@ -1,3 +1,12 @@
import {
addProperty,
copyCellsByProperty,
databaseBlockColumns,
deleteColumn,
getCell,
getProperty,
updateCell,
} from '@blocksuite/affine-block-database';
import {
type Cell,
type Column,
@@ -12,16 +21,6 @@ import type { BlockModel, Doc } from '@blocksuite/store';
import { DocCollection, IdGeneratorType, Schema } from '@blocksuite/store';
import { beforeEach, describe, expect, test } from 'vitest';
import { databaseBlockColumns } from '../../database-block/index.js';
import {
addProperty,
copyCellsByProperty,
deleteColumn,
getCell,
getProperty,
updateCell,
} from '../../database-block/utils/block-utils.js';
const AffineSchemas = [
RootBlockSchema,
NoteBlockSchema,

View File

@@ -1,5 +1,6 @@
import { BookmarkBlockHtmlAdapterExtension } from '@blocksuite/affine-block-bookmark';
import { CodeBlockHtmlAdapterExtension } from '@blocksuite/affine-block-code';
import { DatabaseBlockHtmlAdapterExtension } from '@blocksuite/affine-block-database';
import { DividerBlockHtmlAdapterExtension } from '@blocksuite/affine-block-divider';
import {
EmbedFigmaBlockHtmlAdapterExtension,
@@ -13,7 +14,6 @@ import { ImageBlockHtmlAdapterExtension } from '@blocksuite/affine-block-image';
import { ListBlockHtmlAdapterExtension } from '@blocksuite/affine-block-list';
import { ParagraphBlockHtmlAdapterExtension } from '@blocksuite/affine-block-paragraph';
import { DatabaseBlockHtmlAdapterExtension } from '../../../database-block/adapters/html.js';
import { RootBlockHtmlAdapterExtension } from '../../../root-block/adapters/html.js';
export const defaultBlockHtmlAdapterMatchers = [

View File

@@ -1,5 +1,6 @@
import { bookmarkBlockMarkdownAdapterMatcher } from '@blocksuite/affine-block-bookmark';
import { codeBlockMarkdownAdapterMatcher } from '@blocksuite/affine-block-code';
import { databaseBlockMarkdownAdapterMatcher } from '@blocksuite/affine-block-database';
import { dividerBlockMarkdownAdapterMatcher } from '@blocksuite/affine-block-divider';
import {
embedFigmaBlockMarkdownAdapterMatcher,
@@ -14,7 +15,6 @@ import { latexBlockMarkdownAdapterMatcher } from '@blocksuite/affine-block-latex
import { listBlockMarkdownAdapterMatcher } from '@blocksuite/affine-block-list';
import { paragraphBlockMarkdownAdapterMatcher } from '@blocksuite/affine-block-paragraph';
import { databaseBlockMarkdownAdapterMatcher } from '../../../database-block/adapters/markdown.js';
import { rootBlockMarkdownAdapterMatcher } from '../../../root-block/adapters/markdown.js';
export const defaultBlockMarkdownAdapterMatchers = [

View File

@@ -1,6 +1,7 @@
import { AttachmentBlockNotionHtmlAdapterExtension } from '@blocksuite/affine-block-attachment';
import { BookmarkBlockNotionHtmlAdapterExtension } from '@blocksuite/affine-block-bookmark';
import { CodeBlockNotionHtmlAdapterExtension } from '@blocksuite/affine-block-code';
import { DatabaseBlockNotionHtmlAdapterExtension } from '@blocksuite/affine-block-database';
import { DividerBlockNotionHtmlAdapterExtension } from '@blocksuite/affine-block-divider';
import {
EmbedFigmaBlockNotionHtmlAdapterExtension,
@@ -14,7 +15,6 @@ import { ListBlockNotionHtmlAdapterExtension } from '@blocksuite/affine-block-li
import { ParagraphBlockNotionHtmlAdapterExtension } from '@blocksuite/affine-block-paragraph';
import type { ExtensionType } from '@blocksuite/block-std';
import { DatabaseBlockNotionHtmlAdapterExtension } from '../../../database-block/adapters/notion-html.js';
import { RootBlockNotionHtmlAdapterExtension } from '../../../root-block/adapters/notion-html.js';
export const defaultBlockNotionHtmlAdapterMatchers: ExtensionType[] = [

View File

@@ -1,5 +1,6 @@
import { BookmarkBlockPlainTextAdapterExtension } from '@blocksuite/affine-block-bookmark';
import { CodeBlockPlainTextAdapterExtension } from '@blocksuite/affine-block-code';
import { DatabaseBlockPlainTextAdapterExtension } from '@blocksuite/affine-block-database';
import { DividerBlockPlainTextAdapterExtension } from '@blocksuite/affine-block-divider';
import {
EmbedFigmaBlockPlainTextAdapterExtension,
@@ -14,8 +15,6 @@ import { ListBlockPlainTextAdapterExtension } from '@blocksuite/affine-block-lis
import { ParagraphBlockPlainTextAdapterExtension } from '@blocksuite/affine-block-paragraph';
import type { ExtensionType } from '@blocksuite/block-std';
import { DatabaseBlockPlainTextAdapterExtension } from '../../../database-block/adapters/plain-text.js';
export const defaultBlockPlainTextAdapterMatchers: ExtensionType[] = [
ParagraphBlockPlainTextAdapterExtension,
ListBlockPlainTextAdapterExtension,

View File

@@ -1,6 +1,7 @@
import { AttachmentBlockSpec } from '@blocksuite/affine-block-attachment';
import { BookmarkBlockSpec } from '@blocksuite/affine-block-bookmark';
import { CodeBlockSpec } from '@blocksuite/affine-block-code';
import { DatabaseBlockSpec } from '@blocksuite/affine-block-database';
import { DividerBlockSpec } from '@blocksuite/affine-block-divider';
import { EdgelessTextBlockSpec } from '@blocksuite/affine-block-edgeless-text';
import { EmbedExtensions } from '@blocksuite/affine-block-embed';
@@ -34,7 +35,6 @@ import type { ExtensionType } from '@blocksuite/block-std';
import { AdapterFactoryExtensions } from '../_common/adapters/extension.js';
import { DataViewBlockSpec } from '../data-view-block/data-view-spec.js';
import { DatabaseBlockSpec } from '../database-block/database-spec.js';
export const CommonBlockSpecs: ExtensionType[] = [
DocDisplayMetaService,

View File

@@ -1,7 +1,7 @@
import { richTextColumnConfig } from '@blocksuite/affine-block-database';
import { type ListBlockModel, ListBlockSchema } from '@blocksuite/affine-model';
import { propertyPresets } from '@blocksuite/data-view/property-presets';
import { richTextColumnConfig } from '../../database-block/properties/rich-text/cell-renderer.js';
import { createBlockMeta } from './base.js';
export const todoMeta = createBlockMeta<ListBlockModel>({

View File

@@ -1,8 +1,7 @@
import { richTextColumnConfig } from '@blocksuite/affine-block-database';
import type { PropertyMetaConfig } from '@blocksuite/data-view';
import { propertyPresets } from '@blocksuite/data-view/property-presets';
import { richTextColumnConfig } from '../../database-block/properties/rich-text/cell-renderer.js';
export const queryBlockColumns = [
propertyPresets.datePropertyConfig,
propertyPresets.numberPropertyConfig,

View File

@@ -1,3 +1,7 @@
import {
databaseBlockAllPropertyMap,
databasePropertyConverts,
} from '@blocksuite/affine-block-database';
import type { Column } from '@blocksuite/affine-model';
import {
insertPositionToIndex,
@@ -9,10 +13,6 @@ import { propertyPresets } from '@blocksuite/data-view/property-presets';
import { assertExists, Slot } from '@blocksuite/global/utils';
import type { Block, Doc } from '@blocksuite/store';
import {
databaseBlockAllPropertyMap,
databasePropertyConverts,
} from '../database-block/properties/index.js';
import type { BlockMeta } from './block-meta/base.js';
import { blockMetaMap } from './block-meta/index.js';
import { queryBlockAllColumnMap, queryBlockColumns } from './columns/index.js';

View File

@@ -1,3 +1,4 @@
import { BlockRenderer, NoteRenderer } from '@blocksuite/affine-block-database';
import type { NoteBlockComponent } from '@blocksuite/affine-block-note';
import { CaptionedBlockComponent } from '@blocksuite/affine-components/caption';
import {
@@ -39,8 +40,6 @@ import { computed, signal } from '@preact/signals-core';
import { css, nothing, unsafeCSS } from 'lit';
import { html } from 'lit/static-html.js';
import { BlockRenderer } from '../database-block/detail-panel/block-renderer.js';
import { NoteRenderer } from '../database-block/detail-panel/note-renderer.js';
import {
EdgelessRootBlockComponent,
type RootService,

View File

@@ -1,13 +0,0 @@
import type { ExtensionType } from '@blocksuite/block-std';
import { DatabaseBlockHtmlAdapterExtension } from './html.js';
import { DatabaseBlockMarkdownAdapterExtension } from './markdown.js';
import { DatabaseBlockNotionHtmlAdapterExtension } from './notion-html.js';
import { DatabaseBlockPlainTextAdapterExtension } from './plain-text.js';
export const DatabaseBlockAdapterExtensions: ExtensionType[] = [
DatabaseBlockHtmlAdapterExtension,
DatabaseBlockMarkdownAdapterExtension,
DatabaseBlockNotionHtmlAdapterExtension,
DatabaseBlockPlainTextAdapterExtension,
];

View File

@@ -1,292 +0,0 @@
import {
type Column,
DatabaseBlockSchema,
type SerializedCells,
} from '@blocksuite/affine-model';
import {
BlockHtmlAdapterExtension,
type BlockHtmlAdapterMatcher,
HastUtils,
type InlineHtmlAST,
TextUtils,
} from '@blocksuite/affine-shared/adapters';
import type { DeltaInsert } from '@blocksuite/inline';
import { type BlockSnapshot, nanoid } from '@blocksuite/store';
import { format } from 'date-fns/format';
import type { Element } from 'hast';
const DATABASE_NODE_TYPES = new Set(['table', 'thead', 'tbody', 'th', 'tr']);
export const databaseBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = {
flavour: DatabaseBlockSchema.model.flavour,
toMatch: o =>
HastUtils.isElement(o.node) && DATABASE_NODE_TYPES.has(o.node.tagName),
fromMatch: o => o.node.flavour === DatabaseBlockSchema.model.flavour,
toBlockSnapshot: {
enter: (o, context) => {
if (!HastUtils.isElement(o.node)) {
return;
}
const { walkerContext } = context;
if (o.node.tagName === 'table') {
const tableHeader = HastUtils.querySelector(o.node, 'thead');
if (!tableHeader) {
return;
}
const tableHeaderRow = HastUtils.querySelector(tableHeader, 'tr');
if (!tableHeaderRow) {
return;
}
// Table header row as database header row
const viewsColumns = tableHeaderRow.children.map(() => {
return {
id: nanoid(),
hide: false,
width: 180,
};
});
// Build database cells from table body rows
const cells = Object.create(null);
const tableBody = HastUtils.querySelector(o.node, 'tbody');
tableBody?.children.forEach(row => {
const rowId = nanoid();
cells[rowId] = Object.create(null);
(row as Element).children.forEach((cell, index) => {
cells[rowId][viewsColumns[index].id] = {
columnId: viewsColumns[index].id,
value: TextUtils.createText(
(cell as Element).children
.map(child => ('value' in child ? child.value : ''))
.join('')
),
};
});
});
// Build database columns from table header row
const columns = tableHeaderRow.children.map((_child, index) => {
return {
type: index === 0 ? 'title' : 'rich-text',
name: (_child as Element).children
.map(child => ('value' in child ? child.value : ''))
.join(''),
data: {},
id: viewsColumns[index].id,
};
});
walkerContext.openNode(
{
type: 'block',
id: nanoid(),
flavour: 'affine:database',
props: {
views: [
{
id: nanoid(),
name: 'Table View',
mode: 'table',
columns: [],
filter: {
type: 'group',
op: 'and',
conditions: [],
},
header: {
titleColumn: viewsColumns[0]?.id,
iconColumn: 'type',
},
},
],
title: {
'$blocksuite:internal:text$': true,
delta: [],
},
cells,
columns,
},
children: [],
},
'children'
);
walkerContext.setNodeContext('affine:table:rowid', Object.keys(cells));
walkerContext.skipChildren(1);
}
// The first child of each table body row is the database title cell
if (o.node.tagName === 'tr') {
const { deltaConverter } = context;
walkerContext
.openNode({
type: 'block',
id:
(
walkerContext.getNodeContext(
'affine:table:rowid'
) as Array<string>
).shift() ?? nanoid(),
flavour: 'affine:paragraph',
props: {
text: {
'$blocksuite:internal:text$': true,
delta: deltaConverter.astToDelta(o.node.children[0]),
},
type: 'text',
},
children: [],
})
.closeNode();
walkerContext.skipAllChildren();
}
},
leave: (o, context) => {
if (!HastUtils.isElement(o.node)) {
return;
}
const { walkerContext } = context;
if (o.node.tagName === 'table') {
walkerContext.closeNode();
}
},
},
fromBlockSnapshot: {
enter: (o, context) => {
const { walkerContext } = context;
const columns = o.node.props.columns as Array<Column>;
const children = o.node.children;
const cells = o.node.props.cells as SerializedCells;
const createAstTableCell = (
children: InlineHtmlAST[]
): InlineHtmlAST => ({
type: 'element',
tagName: 'td',
properties: Object.create(null),
children,
});
const createAstTableHeaderCell = (
children: InlineHtmlAST[]
): InlineHtmlAST => ({
type: 'element',
tagName: 'th',
properties: Object.create(null),
children,
});
const createAstTableRow = (cells: InlineHtmlAST[]): Element => ({
type: 'element',
tagName: 'tr',
properties: Object.create(null),
children: cells,
});
const { deltaConverter } = context;
const htmlAstRows = Array.prototype.map.call(
children,
(v: BlockSnapshot) => {
const rowCells = Array.prototype.map.call(columns, col => {
const cell = cells[v.id]?.[col.id];
if (!cell && col.type !== 'title') {
return createAstTableCell([{ type: 'text', value: '' }]);
}
switch (col.type) {
case 'rich-text':
return createAstTableCell(
deltaConverter.deltaToAST(
(cell.value as { delta: DeltaInsert[] }).delta
)
);
case 'title':
return createAstTableCell(
deltaConverter.deltaToAST(
(v.props.text as { delta: DeltaInsert[] }).delta
)
);
case 'date':
return createAstTableCell([
{
type: 'text',
value: format(new Date(cell.value as number), 'yyyy-MM-dd'),
},
]);
case 'select': {
const value =
(col.data.options.find(
(opt: Record<string, string>) => opt.id === cell.value
)?.value as string) ?? '';
return createAstTableCell([{ type: 'text', value }]);
}
case 'multi-select': {
const value = Array.prototype.map
.call(
cell.value,
val =>
col.data.options.find(
(opt: Record<string, string>) => val === opt.id
).value ?? ''
)
.filter(Boolean)
.join(',');
return createAstTableCell([{ type: 'text', value }]);
}
case 'checkbox': {
return createAstTableCell([
{ type: 'text', value: String(cell.value) },
]);
}
// eslint-disable-next-line sonarjs/no-duplicated-branches
default:
return createAstTableCell([
{ type: 'text', value: String(cell.value) },
]);
}
}) as InlineHtmlAST[];
return createAstTableRow(rowCells);
}
) as Element[];
// Handle first row (header).
const headerRow = createAstTableRow(
Array.prototype.map.call(columns, v =>
createAstTableHeaderCell([
{
type: 'text',
value: v.name ?? '',
},
])
) as Element[]
);
const tableHeaderAst: Element = {
type: 'element',
tagName: 'thead',
properties: Object.create(null),
children: [headerRow],
};
const tableBodyAst: Element = {
type: 'element',
tagName: 'tbody',
properties: Object.create(null),
children: [...htmlAstRows],
};
walkerContext
.openNode({
type: 'element',
tagName: 'table',
properties: Object.create(null),
children: [tableHeaderAst, tableBodyAst],
})
.closeNode();
walkerContext.skipAllChildren();
},
},
};
export const DatabaseBlockHtmlAdapterExtension = BlockHtmlAdapterExtension(
databaseBlockHtmlAdapterMatcher
);

View File

@@ -1,3 +0,0 @@
export * from './html.js';
export * from './markdown.js';
export * from './notion-html.js';

View File

@@ -1,253 +0,0 @@
import {
type Column,
DatabaseBlockSchema,
type SerializedCells,
} from '@blocksuite/affine-model';
import {
BlockMarkdownAdapterExtension,
type BlockMarkdownAdapterMatcher,
type MarkdownAST,
TextUtils,
} from '@blocksuite/affine-shared/adapters';
import type { DeltaInsert } from '@blocksuite/inline';
import { type BlockSnapshot, nanoid } from '@blocksuite/store';
import { format } from 'date-fns/format';
import type { TableRow } from 'mdast';
const DATABASE_NODE_TYPES = new Set(['table', 'tableRow']);
const isDatabaseNode = (node: MarkdownAST) =>
DATABASE_NODE_TYPES.has(node.type);
export const databaseBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher =
{
flavour: DatabaseBlockSchema.model.flavour,
toMatch: o => isDatabaseNode(o.node),
fromMatch: o => o.node.flavour === DatabaseBlockSchema.model.flavour,
toBlockSnapshot: {
enter: (o, context) => {
const { walkerContext } = context;
if (o.node.type === 'table') {
const viewsColumns = o.node.children[0].children.map(() => {
return {
id: nanoid(),
hide: false,
width: 180,
};
});
const cells = Object.create(null);
o.node.children.slice(1).forEach(row => {
const rowId = nanoid();
cells[rowId] = Object.create(null);
row.children.slice(1).forEach((cell, index) => {
cells[rowId][viewsColumns[index + 1].id] = {
columnId: viewsColumns[index + 1].id,
value: TextUtils.createText(
cell.children
.map(child => ('value' in child ? child.value : ''))
.join('')
),
};
});
});
const columns = o.node.children[0].children.map((_child, index) => {
return {
type: index === 0 ? 'title' : 'rich-text',
name: _child.children
.map(child => ('value' in child ? child.value : ''))
.join(''),
data: {},
id: viewsColumns[index].id,
};
});
walkerContext.openNode(
{
type: 'block',
id: nanoid(),
flavour: 'affine:database',
props: {
views: [
{
id: nanoid(),
name: 'Table View',
mode: 'table',
columns: [],
filter: {
type: 'group',
op: 'and',
conditions: [],
},
header: {
titleColumn: viewsColumns[0]?.id,
iconColumn: 'type',
},
},
],
title: {
'$blocksuite:internal:text$': true,
delta: [],
},
cells,
columns,
},
children: [],
},
'children'
);
walkerContext.setNodeContext(
'affine:table:rowid',
Object.keys(cells)
);
walkerContext.skipChildren(1);
}
if (o.node.type === 'tableRow') {
const { deltaConverter } = context;
walkerContext
.openNode({
type: 'block',
id:
(
walkerContext.getNodeContext(
'affine:table:rowid'
) as Array<string>
).shift() ?? nanoid(),
flavour: 'affine:paragraph',
props: {
text: {
'$blocksuite:internal:text$': true,
delta: deltaConverter.astToDelta(o.node.children[0]),
},
type: 'text',
},
children: [],
})
.closeNode();
walkerContext.skipAllChildren();
}
},
leave: (o, context) => {
const { walkerContext } = context;
if (o.node.type === 'table') {
walkerContext.closeNode();
}
},
},
fromBlockSnapshot: {
enter: (o, context) => {
const { walkerContext, deltaConverter } = context;
const rows: TableRow[] = [];
const columns = o.node.props.columns as Array<Column>;
const children = o.node.children;
const cells = o.node.props.cells as SerializedCells;
const createAstCell = (children: MarkdownAST[]) => ({
type: 'tableCell',
children,
});
const mdAstCells = Array.prototype.map.call(
children,
(v: BlockSnapshot) =>
Array.prototype.map.call(columns, col => {
const cell = cells[v.id]?.[col.id];
if (!cell && col.type !== 'title') {
return createAstCell([{ type: 'text', value: '' }]);
}
switch (col.type) {
case 'link':
case 'progress':
case 'number':
return createAstCell([
{
type: 'text',
value: cell.value as string,
},
]);
case 'rich-text':
return createAstCell(
deltaConverter.deltaToAST(
(cell.value as { delta: DeltaInsert[] }).delta
)
);
case 'title':
return createAstCell(
deltaConverter.deltaToAST(
(v.props.text as { delta: DeltaInsert[] }).delta
)
);
case 'date':
return createAstCell([
{
type: 'text',
value: format(
new Date(cell.value as number),
'yyyy-MM-dd'
),
},
]);
case 'select': {
const value = col.data.options.find(
(opt: Record<string, string>) => opt.id === cell.value
)?.value;
return createAstCell([{ type: 'text', value }]);
}
case 'multi-select': {
const value = Array.prototype.map
.call(
cell.value,
val =>
col.data.options.find(
(opt: Record<string, string>) => val === opt.id
).value
)
.filter(Boolean)
.join(',');
return createAstCell([{ type: 'text', value }]);
}
case 'checkbox': {
return createAstCell([
{ type: 'text', value: cell.value as string },
]);
}
// eslint-disable-next-line sonarjs/no-duplicated-branches
default:
return createAstCell([
{ type: 'text', value: cell.value as string },
]);
}
})
);
// Handle first row.
if (Array.isArray(columns)) {
rows.push({
type: 'tableRow',
children: Array.prototype.map.call(columns, v =>
createAstCell([
{
type: 'text',
value: v.name,
},
])
) as [],
});
}
// Handle 2-... rows
Array.prototype.forEach.call(mdAstCells, children => {
rows.push({ type: 'tableRow', children });
});
walkerContext
.openNode({
type: 'table',
children: rows,
})
.closeNode();
walkerContext.skipAllChildren();
},
},
};
export const DatabaseBlockMarkdownAdapterExtension =
BlockMarkdownAdapterExtension(databaseBlockMarkdownAdapterMatcher);

View File

@@ -1,348 +0,0 @@
import { DatabaseBlockSchema } from '@blocksuite/affine-model';
import {
BlockNotionHtmlAdapterExtension,
type BlockNotionHtmlAdapterMatcher,
HastUtils,
TextUtils,
} from '@blocksuite/affine-shared/adapters';
import { getTagColor } from '@blocksuite/data-view';
import { type BlockSnapshot, nanoid } from '@blocksuite/store';
const ColumnClassMap: Record<string, string> = {
typesSelect: 'select',
typesMultipleSelect: 'multi-select',
typesNumber: 'number',
typesCheckbox: 'checkbox',
typesText: 'rich-text',
typesTitle: 'title',
};
const NotionDatabaseToken = '.collection-content';
const NotionDatabaseTitleToken = '.collection-title';
type BlocksuiteTableColumn = {
type: string;
name: string;
data: {
options?: {
id: string;
value: string;
color: string;
}[];
};
id: string;
};
type BlocksuiteTableRow = Record<
string,
{
columnId: string;
value: unknown;
}
>;
const DATABASE_NODE_TYPES = new Set(['table', 'th', 'tr']);
export const databaseBlockNotionHtmlAdapterMatcher: BlockNotionHtmlAdapterMatcher =
{
flavour: DatabaseBlockSchema.model.flavour,
toMatch: o =>
HastUtils.isElement(o.node) && DATABASE_NODE_TYPES.has(o.node.tagName),
fromMatch: () => false,
toBlockSnapshot: {
enter: (o, context) => {
if (!HastUtils.isElement(o.node)) {
return;
}
const { walkerContext, deltaConverter, pageMap } = context;
switch (o.node.tagName) {
case 'th': {
const columnId = nanoid();
const columnTypeClass = HastUtils.querySelector(o.node, 'svg')
?.properties?.className;
const columnType = Array.isArray(columnTypeClass)
? (ColumnClassMap[columnTypeClass[0]] ?? 'rich-text')
: 'rich-text';
walkerContext.pushGlobalContextStack<BlocksuiteTableColumn>(
'hast:table:column',
{
type: columnType,
name: HastUtils.getTextContent(
HastUtils.getTextChildrenOnlyAst(o.node)
),
data: Object.create(null),
id: columnId,
}
);
// disable icon img in th
walkerContext.setGlobalContext('hast:disableimg', true);
break;
}
case 'tr': {
if (
o.parent?.node.type === 'element' &&
o.parent.node.tagName === 'tbody'
) {
const columns =
walkerContext.getGlobalContextStack<BlocksuiteTableColumn>(
'hast:table:column'
);
const row = Object.create(null);
let plainTable = false;
HastUtils.getElementChildren(o.node).forEach((child, index) => {
if (plainTable || columns[index] === undefined) {
plainTable = true;
if (columns[index] === undefined) {
columns.push({
type: 'rich-text',
name: '',
data: Object.create(null),
id: nanoid(),
});
walkerContext.pushGlobalContextStack<BlockSnapshot>(
'hast:table:children',
{
type: 'block',
id: nanoid(),
flavour: 'affine:paragraph',
props: {
text: {
'$blocksuite:internal:text$': true,
delta: deltaConverter.astToDelta(child),
},
type: 'text',
},
children: [],
}
);
}
walkerContext.pushGlobalContextStack<BlockSnapshot>(
'hast:table:children',
{
type: 'block',
id: nanoid(),
flavour: 'affine:paragraph',
props: {
text: {
'$blocksuite:internal:text$': true,
delta: deltaConverter.astToDelta(child),
},
type: 'text',
},
children: [],
}
);
row[columns[index].id] = {
columnId: columns[index].id,
value: HastUtils.getTextContent(child),
};
} else if (HastUtils.querySelector(child, '.cell-title')) {
walkerContext.pushGlobalContextStack<BlockSnapshot>(
'hast:table:children',
{
type: 'block',
id: nanoid(),
flavour: 'affine:paragraph',
props: {
text: {
'$blocksuite:internal:text$': true,
delta: deltaConverter.astToDelta(child, { pageMap }),
},
type: 'text',
},
children: [],
}
);
columns[index].type = 'title';
return;
}
const optionIds: string[] = [];
if (HastUtils.querySelector(child, '.selected-value')) {
if (!('options' in columns[index].data)) {
columns[index].data.options = [];
}
if (
!['multi-select', 'select'].includes(columns[index].type)
) {
columns[index].type = 'select';
}
if (
columns[index].type === 'select' &&
child.type === 'element' &&
child.children.length > 1
) {
columns[index].type = 'multi-select';
}
child.type === 'element' &&
child.children.forEach(span => {
const filteredArray = columns[index].data.options?.filter(
option =>
option.value === HastUtils.getTextContent(span)
);
const id = filteredArray?.length
? filteredArray[0].id
: nanoid();
if (!filteredArray?.length) {
columns[index].data.options?.push({
id,
value: HastUtils.getTextContent(span),
color: getTagColor(),
});
}
optionIds.push(id);
});
// Expand will be done when leaving the table
row[columns[index].id] = {
columnId: columns[index].id,
value: optionIds,
};
} else if (HastUtils.querySelector(child, '.checkbox')) {
if (columns[index].type !== 'checkbox') {
columns[index].type = 'checkbox';
}
row[columns[index].id] = {
columnId: columns[index].id,
value: HastUtils.querySelector(child, '.checkbox-on')
? true
: false,
};
} else if (columns[index].type === 'number') {
const text = HastUtils.getTextContent(child);
const number = Number(text);
if (Number.isNaN(number)) {
columns[index].type = 'rich-text';
row[columns[index].id] = {
columnId: columns[index].id,
value: TextUtils.createText(text),
};
} else {
row[columns[index].id] = {
columnId: columns[index].id,
value: number,
};
}
} else {
row[columns[index].id] = {
columnId: columns[index].id,
value: HastUtils.getTextContent(child),
};
}
if (
columns[index].type === 'rich-text' &&
!TextUtils.isText(row[columns[index].id].value)
) {
row[columns[index].id] = {
columnId: columns[index].id,
value: TextUtils.createText(row[columns[index].id].value),
};
}
});
walkerContext.setGlobalContextStack('hast:table:column', columns);
walkerContext.pushGlobalContextStack('hast:table:rows', row);
}
}
}
},
leave: (o, context) => {
if (!HastUtils.isElement(o.node)) {
return;
}
const { walkerContext } = context;
switch (o.node.tagName) {
case 'table': {
const columns =
walkerContext.getGlobalContextStack<BlocksuiteTableColumn>(
'hast:table:column'
);
walkerContext.setGlobalContextStack('hast:table:column', []);
const children = walkerContext.getGlobalContextStack<BlockSnapshot>(
'hast:table:children'
);
walkerContext.setGlobalContextStack('hast:table:children', []);
const cells = Object.create(null);
walkerContext
.getGlobalContextStack<BlocksuiteTableRow>('hast:table:rows')
.forEach((row, i) => {
Object.keys(row).forEach(columnId => {
if (
columns.find(column => column.id === columnId)?.type ===
'select'
) {
row[columnId].value = (row[columnId].value as string[])[0];
}
});
cells[children.at(i)?.id ?? nanoid()] = row;
});
walkerContext.setGlobalContextStack('hast:table:cells', []);
let databaseTitle = '';
if (
o.parent?.node.type === 'element' &&
HastUtils.querySelector(o.parent.node, NotionDatabaseToken)
) {
databaseTitle = HastUtils.getTextContent(
HastUtils.querySelector(o.parent.node, NotionDatabaseTitleToken)
);
}
walkerContext.openNode(
{
type: 'block',
id: nanoid(),
flavour: DatabaseBlockSchema.model.flavour,
props: {
views: [
{
id: nanoid(),
name: 'Table View',
mode: 'table',
columns: [],
filter: {
type: 'group',
op: 'and',
conditions: [],
},
header: {
titleColumn:
columns.find(column => column.type === 'title')?.id ??
'',
iconColumn: 'type',
},
},
],
title: {
'$blocksuite:internal:text$': true,
delta: databaseTitle
? [
{
insert: databaseTitle,
},
]
: [],
},
columns,
cells,
},
children: [],
},
'children'
);
children.forEach(child => {
walkerContext.openNode(child, 'children').closeNode();
});
walkerContext.closeNode();
walkerContext.cleanGlobalContextStack('hast:table:column');
walkerContext.cleanGlobalContextStack('hast:table:rows');
walkerContext.cleanGlobalContextStack('hast:table:children');
break;
}
case 'th': {
walkerContext.setGlobalContext('hast:disableimg', false);
break;
}
}
},
},
fromBlockSnapshot: {},
};
export const DatabaseBlockNotionHtmlAdapterExtension =
BlockNotionHtmlAdapterExtension(databaseBlockNotionHtmlAdapterMatcher);

View File

@@ -1,92 +0,0 @@
import {
type Column,
DatabaseBlockSchema,
type SerializedCells,
} from '@blocksuite/affine-model';
import {
BlockPlainTextAdapterExtension,
type BlockPlainTextAdapterMatcher,
} from '@blocksuite/affine-shared/adapters';
import type { DeltaInsert } from '@blocksuite/inline';
import type { BlockSnapshot } from '@blocksuite/store';
import { format } from 'date-fns/format';
import { formatTable } from './utils.js';
export const databaseBlockPlainTextAdapterMatcher: BlockPlainTextAdapterMatcher =
{
flavour: DatabaseBlockSchema.model.flavour,
toMatch: () => false,
fromMatch: o => o.node.flavour === DatabaseBlockSchema.model.flavour,
toBlockSnapshot: {},
fromBlockSnapshot: {
enter: (o, context) => {
const { walkerContext, deltaConverter } = context;
const rows: string[][] = [];
const columns = o.node.props.columns as Array<Column>;
const children = o.node.children;
const cells = o.node.props.cells as SerializedCells;
const tableCells = children.map((v: BlockSnapshot) =>
columns.map(col => {
const cell = cells[v.id]?.[col.id];
if (!cell && col.type !== 'title') {
return '';
}
switch (col.type) {
case 'rich-text':
return deltaConverter
.deltaToAST((cell.value as { delta: DeltaInsert[] }).delta)
.join('');
case 'title':
return deltaConverter
.deltaToAST((v.props.text as { delta: DeltaInsert[] }).delta)
.join('');
case 'date':
return format(new Date(cell.value as number), 'yyyy-MM-dd');
case 'select': {
const value = (
col.data as { options: Array<Record<string, string>> }
).options.find(opt => opt.id === cell.value)?.value;
return value || '';
}
case 'multi-select': {
const value = (cell.value as string[])
.map(
val =>
(
col.data as { options: Array<Record<string, string>> }
).options.find(opt => val === opt.id)?.value
)
.filter(Boolean)
.join(',');
return value || '';
}
default:
return String(cell.value);
}
})
);
// Handle first row.
if (Array.isArray(columns)) {
rows.push(columns.map(col => col.name));
}
// Handle 2-... rows
tableCells.forEach(children => {
rows.push(children);
});
// Convert rows to table string
const tableString = formatTable(rows);
context.textBuffer.content += tableString;
context.textBuffer.content += '\n';
walkerContext.skipAllChildren();
},
},
};
export const DatabaseBlockPlainTextAdapterExtension =
BlockPlainTextAdapterExtension(databaseBlockPlainTextAdapterMatcher);

View File

@@ -1,32 +0,0 @@
function calculateColumnWidths(rows: string[][]): number[] {
return rows[0].map((_, colIndex) =>
Math.max(...rows.map(row => (row[colIndex] || '').length))
);
}
function formatRow(
row: string[],
columnWidths: number[],
isHeader: boolean
): string {
const cells = row.map((cell, colIndex) =>
cell.padEnd(columnWidths[colIndex], ' ')
);
const rowString = `| ${cells.join(' | ')} |`;
return isHeader
? `${rowString}\n${formatSeparator(columnWidths)}`
: rowString;
}
function formatSeparator(columnWidths: number[]): string {
const separator = columnWidths.map(width => '-'.repeat(width)).join(' | ');
return `| ${separator} |`;
}
export function formatTable(rows: string[][]): string {
const columnWidths = calculateColumnWidths(rows);
const formattedRows = rows.map((row, index) =>
formatRow(row, columnWidths, index === 0)
);
return formattedRows.join('\n');
}

View File

@@ -1,46 +0,0 @@
import type { ParagraphType } from '@blocksuite/affine-model';
import {
BulletedListIcon,
CheckBoxCheckLinearIcon,
Heading1Icon,
Heading2Icon,
Heading3Icon,
Heading4Icon,
Heading5Icon,
Heading6Icon,
NumberedListIcon,
QuoteIcon,
TextIcon,
} from '@blocksuite/icons/lit';
import type { BlockModel } from '@blocksuite/store';
import type { TemplateResult } from 'lit';
export const getIcon = (
model: BlockModel & { type?: string }
): TemplateResult => {
if (model.flavour === 'affine:paragraph') {
const type = model.type as ParagraphType;
return (
{
text: TextIcon(),
quote: QuoteIcon(),
h1: Heading1Icon(),
h2: Heading2Icon(),
h3: Heading3Icon(),
h4: Heading4Icon(),
h5: Heading5Icon(),
h6: Heading6Icon(),
} as Record<ParagraphType, TemplateResult>
)[type];
}
if (model.flavour === 'affine:list') {
return (
{
bulleted: BulletedListIcon(),
numbered: NumberedListIcon(),
todo: CheckBoxCheckLinearIcon(),
}[model.type ?? 'bulleted'] ?? BulletedListIcon()
);
}
return TextIcon();
};

View File

@@ -1,41 +0,0 @@
import type { BlockCommands, Command } from '@blocksuite/block-std';
export const insertDatabaseBlockCommand: Command<
'selectedModels',
'insertedDatabaseBlockId',
{
viewType: string;
place?: 'after' | 'before';
removeEmptyLine?: boolean;
}
> = (ctx, next) => {
const { selectedModels, viewType, place, removeEmptyLine, std } = ctx;
if (!selectedModels?.length) return;
const targetModel =
place === 'before'
? selectedModels[0]
: selectedModels[selectedModels.length - 1];
const service = std.getService('affine:database');
if (!service) return;
const result = std.doc.addSiblingBlocks(
targetModel,
[{ flavour: 'affine:database' }],
place
);
if (result.length === 0) return;
service.initDatabaseBlock(std.doc, targetModel, result[0], viewType, false);
if (removeEmptyLine && targetModel.text?.length === 0) {
std.doc.deleteBlock(targetModel);
}
next({ insertedDatabaseBlockId: result[0] });
};
export const commands: BlockCommands = {
insertDatabaseBlock: insertDatabaseBlockCommand,
};

View File

@@ -1,69 +0,0 @@
import { createModal } from '@blocksuite/affine-components/context-menu';
import { ShadowlessElement } from '@blocksuite/block-std';
import { CloseIcon } from '@blocksuite/icons/lit';
import { css, html, type TemplateResult } from 'lit';
import { property } from 'lit/decorators.js';
export class CenterPeek extends ShadowlessElement {
static override styles = css`
center-peek {
flex-direction: column;
position: absolute;
top: 5%;
left: 5%;
width: 90%;
height: 90%;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
border-radius: 12px;
}
.side-modal-content {
flex: 1;
overflow-y: auto;
}
.close-modal:hover {
background-color: var(--affine-hover-color);
}
.close-modal {
position: absolute;
right: -32px;
top: 0;
width: 24px;
height: 24px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
`;
override render() {
return html`
<div @click="${this.close}" class="close-modal">${CloseIcon()}</div>
${this.content}
`;
}
@property({ attribute: false })
accessor close: (() => void) | undefined = undefined;
@property({ attribute: false })
accessor content: TemplateResult | undefined = undefined;
}
export const popSideDetail = (template: TemplateResult) => {
return new Promise<void>(res => {
const modal = createModal(document.body);
const close = () => {
modal.remove();
res();
};
const sideContainer = new CenterPeek();
sideContainer.content = template;
sideContainer.close = close;
modal.onclick = e => e.target === modal && close();
modal.append(sideContainer);
});
};

View File

@@ -1,185 +0,0 @@
import { stopPropagation } from '@blocksuite/affine-shared/utils';
import { ShadowlessElement } from '@blocksuite/block-std';
import { WithDisposable } from '@blocksuite/global/utils';
import type { Text } from '@blocksuite/store';
import { css, html } from 'lit';
import { property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { styleMap } from 'lit/directives/style-map.js';
import type { DatabaseBlockComponent } from '../../database-block.js';
export class DatabaseTitle extends WithDisposable(ShadowlessElement) {
static override styles = css`
.affine-database-title {
position: relative;
flex: 1;
font-family: inherit;
font-size: 20px;
line-height: 28px;
font-weight: 600;
color: var(--affine-text-primary-color);
overflow: hidden;
}
.affine-database-title textarea {
font-size: inherit;
line-height: inherit;
font-weight: inherit;
letter-spacing: inherit;
font-family: inherit;
border: none;
background-color: transparent;
padding: 0;
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
outline: none;
resize: none;
scrollbar-width: none;
}
.affine-database-title .text {
user-select: none;
opacity: 0;
white-space: pre-wrap;
}
.affine-database-title[data-title-focus='false'] textarea {
opacity: 0;
}
.affine-database-title[data-title-focus='false'] .text {
text-overflow: ellipsis;
overflow: hidden;
opacity: 1;
white-space: pre;
}
.affine-database-title [data-title-empty='true']::before {
content: 'Untitled';
position: absolute;
pointer-events: none;
color: var(--affine-text-primary-color);
}
.affine-database-title [data-title-focus='true']::before {
color: var(--affine-placeholder-color);
}
`;
private readonly compositionEnd = () => {
this.titleText.replace(0, this.titleText.length, this.input.value);
};
private readonly onBlur = () => {
this.isFocus = false;
};
private readonly onFocus = () => {
this.isFocus = true;
if (this.database?.viewSelection$?.value) {
this.database?.setSelection(undefined);
}
};
private readonly onInput = (e: InputEvent) => {
this.text = this.input.value;
if (!e.isComposing) {
this.titleText.replace(0, this.titleText.length, this.input.value);
}
};
private readonly onKeyDown = (event: KeyboardEvent) => {
event.stopPropagation();
if (event.key === 'Enter' && !event.isComposing) {
event.preventDefault();
this.onPressEnterKey?.();
return;
}
};
updateText = () => {
if (!this.isFocus) {
this.input.value = this.titleText.toString();
this.text = this.input.value;
}
};
get database() {
return this.closest<DatabaseBlockComponent>('affine-database');
}
override connectedCallback() {
super.connectedCallback();
requestAnimationFrame(() => {
this.updateText();
});
this.titleText.yText.observe(this.updateText);
this.disposables.add(() => {
this.titleText.yText.unobserve(this.updateText);
});
}
override render() {
const isEmpty = !this.text;
const classList = classMap({
'affine-database-title': true,
ellipsis: !this.isFocus,
});
const untitledStyle = styleMap({
height: isEmpty ? 'auto' : 0,
opacity: isEmpty && !this.isFocus ? 1 : 0,
});
return html` <div
class="${classList}"
data-title-empty="${isEmpty}"
data-title-focus="${this.isFocus}"
>
<div class="text" style="${untitledStyle}">Untitled</div>
<div class="text">${this.text}</div>
<textarea
.disabled="${this.readonly}"
@input="${this.onInput}"
@keydown="${this.onKeyDown}"
@copy="${stopPropagation}"
@paste="${stopPropagation}"
@focus="${this.onFocus}"
@blur="${this.onBlur}"
@compositionend="${this.compositionEnd}"
data-block-is-database-title="true"
title="${this.titleText.toString()}"
></textarea>
</div>`;
}
@query('textarea')
private accessor input!: HTMLTextAreaElement;
@state()
accessor isComposing = false;
@state()
private accessor isFocus = false;
@property({ attribute: false })
accessor onPressEnterKey: (() => void) | undefined = undefined;
@property({ attribute: false })
accessor readonly!: boolean;
@state()
private accessor text = '';
@property({ attribute: false })
accessor titleText!: Text;
}
declare global {
interface HTMLElementTagNameMap {
'affine-database-title': DatabaseTitle;
}
}

View File

@@ -1,6 +0,0 @@
import type { MenuOptions } from '@blocksuite/affine-components/context-menu';
import { type DatabaseBlockModel } from '@blocksuite/affine-model';
export interface DatabaseOptionsConfig {
configure: (model: DatabaseBlockModel, options: MenuOptions) => MenuOptions;
}

View File

@@ -1,7 +0,0 @@
import type { EditorHost } from '@blocksuite/block-std';
import { createContextKey } from '@blocksuite/data-view';
export const HostContextKey = createContextKey<EditorHost | undefined>(
'editor-host',
undefined
);

View File

@@ -1,498 +0,0 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import type { DatabaseBlockModel } from '@blocksuite/affine-model';
import {
insertPositionToIndex,
type InsertToPosition,
} from '@blocksuite/affine-shared/utils';
import type { EditorHost } from '@blocksuite/block-std';
import {
type DatabaseFlags,
DataSourceBase,
type DataViewDataType,
getTagColor,
type PropertyMetaConfig,
type TypeInstance,
type ViewManager,
ViewManagerBase,
type ViewMeta,
} from '@blocksuite/data-view';
import { propertyPresets } from '@blocksuite/data-view/property-presets';
import { IS_MOBILE } from '@blocksuite/global/env';
import { assertExists } from '@blocksuite/global/utils';
import { type BlockModel, nanoid, Text } from '@blocksuite/store';
import { computed, type ReadonlySignal } from '@preact/signals-core';
import { getIcon } from './block-icons.js';
import {
databaseBlockAllPropertyMap,
databaseBlockPropertyList,
databasePropertyConverts,
} from './properties/index.js';
import { titlePurePropertyConfig } from './properties/title/define.js';
import {
addProperty,
applyCellsUpdate,
applyPropertyUpdate,
copyCellsByProperty,
deleteRows,
deleteView,
duplicateView,
findPropertyIndex,
getCell,
getProperty,
moveViewTo,
updateCell,
updateCells,
updateProperty,
updateView,
} from './utils/block-utils.js';
import {
databaseBlockViewConverts,
databaseBlockViewMap,
databaseBlockViews,
} from './views/index.js';
export class DatabaseBlockDataSource extends DataSourceBase {
private _batch = 0;
private readonly _model: DatabaseBlockModel;
override featureFlags$: ReadonlySignal<DatabaseFlags> = computed(() => {
return {
enable_number_formatting:
this.doc.awarenessStore.getFlag('enable_database_number_formatting') ??
false,
};
});
properties$: ReadonlySignal<string[]> = computed(() => {
return this._model.columns$.value.map(column => column.id);
});
readonly$: ReadonlySignal<boolean> = computed(() => {
return (
this._model.doc.readonly ||
// TODO(@L-Sun): use block level readonly
IS_MOBILE
);
});
rows$: ReadonlySignal<string[]> = computed(() => {
return this._model.children.map(v => v.id);
});
viewConverts = databaseBlockViewConverts;
viewDataList$: ReadonlySignal<DataViewDataType[]> = computed(() => {
return this._model.views$.value as DataViewDataType[];
});
override viewManager: ViewManager = new ViewManagerBase(this);
viewMetas = databaseBlockViews;
get doc() {
return this._model.doc;
}
get propertyMetas(): PropertyMetaConfig<any, any, any>[] {
return databaseBlockPropertyList;
}
constructor(model: DatabaseBlockModel) {
super();
this._model = model;
}
private _runCapture() {
if (this._batch) {
return;
}
this._batch = requestAnimationFrame(() => {
this.doc.captureSync();
this._batch = 0;
});
}
private getModelById(rowId: string): BlockModel | undefined {
return this._model.children[this._model.childMap.value.get(rowId) ?? -1];
}
private newPropertyName() {
let i = 1;
while (
this._model.columns$.value.some(column => column.name === `Column ${i}`)
) {
i++;
}
return `Column ${i}`;
}
cellValueChange(rowId: string, propertyId: string, value: unknown): void {
this._runCapture();
const type = this.propertyTypeGet(propertyId);
const update = this.propertyMetaGet(type).config.valueUpdate;
let newValue = value;
if (update) {
const old = this.cellValueGet(rowId, propertyId);
newValue = update({
value: old,
data: this.propertyDataGet(propertyId),
dataSource: this,
newValue: value,
});
}
if (type === 'title' && newValue instanceof Text) {
this._model.doc.transact(() => {
this._model.text?.clear();
this._model.text?.join(newValue);
});
return;
}
if (this._model.columns$.value.some(v => v.id === propertyId)) {
updateCell(this._model, rowId, {
columnId: propertyId,
value: newValue,
});
applyCellsUpdate(this._model);
}
}
cellValueGet(rowId: string, propertyId: string): unknown {
if (propertyId === 'type') {
const model = this.getModelById(rowId);
if (!model) {
return;
}
return getIcon(model);
}
const type = this.propertyTypeGet(propertyId);
if (type === 'title') {
const model = this.getModelById(rowId);
return model?.text;
}
return getCell(this._model, rowId, propertyId)?.value;
}
propertyAdd(insertToPosition: InsertToPosition, type?: string): string {
this.doc.captureSync();
const result = addProperty(
this._model,
insertToPosition,
databaseBlockAllPropertyMap[
type ?? propertyPresets.multiSelectPropertyConfig.type
].create(this.newPropertyName())
);
applyPropertyUpdate(this._model);
return result;
}
propertyDataGet(propertyId: string): Record<string, unknown> {
return (
this._model.columns$.value.find(v => v.id === propertyId)?.data ?? {}
);
}
propertyDataSet(propertyId: string, data: Record<string, unknown>): void {
this._runCapture();
updateProperty(this._model, propertyId, () => ({ data }));
applyPropertyUpdate(this._model);
}
propertyDataTypeGet(propertyId: string): TypeInstance | undefined {
const data = this._model.columns$.value.find(v => v.id === propertyId);
if (!data) {
return;
}
const meta = this.propertyMetaGet(data.type);
return meta.config.type({
data: data.data,
dataSource: this,
});
}
propertyDelete(id: string): void {
this.doc.captureSync();
const index = findPropertyIndex(this._model, id);
if (index < 0) return;
this.doc.transact(() => {
this._model.columns = this._model.columns.filter((_, i) => i !== index);
});
}
propertyDuplicate(propertyId: string): string {
this.doc.captureSync();
const currentSchema = getProperty(this._model, propertyId);
assertExists(currentSchema);
const { id: copyId, ...nonIdProps } = currentSchema;
const names = new Set(this._model.columns$.value.map(v => v.name));
let index = 1;
while (names.has(`${nonIdProps.name}(${index})`)) {
index++;
}
const schema = { ...nonIdProps, name: `${nonIdProps.name}(${index})` };
const id = addProperty(
this._model,
{
before: false,
id: propertyId,
},
schema
);
copyCellsByProperty(this._model, copyId, id);
applyPropertyUpdate(this._model);
return id;
}
propertyMetaGet(type: string): PropertyMetaConfig {
return databaseBlockAllPropertyMap[type];
}
propertyNameGet(propertyId: string): string {
if (propertyId === 'type') {
return 'Block Type';
}
return (
this._model.columns$.value.find(v => v.id === propertyId)?.name ?? ''
);
}
propertyNameSet(propertyId: string, name: string): void {
this.doc.captureSync();
updateProperty(this._model, propertyId, () => ({ name }));
applyPropertyUpdate(this._model);
}
override propertyReadonlyGet(propertyId: string): boolean {
if (propertyId === 'type') return true;
return false;
}
propertyTypeGet(propertyId: string): string {
if (propertyId === 'type') {
return 'image';
}
return (
this._model.columns$.value.find(v => v.id === propertyId)?.type ?? ''
);
}
propertyTypeSet(propertyId: string, toType: string): void {
const currentType = this.propertyTypeGet(propertyId);
const currentData = this.propertyDataGet(propertyId);
const rows = this.rows$.value;
const currentCells = rows.map(rowId =>
this.cellValueGet(rowId, propertyId)
);
const convertFunction = databasePropertyConverts.find(
v => v.from === currentType && v.to === toType
)?.convert;
const result = convertFunction?.(
currentData as any,
currentCells as any
) ?? {
property: databaseBlockAllPropertyMap[toType].config.defaultData(),
cells: currentCells.map(() => undefined),
};
this.doc.captureSync();
updateProperty(this._model, propertyId, () => ({
type: toType,
data: result.property,
}));
const cells: Record<string, unknown> = {};
currentCells.forEach((value, i) => {
if (value != null || result.cells[i] != null) {
cells[rows[i]] = result.cells[i];
}
});
updateCells(this._model, propertyId, cells);
applyPropertyUpdate(this._model);
}
rowAdd(insertPosition: InsertToPosition | number): string {
this.doc.captureSync();
const index =
typeof insertPosition === 'number'
? insertPosition
: insertPositionToIndex(insertPosition, this._model.children);
return this.doc.addBlock('affine:paragraph', {}, this._model.id, index);
}
rowDelete(ids: string[]): void {
this.doc.captureSync();
for (const id of ids) {
const block = this.doc.getBlock(id);
if (block) {
this.doc.deleteBlock(block.model);
}
}
deleteRows(this._model, ids);
}
rowMove(rowId: string, position: InsertToPosition): void {
const model = this.doc.getBlockById(rowId);
if (model) {
const index = insertPositionToIndex(position, this._model.children);
const target = this._model.children[index];
if (target?.id === rowId) {
return;
}
this.doc.moveBlocks([model], this._model, target);
}
}
viewDataAdd(viewData: DataViewDataType): string {
this._model.doc.captureSync();
this._model.doc.transact(() => {
this._model.views = [...this._model.views, viewData];
});
return viewData.id;
}
viewDataDelete(viewId: string): void {
this._model.doc.captureSync();
deleteView(this._model, viewId);
}
viewDataDuplicate(id: string): string {
return duplicateView(this._model, id);
}
viewDataGet(viewId: string): DataViewDataType {
return this.viewDataList$.value.find(data => data.id === viewId)!;
}
viewDataMoveTo(id: string, position: InsertToPosition): void {
moveViewTo(this._model, id, position);
}
viewDataUpdate<ViewData extends DataViewDataType>(
id: string,
updater: (data: ViewData) => Partial<ViewData>
): void {
updateView(this._model, id, updater);
}
viewMetaGet(type: string): ViewMeta {
return databaseBlockViewMap[type];
}
viewMetaGetById(viewId: string): ViewMeta {
const view = this.viewDataGet(viewId);
return this.viewMetaGet(view.mode);
}
}
export const databaseViewAddView = (
model: DatabaseBlockModel,
viewType: string
) => {
const dataSource = new DatabaseBlockDataSource(model);
dataSource.viewManager.viewAdd(viewType);
};
export const databaseViewInitEmpty = (
model: DatabaseBlockModel,
viewType: string
) => {
addProperty(
model,
'start',
titlePurePropertyConfig.create(titlePurePropertyConfig.config.name)
);
databaseViewAddView(model, viewType);
};
export const databaseViewInitConvert = (
model: DatabaseBlockModel,
viewType: string
) => {
addProperty(
model,
'end',
propertyPresets.multiSelectPropertyConfig.create('Tag', { options: [] })
);
databaseViewInitEmpty(model, viewType);
};
export const databaseViewInitTemplate = (
model: DatabaseBlockModel,
viewType: string
) => {
const ids = [nanoid(), nanoid(), nanoid()];
const statusId = addProperty(
model,
'end',
propertyPresets.selectPropertyConfig.create('Status', {
options: [
{
id: ids[0],
color: getTagColor(),
value: 'TODO',
},
{
id: ids[1],
color: getTagColor(),
value: 'In Progress',
},
{
id: ids[2],
color: getTagColor(),
value: 'Done',
},
],
})
);
for (let i = 0; i < 4; i++) {
const rowId = model.doc.addBlock(
'affine:paragraph',
{
text: new model.doc.Text(`Task ${i + 1}`),
},
model.id
);
updateCell(model, rowId, {
columnId: statusId,
value: ids[i],
});
}
databaseViewInitEmpty(model, viewType);
};
export const convertToDatabase = (host: EditorHost, viewType: string) => {
const [_, ctx] = host.std.command
.chain()
.getSelectedModels({
types: ['block', 'text'],
})
.run();
const { selectedModels } = ctx;
if (!selectedModels || selectedModels.length === 0) return;
host.doc.captureSync();
const parentModel = host.doc.getParent(selectedModels[0]);
if (!parentModel) {
return;
}
const id = host.doc.addBlock(
'affine:database',
{},
parentModel,
parentModel.children.indexOf(selectedModels[0])
);
const databaseModel = host.doc.getBlock(id)?.model as
| DatabaseBlockModel
| undefined;
if (!databaseModel) {
return;
}
databaseViewInitConvert(databaseModel, viewType);
applyPropertyUpdate(databaseModel);
host.doc.moveBlocks(selectedModels, databaseModel);
const selectionManager = host.selection;
selectionManager.clear();
};

View File

@@ -1,484 +0,0 @@
import { CaptionedBlockComponent } from '@blocksuite/affine-components/caption';
import {
menu,
popMenu,
popupTargetFromElement,
} from '@blocksuite/affine-components/context-menu';
import { DragIndicator } from '@blocksuite/affine-components/drag-indicator';
import { PeekViewProvider } from '@blocksuite/affine-components/peek';
import { toast } from '@blocksuite/affine-components/toast';
import type { DatabaseBlockModel } from '@blocksuite/affine-model';
import { NOTE_SELECTOR } from '@blocksuite/affine-shared/consts';
import {
DocModeProvider,
NotificationProvider,
type TelemetryEventMap,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
import { getDropResult } from '@blocksuite/affine-widget-drag-handle';
import {
type BlockComponent,
RANGE_SYNC_EXCLUDE_ATTR,
} from '@blocksuite/block-std';
import {
createRecordDetail,
createUniComponentFromWebComponent,
DatabaseSelection,
DataView,
dataViewCommonStyle,
type DataViewInstance,
type DataViewProps,
type DataViewSelection,
type DataViewWidget,
type DataViewWidgetProps,
defineUniComponent,
renderUniLit,
type SingleView,
uniMap,
} from '@blocksuite/data-view';
import { widgetPresets } from '@blocksuite/data-view/widget-presets';
import { Rect } from '@blocksuite/global/utils';
import {
CopyIcon,
DeleteIcon,
MoreHorizontalIcon,
} from '@blocksuite/icons/lit';
import { Slice } from '@blocksuite/store';
import { autoUpdate } from '@floating-ui/dom';
import { computed, signal } from '@preact/signals-core';
import { css, html, nothing, unsafeCSS } from 'lit';
import { popSideDetail } from './components/layout.js';
import type { DatabaseOptionsConfig } from './config.js';
import { HostContextKey } from './context/host-context.js';
import { DatabaseBlockDataSource } from './data-source.js';
import type { DatabaseBlockService } from './database-service.js';
import { BlockRenderer } from './detail-panel/block-renderer.js';
import { NoteRenderer } from './detail-panel/note-renderer.js';
import { currentViewStorage } from './utils/current-view.js';
import { getSingleDocIdFromText } from './utils/title-doc.js';
export class DatabaseBlockComponent extends CaptionedBlockComponent<
DatabaseBlockModel,
DatabaseBlockService
> {
static override styles = css`
${unsafeCSS(dataViewCommonStyle('affine-database'))}
affine-database {
display: block;
border-radius: 8px;
background-color: var(--affine-background-primary-color);
padding: 8px;
margin: 8px -8px -8px;
}
.database-block-selected {
background-color: var(--affine-hover-color);
border-radius: 4px;
}
.database-ops {
padding: 2px;
border-radius: 4px;
display: flex;
cursor: pointer;
align-items: center;
height: max-content;
}
.database-ops svg {
width: 16px;
height: 16px;
color: var(--affine-icon-color);
}
.database-ops:hover {
background-color: var(--affine-hover-color);
}
@media print {
.database-ops {
display: none;
}
.database-header-bar {
display: none !important;
}
}
`;
private readonly _clickDatabaseOps = (e: MouseEvent) => {
const options = this.optionsConfig.configure(this.model, {
items: [
menu.input({
initialValue: this.model.title.toString(),
placeholder: 'Untitled',
onChange: text => {
this.model.title.replace(0, this.model.title.length, text);
},
}),
menu.action({
prefix: CopyIcon(),
name: 'Copy',
select: () => {
const slice = Slice.fromModels(this.doc, [this.model]);
this.std.clipboard
.copySlice(slice)
.then(() => {
toast(this.host, 'Copied to clipboard');
})
.catch(console.error);
},
}),
menu.group({
items: [
menu.action({
prefix: DeleteIcon(),
class: {
'delete-item': true,
},
name: 'Delete Database',
select: () => {
this.model.children.slice().forEach(block => {
this.doc.deleteBlock(block);
});
this.doc.deleteBlock(this.model);
},
}),
],
}),
],
});
popMenu(popupTargetFromElement(e.currentTarget as HTMLElement), {
options,
});
};
private _dataSource?: DatabaseBlockDataSource;
private readonly dataView = new DataView();
private readonly renderTitle = (dataViewMethod: DataViewInstance) => {
const addRow = () => dataViewMethod.addRow?.('start');
return html` <affine-database-title
style="overflow: hidden"
.titleText="${this.model.title}"
.readonly="${this.dataSource.readonly$.value}"
.onPressEnterKey="${addRow}"
></affine-database-title>`;
};
_bindHotkey: DataViewProps['bindHotkey'] = hotkeys => {
return {
dispose: this.host.event.bindHotkey(hotkeys, {
blockId: this.topContenteditableElement?.blockId ?? this.blockId,
}),
};
};
_handleEvent: DataViewProps['handleEvent'] = (name, handler) => {
return {
dispose: this.host.event.add(name, handler, {
blockId: this.blockId,
}),
};
};
createTemplate = (
data: {
view: SingleView;
rowId: string;
},
openDoc: (docId: string) => void
) => {
return createRecordDetail({
...data,
openDoc,
detail: {
header: uniMap(
createUniComponentFromWebComponent(BlockRenderer),
props => ({
...props,
host: this.host,
})
),
note: uniMap(
createUniComponentFromWebComponent(NoteRenderer),
props => ({
...props,
model: this.model,
host: this.host,
})
),
},
});
};
headerWidget: DataViewWidget = defineUniComponent(
(props: DataViewWidgetProps) => {
return html`
<div style="margin-bottom: 16px;display:flex;flex-direction: column">
<div
style="display:flex;gap:12px;margin-bottom: 8px;align-items: center"
>
${this.renderTitle(props.dataViewInstance)}
${this.renderDatabaseOps()}
</div>
<div
style="display:flex;align-items:center;justify-content: space-between;gap: 12px"
class="database-header-bar"
>
<div style="flex:1">
${renderUniLit(widgetPresets.viewBar, {
...props,
onChangeView: id => {
currentViewStorage.setCurrentView(this.blockId, id);
},
})}
</div>
${renderUniLit(this.toolsWidget, props)}
</div>
${renderUniLit(widgetPresets.quickSettingBar, props)}
</div>
`;
}
);
indicator = new DragIndicator();
onDrag = (evt: MouseEvent, id: string): (() => void) => {
const result = getDropResult(evt);
if (result && result.rect) {
document.body.append(this.indicator);
this.indicator.rect = Rect.fromLWTH(
result.rect.left,
result.rect.width,
result.rect.top,
result.rect.height
);
return () => {
this.indicator.remove();
const model = this.doc.getBlock(id)?.model;
const target = result.modelState.model;
let parent = this.doc.getParent(target.id);
const shouldInsertIn = result.type === 'in';
if (shouldInsertIn) {
parent = target;
}
if (model && target && parent) {
if (shouldInsertIn) {
this.doc.moveBlocks([model], parent);
} else {
this.doc.moveBlocks(
[model],
parent,
target,
result.type === 'before'
);
}
}
};
}
this.indicator.remove();
return () => {};
};
setSelection = (selection: DataViewSelection | undefined) => {
if (selection) {
getSelection()?.removeAllRanges();
}
this.selection.setGroup(
'note',
selection
? [
new DatabaseSelection({
blockId: this.blockId,
viewSelection: selection,
}),
]
: []
);
};
toolsWidget: DataViewWidget = widgetPresets.createTools({
table: [
widgetPresets.tools.filter,
widgetPresets.tools.sort,
widgetPresets.tools.search,
widgetPresets.tools.viewOptions,
widgetPresets.tools.tableAddRow,
],
kanban: [
widgetPresets.tools.filter,
widgetPresets.tools.sort,
widgetPresets.tools.search,
widgetPresets.tools.viewOptions,
widgetPresets.tools.tableAddRow,
],
});
viewSelection$ = computed(() => {
const databaseSelection = this.selection.value.find(
(selection): selection is DatabaseSelection => {
if (selection.blockId !== this.blockId) {
return false;
}
return selection instanceof DatabaseSelection;
}
);
return databaseSelection?.viewSelection;
});
virtualPadding$ = signal(0);
get dataSource(): DatabaseBlockDataSource {
if (!this._dataSource) {
this._dataSource = new DatabaseBlockDataSource(this.model);
this._dataSource.contextSet(HostContextKey, this.host);
const id = currentViewStorage.getCurrentView(this.model.id);
if (id) {
this.dataSource.viewManager.setCurrentView(id);
}
}
return this._dataSource;
}
get optionsConfig(): DatabaseOptionsConfig {
return {
configure: (_model, options) => options,
...this.std.getConfig('affine:page')?.databaseOptions,
};
}
override get topContenteditableElement() {
if (this.std.get(DocModeProvider).getEditorMode() === 'edgeless') {
return this.closest<BlockComponent>(NOTE_SELECTOR);
}
return this.rootComponent;
}
get view() {
return this.dataView.expose;
}
private renderDatabaseOps() {
if (this.dataSource.readonly$.value) {
return nothing;
}
return html` <div class="database-ops" @click="${this._clickDatabaseOps}">
${MoreHorizontalIcon()}
</div>`;
}
override connectedCallback() {
super.connectedCallback();
this.setAttribute(RANGE_SYNC_EXCLUDE_ATTR, 'true');
this.listenFullWidthChange();
}
listenFullWidthChange() {
if (this.std.get(DocModeProvider).getEditorMode() === 'edgeless') {
return;
}
this.disposables.add(
autoUpdate(this.host, this, () => {
const padding =
this.getBoundingClientRect().left -
this.host.getBoundingClientRect().left;
this.virtualPadding$.value = Math.max(0, padding - 72);
})
);
}
override renderBlock() {
const peekViewService = this.std.getOptional(PeekViewProvider);
const telemetryService = this.std.getOptional(TelemetryProvider);
return html`
<div
contenteditable="false"
style="position: relative;background-color: var(--affine-background-primary-color);border-radius: 4px"
>
${this.dataView.render({
virtualPadding$: this.virtualPadding$,
bindHotkey: this._bindHotkey,
handleEvent: this._handleEvent,
selection$: this.viewSelection$,
setSelection: this.setSelection,
dataSource: this.dataSource,
headerWidget: this.headerWidget,
onDrag: this.onDrag,
clipboard: this.std.clipboard,
notification: {
toast: message => {
const notification = this.std.getOptional(NotificationProvider);
if (notification) {
notification.toast(message);
} else {
toast(this.host, message);
}
},
},
eventTrace: (key, params) => {
telemetryService?.track(key, {
...(params as TelemetryEventMap[typeof key]),
blockId: this.blockId,
});
},
detailPanelConfig: {
openDetailPanel: (target, data) => {
if (peekViewService) {
const openDoc = (docId: string) => {
return peekViewService.peek({
docId,
databaseId: this.blockId,
databaseDocId: this.model.doc.id,
databaseRowId: data.rowId,
target: this,
});
};
const doc = getSingleDocIdFromText(
this.model.doc.getBlock(data.rowId)?.model?.text
);
if (doc) {
return openDoc(doc);
}
const abort = new AbortController();
return new Promise<void>(focusBack => {
peekViewService
.peek(
{
target,
template: this.createTemplate(data, docId => {
// abort.abort();
openDoc(docId).then(focusBack).catch(focusBack);
}),
},
{ abortSignal: abort.signal }
)
.then(focusBack)
.catch(focusBack);
});
} else {
return popSideDetail(
this.createTemplate(data, () => {
//
})
);
}
},
},
})}
</div>
`;
}
override accessor useZeroWidth = true;
}
declare global {
interface HTMLElementTagNameMap {
'affine-database': DatabaseBlockComponent;
}
}

View File

@@ -1,59 +0,0 @@
import {
type DatabaseBlockModel,
DatabaseBlockSchema,
} from '@blocksuite/affine-model';
import { BlockService } from '@blocksuite/block-std';
import { viewPresets } from '@blocksuite/data-view/view-presets';
import type { BlockModel, Doc } from '@blocksuite/store';
import {
databaseViewAddView,
databaseViewInitEmpty,
databaseViewInitTemplate,
} from './data-source.js';
import {
addProperty,
applyPropertyUpdate,
updateCell,
updateView,
} from './utils/block-utils.js';
export class DatabaseBlockService extends BlockService {
static override readonly flavour = DatabaseBlockSchema.model.flavour;
addColumn = addProperty;
applyColumnUpdate = applyPropertyUpdate;
databaseViewAddView = databaseViewAddView;
databaseViewInitEmpty = databaseViewInitEmpty;
updateCell = updateCell;
updateView = updateView;
viewPresets = viewPresets;
initDatabaseBlock(
doc: Doc,
model: BlockModel,
databaseId: string,
viewType: string,
isAppendNewRow = true
) {
const blockModel = doc.getBlock(databaseId)?.model as
| DatabaseBlockModel
| undefined;
if (!blockModel) {
return;
}
databaseViewInitTemplate(blockModel, viewType);
if (isAppendNewRow) {
const parent = doc.getParent(model);
if (!parent) return;
doc.addBlock('affine:paragraph', {}, parent.id);
}
applyPropertyUpdate(blockModel);
}
}

View File

@@ -1,21 +0,0 @@
import {
BlockViewExtension,
CommandExtension,
type ExtensionType,
FlavourExtension,
} from '@blocksuite/block-std';
import { DatabaseSelectionExtension } from '@blocksuite/data-view';
import { literal } from 'lit/static-html.js';
import { DatabaseBlockAdapterExtensions } from './adapters/extension.js';
import { commands } from './commands.js';
import { DatabaseBlockService } from './database-service.js';
export const DatabaseBlockSpec: ExtensionType[] = [
FlavourExtension('affine:database'),
DatabaseBlockService,
CommandExtension(commands),
BlockViewExtension('affine:database', literal`affine-database`),
DatabaseSelectionExtension,
DatabaseBlockAdapterExtensions,
].flat();

View File

@@ -1,161 +0,0 @@
import { DefaultInlineManagerExtension } from '@blocksuite/affine-components/rich-text';
import type { EditorHost } from '@blocksuite/block-std';
import { ShadowlessElement } from '@blocksuite/block-std';
import type { DetailSlotProps } from '@blocksuite/data-view';
import type {
KanbanSingleView,
TableSingleView,
} from '@blocksuite/data-view/view-presets';
import { WithDisposable } from '@blocksuite/global/utils';
import { css, html } from 'lit';
import { property } from 'lit/decorators.js';
export class BlockRenderer
extends WithDisposable(ShadowlessElement)
implements DetailSlotProps
{
static override styles = css`
database-datasource-block-renderer {
padding-top: 36px;
padding-bottom: 16px;
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 12px;
border-bottom: 1px solid var(--affine-border-color);
font-size: var(--affine-font-base);
line-height: var(--affine-line-height);
}
database-datasource-block-renderer .tips-placeholder {
display: none;
}
database-datasource-block-renderer rich-text {
font-size: 15px;
line-height: 24px;
}
database-datasource-block-renderer.empty rich-text::before {
content: 'Untitled';
position: absolute;
color: var(--affine-text-disable-color);
font-size: 15px;
line-height: 24px;
user-select: none;
pointer-events: none;
}
.database-block-detail-header-icon {
width: 20px;
height: 20px;
padding: 2px;
border-radius: 4px;
background-color: var(--affine-background-secondary-color);
}
.database-block-detail-header-icon svg {
width: 16px;
height: 16px;
}
`;
get attributeRenderer() {
return this.inlineManager.getRenderer();
}
get attributesSchema() {
return this.inlineManager.getSchema();
}
get inlineManager() {
return this.host.std.get(DefaultInlineManagerExtension.identifier);
}
get model() {
return this.host?.doc.getBlock(this.rowId)?.model;
}
get service() {
return this.host.std.getService('affine:database');
}
override connectedCallback() {
super.connectedCallback();
if (this.model && this.model.text) {
const cb = () => {
if (this.model?.text?.length == 0) {
this.classList.add('empty');
} else {
this.classList.remove('empty');
}
};
this.model.text.yText.observe(cb);
this.disposables.add(() => {
this.model?.text?.yText.unobserve(cb);
});
}
this._disposables.addFromEvent(
this,
'keydown',
e => {
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) {
e.stopPropagation();
e.preventDefault();
return;
}
if (
e.key === 'Backspace' &&
!e.shiftKey &&
!e.metaKey &&
this.model?.text?.length === 0
) {
e.stopPropagation();
e.preventDefault();
return;
}
},
true
);
}
protected override render(): unknown {
const model = this.model;
if (!model) {
return;
}
return html`
${this.renderIcon()}
<rich-text
.yText=${model.text}
.attributesSchema=${this.attributesSchema}
.attributeRenderer=${this.attributeRenderer}
.embedChecker=${this.inlineManager.embedChecker}
.markdownShortcutHandler=${this.inlineManager.markdownShortcutHandler}
class="inline-editor"
></rich-text>
`;
}
renderIcon() {
const iconColumn = this.view.mainProperties$.value.iconColumn;
if (!iconColumn) {
return;
}
return html` <div class="database-block-detail-header-icon">
${this.view.cellValueGet(this.rowId, iconColumn)}
</div>`;
}
@property({ attribute: false })
accessor host!: EditorHost;
@property({ attribute: false })
accessor openDoc!: (docId: string) => void;
@property({ attribute: false })
accessor rowId!: string;
@property({ attribute: false })
accessor view!: TableSingleView | KanbanSingleView;
}

View File

@@ -1,126 +0,0 @@
import type {
DatabaseBlockModel,
RootBlockModel,
} from '@blocksuite/affine-model';
import { REFERENCE_NODE } from '@blocksuite/affine-shared/consts';
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import {
createDefaultDoc,
matchFlavours,
} from '@blocksuite/affine-shared/utils';
import { type EditorHost, ShadowlessElement } from '@blocksuite/block-std';
import type { DetailSlotProps, SingleView } from '@blocksuite/data-view';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
import type { BaseTextAttributes } from '@blocksuite/inline';
import { computed } from '@preact/signals-core';
import { css, html } from 'lit';
import { property } from 'lit/decorators.js';
import { isPureText } from '../utils/title-doc.js';
export class NoteRenderer
extends SignalWatcher(WithDisposable(ShadowlessElement))
implements DetailSlotProps
{
static override styles = css`
database-datasource-note-renderer {
width: 100%;
--affine-editor-side-padding: 0;
flex: 1;
}
`;
@property({ attribute: false })
accessor rowId!: string;
rowText$ = computed(() => {
return this.databaseBlock.doc.getBlock(this.rowId)?.model?.text;
});
allowCreateDoc$ = computed(() => {
return isPureText(this.rowText$.value);
});
get databaseBlock(): DatabaseBlockModel {
return this.model;
}
addNote() {
const collection = this.host?.std.collection;
if (!collection) {
return;
}
const note = createDefaultDoc(collection);
if (note) {
this.openDoc(note.id);
const rowContent = this.rowText$.value?.toString();
this.rowText$.value?.replace(
0,
this.rowText$.value.length,
REFERENCE_NODE,
{
reference: {
type: 'LinkedPage',
pageId: note.id,
},
} satisfies AffineTextAttributes as BaseTextAttributes
);
collection.setDocMeta(note.id, { title: rowContent });
if (note.root) {
(note.root as RootBlockModel).title.insert(rowContent ?? '', 0);
note.root.children
.find(child => child.flavour === 'affine:note')
?.children.find(block =>
matchFlavours(block, [
'affine:paragraph',
'affine:list',
'affine:code',
])
);
}
// Track when a linked doc is created in database title column
this.host.std.getOptional(TelemetryProvider)?.track('LinkedDocCreated', {
segment: 'database',
module: 'center peek in database',
type: 'turn into',
parentFlavour: 'affine:database',
});
}
}
protected override render(): unknown {
return html`
<div
style="height: 1px;max-width: var(--affine-editor-width);background-color: var(--affine-border-color);margin: auto;margin-bottom: 16px"
></div>
${this.renderNote()}
`;
}
renderNote() {
if (this.allowCreateDoc$.value) {
return html` <div>
<div
@click="${this.addNote}"
style="max-width: var(--affine-editor-width);margin: auto;cursor: pointer;color: var(--affine-text-disable-color)"
>
Click to create a linked doc in center peek.
</div>
</div>`;
}
return;
}
@property({ attribute: false })
accessor host!: EditorHost;
@property({ attribute: false })
accessor model!: DatabaseBlockModel;
@property({ attribute: false })
accessor openDoc!: (docId: string) => void;
@property({ attribute: false })
accessor view!: SingleView;
}

View File

@@ -1,30 +0,0 @@
import type { DatabaseBlockModel } from '@blocksuite/affine-model';
import type { insertDatabaseBlockCommand } from './commands.js';
export function effects() {
// TODO(@L-Sun): move other effects to this file
}
declare global {
namespace BlockSuite {
interface BlockModels {
'affine:database': DatabaseBlockModel;
}
interface CommandContext {
insertedDatabaseBlockId?: string;
}
interface Commands {
/**
* insert a database block after or before the current block selection
* @param latex the LaTeX content. A input dialog will be shown if not provided
* @param removeEmptyLine remove the current block if it is empty
* @param place where to insert the LaTeX block
* @returns the id of the inserted LaTeX block
*/
insertDatabaseBlock: typeof insertDatabaseBlockCommand;
}
}
}

View File

@@ -1,16 +0,0 @@
import type { DatabaseBlockModel } from '@blocksuite/affine-model';
export * from './adapters/markdown.js';
export type { DatabaseOptionsConfig } from './config.js';
export * from './data-source.js';
export * from './database-block.js';
export * from './database-service.js';
export * from './database-spec.js';
export { databaseBlockColumns } from './properties/index.js';
declare global {
namespace BlockSuite {
interface BlockModels {
'affine:database': DatabaseBlockModel;
}
}
}

View File

@@ -1,176 +0,0 @@
import { clamp } from '@blocksuite/affine-shared/utils';
import {
createPropertyConvert,
getTagColor,
type SelectTag,
} from '@blocksuite/data-view';
import { presetPropertyConverts } from '@blocksuite/data-view/property-presets';
import { propertyModelPresets } from '@blocksuite/data-view/property-pure-presets';
import { nanoid, Text } from '@blocksuite/store';
import { richTextColumnModelConfig } from './rich-text/define.js';
export const databasePropertyConverts = [
...presetPropertyConverts,
createPropertyConvert(
richTextColumnModelConfig,
propertyModelPresets.selectPropertyModelConfig,
(_property, cells) => {
const options: Record<string, SelectTag> = {};
const getTag = (name: string) => {
if (options[name]) return options[name];
const tag: SelectTag = {
id: nanoid(),
value: name,
color: getTagColor(),
};
options[name] = tag;
return tag;
};
return {
cells: cells.map(v => {
const tags = v?.toString().split(',');
const value = tags?.[0]?.trim();
if (value) {
return getTag(value).id;
}
return undefined;
}),
property: {
options: Object.values(options),
},
};
}
),
createPropertyConvert(
richTextColumnModelConfig,
propertyModelPresets.multiSelectPropertyModelConfig,
(_property, cells) => {
const options: Record<string, SelectTag> = {};
// eslint-disable-next-line sonarjs/no-identical-functions
const getTag = (name: string) => {
if (options[name]) return options[name];
const tag: SelectTag = {
id: nanoid(),
value: name,
color: getTagColor(),
};
options[name] = tag;
return tag;
};
return {
cells: cells.map(v => {
const result: string[] = [];
const values = v?.toString().split(',');
values?.forEach(value => {
value = value.trim();
if (value) {
result.push(getTag(value).id);
}
});
return result;
}),
property: {
options: Object.values(options),
},
};
}
),
createPropertyConvert(
richTextColumnModelConfig,
propertyModelPresets.numberPropertyModelConfig,
(_property, cells) => {
return {
property: {
decimal: 0,
format: 'number' as const,
},
cells: cells.map(v => {
const num = v ? parseFloat(v.toString()) : NaN;
return isNaN(num) ? undefined : num;
}),
};
}
),
createPropertyConvert(
richTextColumnModelConfig,
propertyModelPresets.progressPropertyModelConfig,
(_property, cells) => {
return {
property: {},
cells: cells.map(v => {
const progress = v ? parseInt(v.toString()) : NaN;
return !isNaN(progress) ? clamp(progress, 0, 100) : undefined;
}),
};
}
),
createPropertyConvert(
richTextColumnModelConfig,
propertyModelPresets.checkboxPropertyModelConfig,
(_property, cells) => {
const truthyValues = new Set(['yes', 'true']);
return {
property: {},
cells: cells.map(v =>
v && truthyValues.has(v.toString().toLowerCase()) ? true : undefined
),
};
}
),
createPropertyConvert(
propertyModelPresets.checkboxPropertyModelConfig,
richTextColumnModelConfig,
(_property, cells) => {
return {
property: {},
cells: cells.map(v => new Text(v ? 'Yes' : 'No').yText),
};
}
),
createPropertyConvert(
propertyModelPresets.multiSelectPropertyModelConfig,
richTextColumnModelConfig,
(property, cells) => {
const optionMap = Object.fromEntries(
property.options.map(v => [v.id, v])
);
return {
property: {},
cells: cells.map(
arr =>
new Text(arr?.map(v => optionMap[v]?.value ?? '').join(',')).yText
),
};
}
),
createPropertyConvert(
propertyModelPresets.numberPropertyModelConfig,
richTextColumnModelConfig,
(_property, cells) => ({
property: {},
cells: cells.map(v => new Text(v?.toString()).yText),
})
),
createPropertyConvert(
propertyModelPresets.progressPropertyModelConfig,
richTextColumnModelConfig,
(_property, cells) => ({
property: {},
cells: cells.map(v => new Text(v?.toString()).yText),
})
),
createPropertyConvert(
propertyModelPresets.selectPropertyModelConfig,
richTextColumnModelConfig,
(property, cells) => {
const optionMap = Object.fromEntries(
property.options.map(v => [v.id, v])
);
return {
property: {},
cells: cells.map(v => new Text(v ? optionMap[v]?.value : '').yText),
};
}
),
];

View File

@@ -1,38 +0,0 @@
import type { PropertyMetaConfig } from '@blocksuite/data-view';
import { propertyPresets } from '@blocksuite/data-view/property-presets';
import { linkColumnConfig } from './link/cell-renderer.js';
import { richTextColumnConfig } from './rich-text/cell-renderer.js';
import { titleColumnConfig } from './title/cell-renderer.js';
export * from './converts.js';
const {
checkboxPropertyConfig,
datePropertyConfig,
multiSelectPropertyConfig,
numberPropertyConfig,
progressPropertyConfig,
selectPropertyConfig,
} = propertyPresets;
export const databaseBlockColumns = {
checkboxColumnConfig: checkboxPropertyConfig,
dateColumnConfig: datePropertyConfig,
multiSelectColumnConfig: multiSelectPropertyConfig,
numberColumnConfig: numberPropertyConfig,
progressColumnConfig: progressPropertyConfig,
selectColumnConfig: selectPropertyConfig,
linkColumnConfig,
richTextColumnConfig,
};
export const databaseBlockPropertyList = Object.values(databaseBlockColumns);
export const databaseBlockHiddenColumns = [
propertyPresets.imagePropertyConfig,
titleColumnConfig,
];
const databaseBlockAllColumns = [
...databaseBlockPropertyList,
...databaseBlockHiddenColumns,
];
export const databaseBlockAllPropertyMap = Object.fromEntries(
databaseBlockAllColumns.map(v => [v.type, v as PropertyMetaConfig])
);

View File

@@ -1,256 +0,0 @@
import { RefNodeSlotsProvider } from '@blocksuite/affine-components/rich-text';
import { ParseDocUrlProvider } from '@blocksuite/affine-shared/services';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import {
isValidUrl,
normalizeUrl,
stopPropagation,
} from '@blocksuite/affine-shared/utils';
import {
BaseCellRenderer,
createFromBaseCellRenderer,
createIcon,
} from '@blocksuite/data-view';
import { EditIcon } from '@blocksuite/icons/lit';
import { baseTheme } from '@toeverything/theme';
import { css, nothing, unsafeCSS } from 'lit';
import { query, state } from 'lit/decorators.js';
import { html } from 'lit/static-html.js';
import { HostContextKey } from '../../context/host-context.js';
import { linkColumnModelConfig } from './define.js';
export class LinkCell extends BaseCellRenderer<string> {
static override styles = css`
affine-database-link-cell {
width: 100%;
user-select: none;
position: relative;
}
affine-database-link-cell:hover .affine-database-link-icon {
visibility: visible;
}
.affine-database-link {
display: flex;
position: relative;
align-items: center;
width: 100%;
height: 100%;
outline: none;
overflow: hidden;
font-size: var(--data-view-cell-text-size);
line-height: var(--data-view-cell-text-line-height);
word-break: break-all;
}
affine-database-link-node {
flex: 1;
word-break: break-all;
}
.affine-database-link-icon {
position: absolute;
right: 8px;
top: 8px;
display: flex;
align-items: center;
visibility: hidden;
cursor: pointer;
background: ${unsafeCSSVarV2('button/iconButtonSolid')};
color: ${unsafeCSSVarV2('icon/primary')};
box-shadow: var(--affine-button-shadow);
border-radius: 4px;
font-size: 14px;
padding: 2px;
}
.affine-database-link-icon:hover {
background: var(--affine-hover-color);
}
.data-view-link-column-linked-doc {
text-decoration: underline;
text-decoration-color: var(--affine-divider-color);
transition: text-decoration-color 0.2s ease-out;
cursor: pointer;
}
.data-view-link-column-linked-doc:hover {
text-decoration-color: var(--affine-icon-color);
}
`;
private readonly _onClick = (event: Event) => {
event.stopPropagation();
const value = this.value ?? '';
if (!value || !isValidUrl(value)) {
this.selectCurrentCell(true);
return;
}
if (isValidUrl(value)) {
const target = event.target as HTMLElement;
const link = target.querySelector<HTMLAnchorElement>('.link-node');
if (link) {
event.preventDefault();
link.click();
}
return;
}
};
private readonly _onEdit = (e: Event) => {
e.stopPropagation();
this.selectCurrentCell(true);
};
private preValue?: string;
openDoc = (e: MouseEvent) => {
e.stopPropagation();
if (!this.docId) {
return;
}
const std = this.std;
if (!std) {
return;
}
std
.getOptional(RefNodeSlotsProvider)
?.docLinkClicked.emit({ pageId: this.docId });
};
get std() {
const host = this.view.contextGet(HostContextKey);
return host?.std;
}
override render() {
const linkText = this.value ?? '';
const docName =
this.docId && this.std?.collection.getDoc(this.docId)?.meta?.title;
return html`
<div class="affine-database-link" @click="${this._onClick}">
${docName
? html`<span
class="data-view-link-column-linked-doc"
@click="${this.openDoc}"
>${docName}</span
>`
: html` <affine-database-link-node
.link="${linkText}"
></affine-database-link-node>`}
</div>
${docName || linkText
? html` <div class="affine-database-link-icon" @click="${this._onEdit}">
${EditIcon()}
</div>`
: nothing}
`;
}
override updated() {
if (this.value !== this.preValue) {
const std = this.std;
this.preValue = this.value;
if (!this.value || !isValidUrl(this.value)) {
this.docId = undefined;
return;
}
this.docId =
std?.getOptional(ParseDocUrlProvider)?.parseDocUrl(this.value)?.docId ??
undefined;
}
}
@state()
accessor docId: string | undefined = undefined;
}
export class LinkCellEditing extends BaseCellRenderer<string> {
static override styles = css`
affine-database-link-cell-editing {
width: 100%;
cursor: text;
}
.affine-database-link-editing {
display: flex;
align-items: center;
width: 100%;
padding: 0;
border: none;
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
color: var(--affine-text-primary-color);
font-weight: 400;
background-color: transparent;
font-size: var(--data-view-cell-text-size);
line-height: var(--data-view-cell-text-line-height);
word-break: break-all;
}
.affine-database-link-editing:focus {
outline: none;
}
`;
private readonly _focusEnd = () => {
const end = this._container.value.length;
this._container.focus();
this._container.setSelectionRange(end, end);
};
private readonly _onKeydown = (e: KeyboardEvent) => {
if (e.key === 'Enter' && !e.isComposing) {
this._setValue();
setTimeout(() => {
this.selectCurrentCell(false);
});
}
};
private readonly _setValue = (value: string = this._container.value) => {
let url = value;
if (isValidUrl(value)) {
url = normalizeUrl(value);
}
this.onChange(url);
this._container.value = url;
};
override firstUpdated() {
this._focusEnd();
}
override onExitEditMode() {
this._setValue();
}
override render() {
const linkText = this.value ?? '';
return html`<input
class="affine-database-link-editing link"
.value="${linkText}"
@keydown="${this._onKeydown}"
@pointerdown="${stopPropagation}"
/>`;
}
@query('.affine-database-link-editing')
private accessor _container!: HTMLInputElement;
}
export const linkColumnConfig = linkColumnModelConfig.createPropertyMeta({
icon: createIcon('LinkIcon'),
cellRenderer: {
view: createFromBaseCellRenderer(LinkCell),
edit: createFromBaseCellRenderer(LinkCellEditing),
},
});

View File

@@ -1,41 +0,0 @@
import { isValidUrl } from '@blocksuite/affine-shared/utils';
import { ShadowlessElement } from '@blocksuite/block-std';
import { css, html } from 'lit';
import { property } from 'lit/decorators.js';
export class LinkNode extends ShadowlessElement {
static override styles = css`
.link-node {
word-break: break-all;
color: var(--affine-link-color);
fill: var(--affine-link-color);
cursor: pointer;
font-weight: normal;
font-style: normal;
text-decoration: none;
}
`;
protected override render() {
if (!isValidUrl(this.link)) {
return html`<span class="normal-text">${this.link}</span>`;
}
return html`<a
class="link-node"
href=${this.link}
rel="noopener noreferrer"
target="_blank"
><span class="link-node-text">${this.link}</span></a
>`;
}
@property({ attribute: false })
accessor link!: string;
}
declare global {
interface HTMLElementTagNameMap {
'affine-database-link-node': LinkNode;
}
}

View File

@@ -1,18 +0,0 @@
import { propertyType, t } from '@blocksuite/data-view';
export const linkColumnType = propertyType('link');
export const linkColumnModelConfig = linkColumnType.modelConfig<string>({
name: 'Link',
type: () => t.string.instance(),
defaultData: () => ({}),
cellToString: ({ value }) => value?.toString() ?? '',
cellFromString: ({ value }) => {
return {
value: value,
};
},
cellToJson: ({ value }) => value ?? null,
cellFromJson: ({ value }) => (typeof value !== 'string' ? undefined : value),
isEmpty: ({ value }) => value == null || value.length == 0,
});

View File

@@ -1,398 +0,0 @@
import {
type AffineInlineEditor,
DefaultInlineManagerExtension,
type RichText,
} from '@blocksuite/affine-components/rich-text';
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import { getViewportElement } from '@blocksuite/affine-shared/utils';
import {
BaseCellRenderer,
createFromBaseCellRenderer,
createIcon,
} from '@blocksuite/data-view';
import { IS_MAC } from '@blocksuite/global/env';
import { assertExists } from '@blocksuite/global/utils';
import { Text } from '@blocksuite/store';
import { css, nothing, type PropertyValues } from 'lit';
import { query } from 'lit/decorators.js';
import { keyed } from 'lit/directives/keyed.js';
import { html } from 'lit/static-html.js';
import { HostContextKey } from '../../context/host-context.js';
import type { DatabaseBlockComponent } from '../../database-block.js';
import { richTextColumnModelConfig } from './define.js';
function toggleStyle(
inlineEditor: AffineInlineEditor,
attrs: AffineTextAttributes
): void {
const inlineRange = inlineEditor.getInlineRange();
if (!inlineRange) return;
const root = inlineEditor.rootElement;
if (!root) {
return;
}
const deltas = inlineEditor.getDeltasByInlineRange(inlineRange);
let oldAttributes: AffineTextAttributes = {};
for (const [delta] of deltas) {
const attributes = delta.attributes;
if (!attributes) {
continue;
}
oldAttributes = { ...attributes };
}
const newAttributes = Object.fromEntries(
Object.entries(attrs).map(([k, v]) => {
if (
typeof v === 'boolean' &&
v === (oldAttributes as Record<string, unknown>)[k]
) {
return [k, !v];
} else {
return [k, v];
}
})
);
inlineEditor.formatText(inlineRange, newAttributes, {
mode: 'merge',
});
root.blur();
inlineEditor.syncInlineRange();
}
export class RichTextCell extends BaseCellRenderer<Text> {
static override styles = css`
affine-database-rich-text-cell {
display: flex;
align-items: center;
width: 100%;
user-select: none;
}
.affine-database-rich-text {
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
height: 100%;
outline: none;
font-size: var(--data-view-cell-text-size);
line-height: var(--data-view-cell-text-line-height);
word-break: break-all;
}
.affine-database-rich-text v-line {
display: flex !important;
align-items: center;
height: 100%;
width: 100%;
}
.affine-database-rich-text v-line > div {
flex-grow: 1;
}
`;
get attributeRenderer() {
return this.inlineManager?.getRenderer();
}
get attributesSchema() {
return this.inlineManager?.getSchema();
}
get inlineEditor() {
assertExists(this._richTextElement);
const inlineEditor = this._richTextElement.inlineEditor;
assertExists(inlineEditor);
return inlineEditor;
}
get inlineManager() {
return this.view
.contextGet(HostContextKey)
?.std.get(DefaultInlineManagerExtension.identifier);
}
get service() {
return this.view
.contextGet(HostContextKey)
?.std.getService('affine:database');
}
get topContenteditableElement() {
const databaseBlock =
this.closest<DatabaseBlockComponent>('affine-database');
return databaseBlock?.topContenteditableElement;
}
private changeUserSelectAccordToReadOnly() {
if (this && this instanceof HTMLElement) {
this.style.userSelect = this.readonly ? 'text' : 'none';
}
}
override connectedCallback() {
super.connectedCallback();
this.changeUserSelectAccordToReadOnly();
}
override render() {
if (!this.service) return nothing;
if (!this.value || !(this.value instanceof Text)) {
return html`<div class="affine-database-rich-text"></div>`;
}
return keyed(
this.value,
html`<rich-text
.yText=${this.value}
.attributesSchema=${this.attributesSchema}
.attributeRenderer=${this.attributeRenderer}
.embedChecker=${this.inlineManager?.embedChecker}
.markdownShortcutHandler=${this.inlineManager?.markdownShortcutHandler}
.readonly=${true}
class="affine-database-rich-text inline-editor"
></rich-text>`
);
}
override updated(changedProperties: PropertyValues) {
if (changedProperties.has('readonly')) {
this.changeUserSelectAccordToReadOnly();
}
}
@query('rich-text')
private accessor _richTextElement: RichText | null = null;
}
export class RichTextCellEditing extends BaseCellRenderer<Text> {
static override styles = css`
affine-database-rich-text-cell-editing {
display: flex;
align-items: center;
width: 100%;
min-width: 1px;
cursor: text;
}
.affine-database-rich-text {
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
height: 100%;
outline: none;
}
.affine-database-rich-text v-line {
display: flex !important;
align-items: center;
height: 100%;
width: 100%;
}
.affine-database-rich-text v-line > div {
flex-grow: 1;
}
`;
private readonly _handleKeyDown = (event: KeyboardEvent) => {
if (event.key !== 'Escape') {
if (event.key === 'Tab') {
event.preventDefault();
return;
}
event.stopPropagation();
}
if (event.key === 'Enter' && !event.isComposing) {
if (event.shiftKey) {
// soft enter
this._onSoftEnter();
} else {
// exit editing
this.selectCurrentCell(false);
}
event.preventDefault();
return;
}
const inlineEditor = this.inlineEditor;
switch (event.key) {
// bold ctrl+b
case 'B':
case 'b':
if (event.metaKey || event.ctrlKey) {
event.preventDefault();
toggleStyle(this.inlineEditor, { bold: true });
}
break;
// italic ctrl+i
case 'I':
case 'i':
if (event.metaKey || event.ctrlKey) {
event.preventDefault();
toggleStyle(this.inlineEditor, { italic: true });
}
break;
// underline ctrl+u
case 'U':
case 'u':
if (event.metaKey || event.ctrlKey) {
event.preventDefault();
toggleStyle(this.inlineEditor, { underline: true });
}
break;
// strikethrough ctrl+shift+s
case 'S':
case 's':
if ((event.metaKey || event.ctrlKey) && event.shiftKey) {
event.preventDefault();
toggleStyle(inlineEditor, { strike: true });
}
break;
// inline code ctrl+shift+e
case 'E':
case 'e':
if ((event.metaKey || event.ctrlKey) && event.shiftKey) {
event.preventDefault();
toggleStyle(inlineEditor, { code: true });
}
break;
default:
break;
}
};
private readonly _initYText = (text?: string) => {
const yText = new Text(text);
this.onChange(yText);
};
private readonly _onSoftEnter = () => {
if (this.value && this.inlineEditor) {
const inlineRange = this.inlineEditor.getInlineRange();
assertExists(inlineRange);
const text = new Text(this.inlineEditor.yText);
text.replace(inlineRange.index, inlineRange.length, '\n');
this.inlineEditor.setInlineRange({
index: inlineRange.index + 1,
length: 0,
});
}
};
get attributeRenderer() {
return this.inlineManager?.getRenderer();
}
get attributesSchema() {
return this.inlineManager?.getSchema();
}
// eslint-disable-next-line sonarjs/no-identical-functions
get inlineEditor() {
assertExists(this._richTextElement);
const inlineEditor = this._richTextElement.inlineEditor;
assertExists(inlineEditor);
return inlineEditor;
}
// eslint-disable-next-line sonarjs/no-identical-functions
get inlineManager() {
return this.view
.contextGet(HostContextKey)
?.std.get(DefaultInlineManagerExtension.identifier);
}
// eslint-disable-next-line sonarjs/no-identical-functions
get service() {
return this.view
.contextGet(HostContextKey)
?.std.getService('affine:database');
}
// eslint-disable-next-line sonarjs/no-identical-functions
get topContenteditableElement() {
const databaseBlock =
this.closest<DatabaseBlockComponent>('affine-database');
return databaseBlock?.topContenteditableElement;
}
override connectedCallback() {
super.connectedCallback();
if (!this.value || typeof this.value === 'string') {
this._initYText(this.value);
}
const selectAll = (e: KeyboardEvent) => {
if (e.key === 'a' && (IS_MAC ? e.metaKey : e.ctrlKey)) {
e.stopPropagation();
e.preventDefault();
this.inlineEditor.selectAll();
}
};
this.addEventListener('keydown', selectAll);
this.disposables.addFromEvent(this, 'keydown', selectAll);
}
override firstUpdated() {
this._richTextElement?.updateComplete
.then(() => {
this.disposables.add(
this.inlineEditor.slots.keydown.on(this._handleKeyDown)
);
this.inlineEditor.focusEnd();
})
.catch(console.error);
}
override render() {
if (!this.service) return nothing;
return html`<rich-text
.yText=${this.value}
.inlineEventSource=${this.topContenteditableElement}
.attributesSchema=${this.attributesSchema}
.attributeRenderer=${this.attributeRenderer}
.embedChecker=${this.inlineManager?.embedChecker}
.markdownShortcutHandler=${this.inlineManager?.markdownShortcutHandler}
.verticalScrollContainerGetter=${() =>
this.topContenteditableElement?.host
? getViewportElement(this.topContenteditableElement.host)
: null}
class="affine-database-rich-text inline-editor"
></rich-text>`;
}
@query('rich-text')
private accessor _richTextElement: RichText | null = null;
}
declare global {
interface HTMLElementTagNameMap {
'affine-database-rich-text-cell-editing': RichTextCellEditing;
}
}
export const richTextColumnConfig =
richTextColumnModelConfig.createPropertyMeta({
icon: createIcon('TextIcon'),
cellRenderer: {
view: createFromBaseCellRenderer(RichTextCell),
edit: createFromBaseCellRenderer(RichTextCellEditing),
},
});

View File

@@ -1,34 +0,0 @@
import { propertyType, t } from '@blocksuite/data-view';
import { Text } from '@blocksuite/store';
import { type RichTextCellType, toYText } from '../utils.js';
export const richTextColumnType = propertyType('rich-text');
export const richTextColumnModelConfig =
richTextColumnType.modelConfig<RichTextCellType>({
name: 'Text',
type: () => t.richText.instance(),
defaultData: () => ({}),
cellToString: ({ value }) => value?.toString() ?? '',
cellFromString: ({ value }) => {
return {
value: new Text(value),
};
},
cellToJson: ({ value }) => value?.toString() ?? null,
cellFromJson: ({ value }) =>
typeof value !== 'string' ? undefined : new Text(value),
onUpdate: ({ value, callback }) => {
const yText = toYText(value);
yText.observe(callback);
callback();
return {
dispose: () => {
yText.unobserve(callback);
},
};
},
isEmpty: ({ value }) => value == null || value.length === 0,
values: ({ value }) => (value?.toString() ? [value.toString()] : []),
});

View File

@@ -1,30 +0,0 @@
import {
type CellRenderProps,
createFromBaseCellRenderer,
createIcon,
uniMap,
} from '@blocksuite/data-view';
import { TableSingleView } from '@blocksuite/data-view/view-presets';
import { titlePurePropertyConfig } from './define.js';
import { HeaderAreaTextCell, HeaderAreaTextCellEditing } from './text.js';
export const titleColumnConfig = titlePurePropertyConfig.createPropertyMeta({
icon: createIcon('TitleIcon'),
cellRenderer: {
view: uniMap(
createFromBaseCellRenderer(HeaderAreaTextCell),
(props: CellRenderProps) => ({
...props,
showIcon: props.cell.view instanceof TableSingleView,
})
),
edit: uniMap(
createFromBaseCellRenderer(HeaderAreaTextCellEditing),
(props: CellRenderProps) => ({
...props,
showIcon: props.cell.view instanceof TableSingleView,
})
),
},
});

View File

@@ -1,62 +0,0 @@
import { propertyType, t } from '@blocksuite/data-view';
import { Text } from '@blocksuite/store';
import { HostContextKey } from '../../context/host-context.js';
import { isLinkedDoc } from '../../utils/title-doc.js';
export const titleColumnType = propertyType('title');
export const titlePurePropertyConfig = titleColumnType.modelConfig<Text>({
name: 'Title',
type: () => t.richText.instance(),
defaultData: () => ({}),
cellToString: ({ value }) => value?.toString() ?? '',
cellFromString: ({ value }) => {
return {
value: value,
};
},
cellToJson: ({ value, dataSource }) => {
const host = dataSource.contextGet(HostContextKey);
if (host) {
const collection = host.std.collection;
const deltas = value.deltas$.value;
const text = deltas
.map(delta => {
if (isLinkedDoc(delta)) {
const linkedDocId = delta.attributes?.reference?.pageId as string;
return collection.getDoc(linkedDocId)?.meta?.title;
}
return delta.insert;
})
.join('');
return text;
}
return value?.toString() ?? null;
},
cellFromJson: ({ value }) =>
typeof value !== 'string' ? undefined : new Text(value),
onUpdate: ({ value, callback }) => {
value.yText.observe(callback);
callback();
return {
dispose: () => {
value.yText.unobserve(callback);
},
};
},
valueUpdate: ({ value, newValue }) => {
const v = newValue as unknown;
if (typeof v === 'string') {
value.replace(0, value.length, v);
return value;
}
if (v == null) {
value.replace(0, value.length, '');
return value;
}
return newValue;
},
isEmpty: ({ value }) => value == null || value.length === 0,
values: ({ value }) => (value?.toString() ? [value.toString()] : []),
});

View File

@@ -1,21 +0,0 @@
import { BaseCellRenderer } from '@blocksuite/data-view';
import { css, html } from 'lit';
export class IconCell extends BaseCellRenderer<string> {
static override styles = css`
affine-database-image-cell {
width: 100%;
height: 100%;
display: flex;
align-items: center;
}
affine-database-image-cell img {
width: 20px;
height: 20px;
}
`;
override render() {
return html`<img src=${this.value ?? ''}></img>`;
}
}

View File

@@ -1,429 +0,0 @@
import {
DefaultInlineManagerExtension,
type RichText,
} from '@blocksuite/affine-components/rich-text';
import type { RootBlockModel } from '@blocksuite/affine-model';
import {
ParseDocUrlProvider,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
import {
getViewportElement,
isValidUrl,
} from '@blocksuite/affine-shared/utils';
import { BaseCellRenderer } from '@blocksuite/data-view';
import { IS_MAC } from '@blocksuite/global/env';
import { assertExists } from '@blocksuite/global/utils';
import { LinkedPageIcon } from '@blocksuite/icons/lit';
import type { DeltaInsert } from '@blocksuite/inline';
import type { BlockSnapshot, Text } from '@blocksuite/store';
import { computed, effect, signal } from '@preact/signals-core';
import { css, type TemplateResult } from 'lit';
import { property, query } from 'lit/decorators.js';
import { html } from 'lit/static-html.js';
import { ClipboardAdapter } from '../../../root-block/clipboard/adapter.js';
import { HostContextKey } from '../../context/host-context.js';
import type { DatabaseBlockComponent } from '../../database-block.js';
import { getSingleDocIdFromText } from '../../utils/title-doc.js';
const styles = css`
data-view-header-area-text {
width: 100%;
display: flex;
}
data-view-header-area-text rich-text {
pointer-events: none;
user-select: none;
}
data-view-header-area-text-editing {
width: 100%;
display: flex;
cursor: text;
}
.data-view-header-area-rich-text {
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
height: 100%;
outline: none;
word-break: break-all;
font-size: var(--data-view-cell-text-size);
line-height: var(--data-view-cell-text-line-height);
}
.data-view-header-area-rich-text v-line {
display: flex !important;
align-items: center;
height: 100%;
width: 100%;
}
.data-view-header-area-rich-text v-line > div {
flex-grow: 1;
}
.data-view-header-area-icon {
height: max-content;
display: flex;
align-items: center;
margin-right: 8px;
padding: 2px;
border-radius: 4px;
margin-top: 2px;
background-color: var(--affine-background-secondary-color);
}
.data-view-header-area-icon svg {
width: 14px;
height: 14px;
fill: var(--affine-icon-color);
color: var(--affine-icon-color);
}
`;
abstract class BaseTextCell extends BaseCellRenderer<Text> {
static override styles = styles;
activity = true;
docId$ = signal<string>();
isLinkedDoc$ = computed(() => false);
linkedDocTitle$ = computed(() => {
if (!this.docId$.value) {
return this.value;
}
const doc = this.host?.std.collection.getDoc(this.docId$.value);
const root = doc?.root as RootBlockModel;
return root.title;
});
get attributeRenderer() {
return this.inlineManager?.getRenderer();
}
get attributesSchema() {
return this.inlineManager?.getSchema();
}
get host() {
return this.view.contextGet(HostContextKey);
}
get inlineEditor() {
return this.richText.inlineEditor;
}
get inlineManager() {
return this.host?.std.get(DefaultInlineManagerExtension.identifier);
}
get service() {
return this.host?.std.getService('affine:database');
}
get topContenteditableElement() {
const databaseBlock =
this.closest<DatabaseBlockComponent>('affine-database');
return databaseBlock?.topContenteditableElement;
}
override connectedCallback() {
super.connectedCallback();
const yText = this.value?.yText;
if (yText) {
const cb = () => {
const id = getSingleDocIdFromText(this.value);
this.docId$.value = id;
};
cb();
if (this.activity) {
yText.observe(cb);
this.disposables.add(() => {
yText.unobserve(cb);
});
}
}
}
protected override render(): unknown {
return html`${this.renderIcon()}${this.renderBlockText()}`;
}
abstract renderBlockText(): TemplateResult;
renderIcon() {
if (this.docId$.value) {
return html` <div class="data-view-header-area-icon">
${LinkedPageIcon()}
</div>`;
}
if (!this.showIcon) {
return;
}
const iconColumn = this.view.mainProperties$.value.iconColumn;
if (!iconColumn) return;
const icon = this.view.cellValueGet(this.cell.rowId, iconColumn) as string;
if (!icon) return;
return html` <div class="data-view-header-area-icon">${icon}</div>`;
}
abstract renderLinkedDoc(): TemplateResult;
@query('rich-text')
accessor richText!: RichText;
@property({ attribute: false })
accessor showIcon = false;
}
export class HeaderAreaTextCell extends BaseTextCell {
override renderBlockText() {
return html` <rich-text
.yText="${this.value}"
.attributesSchema="${this.attributesSchema}"
.attributeRenderer="${this.attributeRenderer}"
.embedChecker="${this.inlineManager?.embedChecker}"
.markdownShortcutHandler="${this.inlineManager?.markdownShortcutHandler}"
.readonly="${true}"
class="data-view-header-area-rich-text"
></rich-text>`;
}
override renderLinkedDoc(): TemplateResult {
return html` <rich-text
.yText="${this.linkedDocTitle$.value}"
.readonly="${true}"
class="data-view-header-area-rich-text"
></rich-text>`;
}
}
export class HeaderAreaTextCellEditing extends BaseTextCell {
private readonly _onCopy = (e: ClipboardEvent) => {
const inlineEditor = this.inlineEditor;
assertExists(inlineEditor);
const inlineRange = inlineEditor.getInlineRange();
if (!inlineRange) return;
const text = inlineEditor.yTextString.slice(
inlineRange.index,
inlineRange.index + inlineRange.length
);
e.clipboardData?.setData('text/plain', text);
e.preventDefault();
e.stopPropagation();
};
private readonly _onCut = (e: ClipboardEvent) => {
const inlineEditor = this.inlineEditor;
assertExists(inlineEditor);
const inlineRange = inlineEditor.getInlineRange();
if (!inlineRange) return;
const text = inlineEditor.yTextString.slice(
inlineRange.index,
inlineRange.index + inlineRange.length
);
inlineEditor.deleteText(inlineRange);
inlineEditor.setInlineRange({
index: inlineRange.index,
length: 0,
});
e.clipboardData?.setData('text/plain', text);
e.preventDefault();
e.stopPropagation();
};
private readonly _onPaste = (e: ClipboardEvent) => {
const inlineEditor = this.inlineEditor;
const inlineRange = inlineEditor?.getInlineRange();
if (!inlineRange) return;
if (e.clipboardData) {
try {
const getDeltas = (snapshot: BlockSnapshot): DeltaInsert[] => {
// @ts-expect-error FIXME: ts error
const text = snapshot.props?.text?.delta;
return text
? [...text, ...(snapshot.children?.flatMap(getDeltas) ?? [])]
: snapshot.children?.flatMap(getDeltas);
};
const snapshot = this.std?.clipboard?.readFromClipboard(
e.clipboardData
)[ClipboardAdapter.MIME];
const deltas = (
JSON.parse(snapshot).snapshot.content as BlockSnapshot[]
).flatMap(getDeltas);
deltas.forEach(delta => this.insertDelta(delta));
return;
} catch {
//
}
}
const text = e.clipboardData
?.getData('text/plain')
?.replace(/\r?\n|\r/g, '\n');
if (!text) return;
e.preventDefault();
e.stopPropagation();
if (isValidUrl(text)) {
const std = this.std;
const result = std?.getOptional(ParseDocUrlProvider)?.parseDocUrl(text);
if (result) {
const text = ' ';
inlineEditor?.insertText(inlineRange, text, {
reference: {
type: 'LinkedPage',
pageId: result.docId,
params: {
blockIds: result.blockIds,
elementIds: result.elementIds,
mode: result.mode,
},
},
});
inlineEditor?.setInlineRange({
index: inlineRange.index + text.length,
length: 0,
});
// Track when a linked doc is created in database title column
std?.getOptional(TelemetryProvider)?.track('LinkedDocCreated', {
module: 'database title cell',
type: 'paste',
segment: 'database',
parentFlavour: 'affine:database',
});
} else {
inlineEditor?.insertText(inlineRange, text, {
link: text,
});
inlineEditor?.setInlineRange({
index: inlineRange.index + text.length,
length: 0,
});
}
} else {
inlineEditor?.insertText(inlineRange, text);
inlineEditor?.setInlineRange({
index: inlineRange.index + text.length,
length: 0,
});
}
};
override activity = false;
insertDelta = (delta: DeltaInsert) => {
const inlineEditor = this.inlineEditor;
const range = inlineEditor?.getInlineRange();
if (!range || !delta.insert) {
return;
}
inlineEditor?.insertText(range, delta.insert, delta.attributes);
inlineEditor?.setInlineRange({
index: range.index + delta.insert.length,
length: 0,
});
};
private get std() {
return this.host?.std;
}
override connectedCallback() {
super.connectedCallback();
const selectAll = (e: KeyboardEvent) => {
if (e.key === 'a' && (IS_MAC ? e.metaKey : e.ctrlKey)) {
e.stopPropagation();
e.preventDefault();
this.inlineEditor?.selectAll();
}
};
this.addEventListener('keydown', selectAll);
this.disposables.add(() => {
this.removeEventListener('keydown', selectAll);
});
}
override firstUpdated(props: Map<string, unknown>) {
super.firstUpdated(props);
if (!this.isLinkedDoc$.value) {
this.disposables.addFromEvent(this.richText, 'copy', this._onCopy);
this.disposables.addFromEvent(this.richText, 'cut', this._onCut);
this.disposables.addFromEvent(this.richText, 'paste', this._onPaste);
}
this.richText.updateComplete
.then(() => {
this.inlineEditor?.focusEnd();
this.disposables.add(
effect(() => {
const inlineRange = this.inlineEditor?.inlineRange$.value;
if (inlineRange) {
if (!this.isEditing) {
this.selectCurrentCell(true);
}
} else {
if (this.isEditing) {
this.selectCurrentCell(false);
}
}
})
);
})
.catch(console.error);
}
override renderBlockText() {
return html` <rich-text
.yText="${this.value}"
.inlineEventSource="${this.topContenteditableElement}"
.attributesSchema="${this.attributesSchema}"
.attributeRenderer="${this.attributeRenderer}"
.embedChecker="${this.inlineManager?.embedChecker}"
.markdownShortcutHandler="${this.inlineManager?.markdownShortcutHandler}"
.readonly="${this.readonly}"
.enableClipboard="${false}"
.verticalScrollContainerGetter="${() =>
this.topContenteditableElement?.host
? getViewportElement(this.topContenteditableElement.host)
: null}"
data-parent-flavour="affine:database"
class="data-view-header-area-rich-text can-link-doc"
></rich-text>`;
}
override renderLinkedDoc(): TemplateResult {
return html` <rich-text
.yText="${this.linkedDocTitle$.value}"
.inlineEventSource="${this.topContenteditableElement}"
.readonly="${this.readonly}"
.enableClipboard="${true}"
.verticalScrollContainerGetter="${() =>
this.topContenteditableElement?.host
? getViewportElement(this.topContenteditableElement.host)
: null}"
class="data-view-header-area-rich-text"
></rich-text>`;
}
}
declare global {
interface HTMLElementTagNameMap {
'data-view-header-area-text': HeaderAreaTextCell;
'data-view-header-area-text-editing': HeaderAreaTextCellEditing;
}
}

View File

@@ -1,9 +0,0 @@
import { Text } from '@blocksuite/store';
export type RichTextCellType = Text | Text['yText'];
export const toYText = (text: RichTextCellType): Text['yText'] => {
if (text instanceof Text) {
return text.yText;
}
return text;
};

View File

@@ -1,247 +0,0 @@
import type {
Cell,
Column,
ColumnUpdater,
DatabaseBlockModel,
ViewBasicDataType,
} from '@blocksuite/affine-model';
import {
arrayMove,
insertPositionToIndex,
type InsertToPosition,
} from '@blocksuite/affine-shared/utils';
import type { BlockModel } from '@blocksuite/store';
export function addProperty(
model: DatabaseBlockModel,
position: InsertToPosition,
column: Omit<Column, 'id'> & {
id?: string;
}
): string {
const id = column.id ?? model.doc.generateBlockId();
if (model.columns.some(v => v.id === id)) {
return id;
}
model.doc.transact(() => {
const col: Column = {
...column,
id,
};
model.columns.splice(
insertPositionToIndex(position, model.columns),
0,
col
);
});
return id;
}
export function applyCellsUpdate(model: DatabaseBlockModel) {
model.doc.updateBlock(model, {
cells: model.cells,
});
}
export function applyPropertyUpdate(model: DatabaseBlockModel) {
model.doc.updateBlock(model, {
columns: model.columns,
});
}
export function applyViewsUpdate(model: DatabaseBlockModel) {
model.doc.updateBlock(model, {
views: model.views,
});
}
export function copyCellsByProperty(
model: DatabaseBlockModel,
fromId: Column['id'],
toId: Column['id']
) {
model.doc.transact(() => {
Object.keys(model.cells).forEach(rowId => {
const cell = model.cells[rowId][fromId];
if (cell) {
model.cells[rowId][toId] = {
...cell,
columnId: toId,
};
}
});
});
}
export function deleteColumn(
model: DatabaseBlockModel,
columnId: Column['id']
) {
const index = findPropertyIndex(model, columnId);
if (index < 0) return;
model.doc.transact(() => {
model.columns.splice(index, 1);
});
}
export function deleteRows(model: DatabaseBlockModel, rowIds: string[]) {
model.doc.transact(() => {
for (const rowId of rowIds) {
delete model.cells[rowId];
}
});
}
export function deleteView(model: DatabaseBlockModel, id: string) {
model.doc.captureSync();
model.doc.transact(() => {
model.views = model.views.filter(v => v.id !== id);
});
}
export function duplicateView(model: DatabaseBlockModel, id: string): string {
const newId = model.doc.generateBlockId();
model.doc.transact(() => {
const index = model.views.findIndex(v => v.id === id);
const view = model.views[index];
if (view) {
model.views.splice(
index + 1,
0,
JSON.parse(JSON.stringify({ ...view, id: newId }))
);
}
});
return newId;
}
export function findPropertyIndex(model: DatabaseBlockModel, id: Column['id']) {
return model.columns.findIndex(v => v.id === id);
}
export function getCell(
model: DatabaseBlockModel,
rowId: BlockModel['id'],
columnId: Column['id']
): Cell | null {
if (columnId === 'title') {
return {
columnId: 'title',
value: rowId,
};
}
const yRow = model.cells$.value[rowId];
const yCell = yRow?.[columnId] ?? null;
if (!yCell) return null;
return {
columnId: yCell.columnId,
value: yCell.value,
};
}
export function getProperty(
model: DatabaseBlockModel,
id: Column['id']
): Column | undefined {
return model.columns.find(v => v.id === id);
}
export function moveViewTo(
model: DatabaseBlockModel,
id: string,
position: InsertToPosition
) {
model.doc.transact(() => {
model.views = arrayMove(
model.views,
v => v.id === id,
arr => insertPositionToIndex(position, arr)
);
});
applyViewsUpdate(model);
}
export function updateCell(
model: DatabaseBlockModel,
rowId: string,
cell: Cell
) {
if (
rowId === '__proto__' ||
rowId === 'constructor' ||
rowId === 'prototype'
) {
throw new Error('Invalid rowId');
}
const hasRow = rowId in model.cells;
if (!hasRow) {
model.cells[rowId] = Object.create(null);
}
model.doc.transact(() => {
model.cells[rowId][cell.columnId] = {
columnId: cell.columnId,
value: cell.value,
};
});
}
export function updateCells(
model: DatabaseBlockModel,
columnId: string,
cells: Record<string, unknown>
) {
model.doc.transact(() => {
Object.entries(cells).forEach(([rowId, value]) => {
if (
rowId === '__proto__' ||
rowId === 'constructor' ||
rowId === 'prototype'
) {
throw new Error('Invalid rowId');
}
if (!model.cells[rowId]) {
model.cells[rowId] = Object.create(null);
}
model.cells[rowId][columnId] = {
columnId,
value,
};
});
});
}
export function updateProperty(
model: DatabaseBlockModel,
id: string,
updater: ColumnUpdater
) {
const index = model.columns.findIndex(v => v.id === id);
if (index == null) {
return;
}
model.doc.transact(() => {
const column = model.columns[index];
const result = updater(column);
model.columns[index] = { ...column, ...result };
});
return id;
}
export const updateView = <ViewData extends ViewBasicDataType>(
model: DatabaseBlockModel,
id: string,
update: (data: ViewData) => Partial<ViewData>
) => {
model.doc.transact(() => {
model.views = model.views.map(v => {
if (v.id !== id) {
return v;
}
return { ...v, ...update(v as ViewData) };
});
});
applyViewsUpdate(model);
};
export const DATABASE_CONVERT_WHITE_LIST = ['affine:list', 'affine:paragraph'];

View File

@@ -1,52 +0,0 @@
import { z } from 'zod';
const currentViewListSchema = z.array(
z.object({
blockId: z.string(),
viewId: z.string(),
})
);
const maxLength = 20;
const currentViewListKey = 'blocksuite:databaseBlock:view:currentViewList';
const storage = globalThis.sessionStorage;
const createCurrentViewStorage = () => {
const getList = () => {
const string = storage?.getItem(currentViewListKey);
if (!string) {
return;
}
try {
const result = currentViewListSchema.safeParse(JSON.parse(string));
if (result.success) {
return result.data;
}
} catch {
// do nothing
}
return;
};
const saveList = () => {
storage.setItem(currentViewListKey, JSON.stringify(list));
};
const list = getList() ?? [];
return {
getCurrentView: (blockId: string) => {
return list.find(item => item.blockId === blockId)?.viewId;
},
setCurrentView: (blockId: string, viewId: string) => {
const configIndex = list.findIndex(item => item.blockId === blockId);
if (configIndex >= 0) {
list.splice(configIndex, 1);
}
if (list.length >= maxLength) {
list.pop();
}
list.unshift({ blockId, viewId });
saveList();
},
};
};
export const currentViewStorage = createCurrentViewStorage();

View File

@@ -1,30 +0,0 @@
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import type { DeltaOperation, Text } from '@blocksuite/store';
export const getSingleDocIdFromText = (text?: Text) => {
const deltas = text?.deltas$.value;
if (!deltas) return;
let linkedDocId: string | undefined = undefined;
for (const delta of deltas) {
if (isLinkedDoc(delta)) {
if (linkedDocId) {
return;
}
linkedDocId = delta.attributes?.reference?.pageId as string;
} else if (delta.insert) {
return;
}
}
return linkedDocId;
};
export const isLinkedDoc = (delta: DeltaOperation) => {
const attributes: AffineTextAttributes | undefined = delta.attributes;
return attributes?.reference?.type === 'LinkedPage';
};
export const isPureText = (text?: Text): boolean => {
const deltas = text?.deltas$.value;
if (!deltas) return true;
return deltas.every(v => !isLinkedDoc(v));
};

View File

@@ -1,12 +0,0 @@
import type { ViewMeta } from '@blocksuite/data-view';
import { viewConverts, viewPresets } from '@blocksuite/data-view/view-presets';
export const databaseBlockViews: ViewMeta[] = [
viewPresets.tableViewMeta,
viewPresets.kanbanViewMeta,
];
export const databaseBlockViewMap = Object.fromEntries(
databaseBlockViews.map(view => [view.type, view])
);
export const databaseBlockViewConverts = [...viewConverts];

View File

@@ -1 +0,0 @@
export const commonTools = [];

View File

@@ -1,6 +1,7 @@
import { effects as blockAttachmentEffects } from '@blocksuite/affine-block-attachment/effects';
import { effects as blockBookmarkEffects } from '@blocksuite/affine-block-bookmark/effects';
import { effects as blockCodeEffects } from '@blocksuite/affine-block-code/effects';
import { effects as blockDatabaseEffects } from '@blocksuite/affine-block-database/effects';
import { effects as blockDividerEffects } from '@blocksuite/affine-block-divider/effects';
import { effects as blockEdgelessTextEffects } from '@blocksuite/affine-block-edgeless-text/effects';
import { effects as blockEmbedEffects } from '@blocksuite/affine-block-embed/effects';
@@ -38,29 +39,6 @@ import type { BlockModel } from '@blocksuite/store';
import { registerSpecs } from './_specs/register-specs.js';
import { DataViewBlockComponent } from './data-view-block/index.js';
import { CenterPeek } from './database-block/components/layout.js';
import { DatabaseTitle } from './database-block/components/title/index.js';
import { BlockRenderer } from './database-block/detail-panel/block-renderer.js';
import { NoteRenderer } from './database-block/detail-panel/note-renderer.js';
import { effects as blockDatabaseEffects } from './database-block/effects.js';
import {
DatabaseBlockComponent,
type DatabaseBlockService,
} from './database-block/index.js';
import {
LinkCell,
LinkCellEditing,
} from './database-block/properties/link/cell-renderer.js';
import { LinkNode } from './database-block/properties/link/components/link-node.js';
import {
RichTextCell,
RichTextCellEditing,
} from './database-block/properties/rich-text/cell-renderer.js';
import { IconCell } from './database-block/properties/title/icon.js';
import {
HeaderAreaTextCell,
HeaderAreaTextCellEditing,
} from './database-block/properties/title/text.js';
import { EdgelessAutoCompletePanel } from './root-block/edgeless/components/auto-complete/auto-complete-panel.js';
import { EdgelessAutoComplete } from './root-block/edgeless/components/auto-complete/edgeless-auto-complete.js';
import { EdgelessToolIconButton } from './root-block/edgeless/components/buttons/tool-icon-button.js';
@@ -252,23 +230,6 @@ export function effects() {
widgetDragHandleEffects();
dataViewEffects();
customElements.define('affine-database-title', DatabaseTitle);
customElements.define('data-view-header-area-icon', IconCell);
customElements.define('affine-database-link-cell', LinkCell);
customElements.define('affine-database-link-cell-editing', LinkCellEditing);
customElements.define('data-view-header-area-text', HeaderAreaTextCell);
customElements.define(
'data-view-header-area-text-editing',
HeaderAreaTextCellEditing
);
customElements.define('affine-database-rich-text-cell', RichTextCell);
customElements.define(
'affine-database-rich-text-cell-editing',
RichTextCellEditing
);
customElements.define('center-peek', CenterPeek);
customElements.define('database-datasource-note-renderer', NoteRenderer);
customElements.define('database-datasource-block-renderer', BlockRenderer);
customElements.define('affine-page-root', PageRootBlockComponent);
customElements.define('affine-preview-root', PreviewRootBlockComponent);
customElements.define('mini-mindmap-preview', MiniMindmapPreview);
@@ -291,7 +252,6 @@ export function effects() {
EdgelessRootPreviewBlockComponent
);
customElements.define('affine-custom-modal', AffineCustomModal);
customElements.define('affine-database', DatabaseBlockComponent);
customElements.define('affine-slash-menu', SlashMenu);
customElements.define('inner-slash-menu', InnerSlashMenu);
customElements.define('generating-placeholder', GeneratingPlaceholder);
@@ -357,7 +317,6 @@ export function effects() {
customElements.define('edgeless-note-tool-button', EdgelessNoteToolButton);
customElements.define('edgeless-note-menu', EdgelessNoteMenu);
customElements.define('edgeless-line-width-panel', EdgelessLineWidthPanel);
customElements.define('affine-database-link-node', LinkNode);
customElements.define(
'edgeless-frame-order-button',
EdgelessFrameOrderButton
@@ -470,7 +429,6 @@ declare global {
}
interface BlockServices {
'affine:page': RootService;
'affine:database': DatabaseBlockService;
}
}
}

View File

@@ -17,7 +17,6 @@ export * from './_common/transformers/index.js';
export { type AbstractEditor } from './_common/types.js';
export * from './_specs/index.js';
export * from './data-view-block';
export * from './database-block';
export { EdgelessTemplatePanel } from './root-block/edgeless/components/toolbar/template/template-panel.js';
export type {
Template,
@@ -42,6 +41,7 @@ export {
export * from '@blocksuite/affine-block-attachment';
export * from '@blocksuite/affine-block-bookmark';
export * from '@blocksuite/affine-block-code';
export * from '@blocksuite/affine-block-database';
export * from '@blocksuite/affine-block-divider';
export * from '@blocksuite/affine-block-edgeless-text';
export * from '@blocksuite/affine-block-embed';

View File

@@ -1,12 +1,10 @@
import type { ToolbarMoreMenuConfig } from '@blocksuite/affine-components/toolbar';
import type { DatabaseOptionsConfig } from '../database-block/config.js';
import type { KeyboardToolbarConfig } from './widgets/keyboard-toolbar/config.js';
import type { LinkedWidgetConfig } from './widgets/linked-doc/index.js';
export interface RootBlockConfig {
linkedWidget?: Partial<LinkedWidgetConfig>;
toolbarMoreMenu?: Partial<ToolbarMoreMenuConfig>;
databaseOptions?: Partial<DatabaseOptionsConfig>;
keyboardToolbar?: Partial<KeyboardToolbarConfig>;
}

View File

@@ -1,3 +1,7 @@
import {
convertToDatabase,
DATABASE_CONVERT_WHITE_LIST,
} from '@blocksuite/affine-block-database';
import {
convertSelectedBlocksToLinkedDoc,
getTitleFromSelectedModels,
@@ -43,8 +47,6 @@ import { assertExists } from '@blocksuite/global/utils';
import { Slice } from '@blocksuite/store';
import { html, type TemplateResult } from 'lit';
import { convertToDatabase } from '../../../database-block/data-source.js';
import { DATABASE_CONVERT_WHITE_LIST } from '../../../database-block/utils/block-utils.js';
import { FormatBarContext } from './context.js';
import type { AffineFormatBarWidget } from './format-bar.js';