From ff295f383f87baa288996f8bd35e93218275499d Mon Sep 17 00:00:00 2001 From: zzj3720 <17165520+zzj3720@users.noreply.github.com> Date: Tue, 14 Jan 2025 14:35:46 +0000 Subject: [PATCH] refactor(editor): enable the noUncheckedIndexedAccess rule for the block-database package (#9691) close: BS-2269 --- .../block-database/src/adapters/html.ts | 130 ++++++----------- .../block-database/src/adapters/markdown.ts | 134 +++++------------- .../src/adapters/notion-html.ts | 80 ++++++----- .../block-database/src/adapters/plain-text.ts | 66 ++------- .../block-database/src/adapters/utils.ts | 84 ++++++++++- .../affine/block-database/src/commands.ts | 10 +- .../affine/block-database/src/data-source.ts | 46 ++++-- .../block-database/src/properties/converts.ts | 22 +-- .../src/properties/link/cell-renderer.ts | 4 +- .../src/properties/link/define.ts | 2 +- .../block-database/src/properties/model.ts | 20 +++ .../src/properties/rich-text/cell-renderer.ts | 4 +- .../src/properties/rich-text/define.ts | 2 +- .../src/properties/title/cell-renderer.ts | 4 +- .../src/properties/title/define.ts | 2 +- .../block-database/src/utils/block-utils.ts | 30 ++-- .../affine/block-database/tsconfig.json | 3 +- .../src/core/property/property-config.ts | 2 +- .../data-view/src/core/property/types.ts | 7 +- .../src/core/view-manager/single-view.ts | 1 - .../src/property-presets/date/define.ts | 9 +- .../src/__tests__/adapters/html.unit.spec.ts | 2 +- .../__tests__/adapters/markdown.unit.spec.ts | 2 +- .../adapters/plain-text.unit.spec.ts | 2 +- 24 files changed, 324 insertions(+), 344 deletions(-) create mode 100644 blocksuite/affine/block-database/src/properties/model.ts diff --git a/blocksuite/affine/block-database/src/adapters/html.ts b/blocksuite/affine/block-database/src/adapters/html.ts index c29dd7e4f..012a28fa6 100644 --- a/blocksuite/affine/block-database/src/adapters/html.ts +++ b/blocksuite/affine/block-database/src/adapters/html.ts @@ -10,11 +10,11 @@ import { 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 { nanoid } from '@blocksuite/store'; import type { Element } from 'hast'; +import { processTable } from './utils'; + const DATABASE_NODE_TYPES = new Set(['table', 'thead', 'tbody', 'th', 'tr']); export const databaseBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = { @@ -53,8 +53,12 @@ export const databaseBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = { const rowId = nanoid(); cells[rowId] = Object.create(null); (row as Element).children.forEach((cell, index) => { - cells[rowId][viewsColumns[index].id] = { - columnId: viewsColumns[index].id, + const column = viewsColumns[index]; + if (!column) { + return; + } + cells[rowId][column.id] = { + columnId: column.id, value: TextUtils.createText( (cell as Element).children .map(child => ('value' in child ? child.value : '')) @@ -65,14 +69,18 @@ export const databaseBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = { }); // Build database columns from table header row - const columns = tableHeaderRow.children.map((_child, index) => { + const columns = tableHeaderRow.children.flatMap((_child, index) => { + const column = viewsColumns[index]; + if (!column) { + return []; + } 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, + id: column.id, }; }); @@ -117,6 +125,10 @@ export const databaseBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = { // The first child of each table body row is the database title cell if (o.node.tagName === 'tr') { const { deltaConverter } = context; + const firstChild = o.node.children[0]; + if (!firstChild) { + return; + } walkerContext .openNode({ type: 'block', @@ -130,7 +142,7 @@ export const databaseBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = { props: { text: { '$blocksuite:internal:text$': true, - delta: deltaConverter.astToDelta(o.node.children[0]), + delta: deltaConverter.astToDelta(firstChild), }, type: 'text', }, @@ -156,7 +168,7 @@ export const databaseBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = { const columns = o.node.props.columns as Array; const children = o.node.children; const cells = o.node.props.cells as SerializedCells; - + const table = processTable(columns, children, cells); const createAstTableCell = ( children: InlineHtmlAST[] ): InlineHtmlAST => ({ @@ -183,94 +195,40 @@ export const databaseBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = { }); 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) => 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) => 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], + children: [ + createAstTableRow( + table.headers.map(v => + createAstTableHeaderCell([ + { + type: 'text', + value: v.name ?? '', + }, + ]) + ) + ), + ], }; const tableBodyAst: Element = { type: 'element', tagName: 'tbody', properties: Object.create(null), - children: [...htmlAstRows], + children: table.rows.map(v => { + return createAstTableRow( + v.cells.map(cell => { + return createAstTableCell( + typeof cell.value === 'string' + ? [{ type: 'text', value: cell.value }] + : deltaConverter.deltaToAST(cell.value.delta) + ); + }) + ); + }), }; walkerContext diff --git a/blocksuite/affine/block-database/src/adapters/markdown.ts b/blocksuite/affine/block-database/src/adapters/markdown.ts index c013d5cd4..f94c80a84 100644 --- a/blocksuite/affine/block-database/src/adapters/markdown.ts +++ b/blocksuite/affine/block-database/src/adapters/markdown.ts @@ -9,11 +9,11 @@ import { 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 { nanoid } from '@blocksuite/store'; import type { TableRow } from 'mdast'; +import { processTable } from './utils'; + const DATABASE_NODE_TYPES = new Set(['table', 'tableRow']); const isDatabaseNode = (node: MarkdownAST) => @@ -28,7 +28,7 @@ export const databaseBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher = enter: (o, context) => { const { walkerContext } = context; if (o.node.type === 'table') { - const viewsColumns = o.node.children[0].children.map(() => { + const viewsColumns = o.node.children[0]?.children.map(() => { return { id: nanoid(), hide: false, @@ -40,8 +40,12 @@ export const databaseBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher = 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, + const column = viewsColumns?.[index + 1]; + if (!column) { + return; + } + cells[rowId][column.id] = { + columnId: column.id, value: TextUtils.createText( cell.children .map(child => ('value' in child ? child.value : '')) @@ -50,14 +54,14 @@ export const databaseBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher = }; }); }); - const columns = o.node.children[0].children.map((_child, index) => { + 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, + id: viewsColumns?.[index]?.id, }; }); walkerContext.openNode( @@ -78,7 +82,7 @@ export const databaseBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher = conditions: [], }, header: { - titleColumn: viewsColumns[0]?.id, + titleColumn: viewsColumns?.[0]?.id, iconColumn: 'type', }, }, @@ -103,6 +107,10 @@ export const databaseBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher = if (o.node.type === 'tableRow') { const { deltaConverter } = context; + const firstChild = o.node.children[0]; + if (!firstChild) { + return; + } walkerContext .openNode({ type: 'block', @@ -116,7 +124,7 @@ export const databaseBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher = props: { text: { '$blocksuite:internal:text$': true, - delta: deltaConverter.astToDelta(o.node.children[0]), + delta: deltaConverter.astToDelta(firstChild), }, type: 'text', }, @@ -140,101 +148,25 @@ export const databaseBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher = const columns = o.node.props.columns as Array; const children = o.node.children; const cells = o.node.props.cells as SerializedCells; - const createAstCell = (children: MarkdownAST[]) => ({ - type: 'tableCell', - children, + const table = processTable(columns, children, cells); + rows.push({ + type: 'tableRow', + children: table.headers.map(v => ({ + type: 'tableCell', + children: [{ type: 'text', value: v.name }], + })), }); - 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) => 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) => 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)) { + table.rows.forEach(v => { rows.push({ type: 'tableRow', - children: Array.prototype.map.call(columns, v => - createAstCell([ - { - type: 'text', - value: v.name, - }, - ]) - ) as [], + children: v.cells.map(v => ({ + type: 'tableCell', + children: + typeof v.value === 'string' + ? [{ type: 'text', value: v.value }] + : deltaConverter.deltaToAST(v.value.delta), + })), }); - } - - // Handle 2-... rows - Array.prototype.forEach.call(mdAstCells, children => { - rows.push({ type: 'tableRow', children }); }); walkerContext diff --git a/blocksuite/affine/block-database/src/adapters/notion-html.ts b/blocksuite/affine/block-database/src/adapters/notion-html.ts index 74c365cef..5d3869bd3 100644 --- a/blocksuite/affine/block-database/src/adapters/notion-html.ts +++ b/blocksuite/affine/block-database/src/adapters/notion-html.ts @@ -61,7 +61,7 @@ export const databaseBlockNotionHtmlAdapterMatcher: BlockNotionHtmlAdapterMatche const columnTypeClass = HastUtils.querySelector(o.node, 'svg') ?.properties?.className; const columnType = Array.isArray(columnTypeClass) - ? (ColumnClassMap[columnTypeClass[0]] ?? 'rich-text') + ? (ColumnClassMap[columnTypeClass[0] ?? ''] ?? 'rich-text') : 'rich-text'; walkerContext.pushGlobalContextStack( 'hast:table:column', @@ -132,8 +132,12 @@ export const databaseBlockNotionHtmlAdapterMatcher: BlockNotionHtmlAdapterMatche children: [], } ); - row[columns[index].id] = { - columnId: columns[index].id, + const column = columns[index]; + if (!column) { + return; + } + row[column.id] = { + columnId: column.id, value: HastUtils.getTextContent(child), }; } else if (HastUtils.querySelector(child, '.cell-title')) { @@ -157,33 +161,35 @@ export const databaseBlockNotionHtmlAdapterMatcher: BlockNotionHtmlAdapterMatche return; } const optionIds: string[] = []; + const column = columns[index]; + if (!column) { + return; + } if (HastUtils.querySelector(child, '.selected-value')) { - if (!('options' in columns[index].data)) { - columns[index].data.options = []; + if (!('options' in column.data)) { + column.data.options = []; + } + if (!['multi-select', 'select'].includes(column.type)) { + column.type = 'select'; } if ( - !['multi-select', 'select'].includes(columns[index].type) - ) { - columns[index].type = 'select'; - } - if ( - columns[index].type === 'select' && + column.type === 'select' && child.type === 'element' && child.children.length > 1 ) { - columns[index].type = 'multi-select'; + column.type = 'multi-select'; } child.type === 'element' && child.children.forEach(span => { - const filteredArray = columns[index].data.options?.filter( + const filteredArray = column.data.options?.filter( option => option.value === HastUtils.getTextContent(span) ); const id = filteredArray?.length - ? filteredArray[0].id + ? (filteredArray[0]?.id ?? nanoid()) : nanoid(); if (!filteredArray?.length) { - columns[index].data.options?.push({ + column.data.options?.push({ id, value: HastUtils.getTextContent(span), color: getTagColor(), @@ -192,48 +198,48 @@ export const databaseBlockNotionHtmlAdapterMatcher: BlockNotionHtmlAdapterMatche optionIds.push(id); }); // Expand will be done when leaving the table - row[columns[index].id] = { - columnId: columns[index].id, + row[column.id] = { + columnId: column.id, value: optionIds, }; } else if (HastUtils.querySelector(child, '.checkbox')) { - if (columns[index].type !== 'checkbox') { - columns[index].type = 'checkbox'; + if (column.type !== 'checkbox') { + column.type = 'checkbox'; } - row[columns[index].id] = { - columnId: columns[index].id, + row[column.id] = { + columnId: column.id, value: HastUtils.querySelector(child, '.checkbox-on') ? true : false, }; - } else if (columns[index].type === 'number') { + } else if (column.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, + column.type = 'rich-text'; + row[column.id] = { + columnId: column.id, value: TextUtils.createText(text), }; } else { - row[columns[index].id] = { - columnId: columns[index].id, + row[column.id] = { + columnId: column.id, value: number, }; } } else { - row[columns[index].id] = { - columnId: columns[index].id, + row[column.id] = { + columnId: column.id, value: HastUtils.getTextContent(child), }; } if ( - columns[index].type === 'rich-text' && - !TextUtils.isText(row[columns[index].id].value) + column.type === 'rich-text' && + !TextUtils.isText(row[column.id].value) ) { - row[columns[index].id] = { - columnId: columns[index].id, - value: TextUtils.createText(row[columns[index].id].value), + row[column.id] = { + columnId: column.id, + value: TextUtils.createText(row[column.id].value), }; } }); @@ -264,11 +270,15 @@ export const databaseBlockNotionHtmlAdapterMatcher: BlockNotionHtmlAdapterMatche .getGlobalContextStack('hast:table:rows') .forEach((row, i) => { Object.keys(row).forEach(columnId => { + const cell = row[columnId]; + if (!cell) { + return; + } if ( columns.find(column => column.id === columnId)?.type === 'select' ) { - row[columnId].value = (row[columnId].value as string[])[0]; + cell.value = (cell.value as string[])[0]; } }); cells[children.at(i)?.id ?? nanoid()] = row; diff --git a/blocksuite/affine/block-database/src/adapters/plain-text.ts b/blocksuite/affine/block-database/src/adapters/plain-text.ts index 2399aacd7..bfcaed2b4 100644 --- a/blocksuite/affine/block-database/src/adapters/plain-text.ts +++ b/blocksuite/affine/block-database/src/adapters/plain-text.ts @@ -7,11 +7,8 @@ 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'; +import { formatTable, processTable } from './utils.js'; export const databaseBlockPlainTextAdapterMatcher: BlockPlainTextAdapterMatcher = { @@ -26,63 +23,22 @@ export const databaseBlockPlainTextAdapterMatcher: BlockPlainTextAdapterMatcher const columns = o.node.props.columns as Array; 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> } - ).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> } - ).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); + const table = processTable(columns, children, cells); + rows.push(table.headers.map(v => v.name)); + table.rows.forEach(v => { + rows.push( + v.cells.map(v => + typeof v.value === 'string' + ? v.value + : deltaConverter.deltaToAST(v.value.delta).join('') + ) + ); }); - // Convert rows to table string const tableString = formatTable(rows); context.textBuffer.content += tableString; context.textBuffer.content += '\n'; - walkerContext.skipAllChildren(); }, }, diff --git a/blocksuite/affine/block-database/src/adapters/utils.ts b/blocksuite/affine/block-database/src/adapters/utils.ts index 11fd24bb5..9b605d50f 100644 --- a/blocksuite/affine/block-database/src/adapters/utils.ts +++ b/blocksuite/affine/block-database/src/adapters/utils.ts @@ -1,6 +1,14 @@ +import type { Column, SerializedCells } from '@blocksuite/affine-model'; +import type { DeltaInsert } from '@blocksuite/inline'; +import type { BlockSnapshot } from '@blocksuite/store'; + +import { databaseBlockModels } from '../properties/model'; + function calculateColumnWidths(rows: string[][]): number[] { - return rows[0].map((_, colIndex) => - Math.max(...rows.map(row => (row[colIndex] || '').length)) + return ( + rows[0]?.map((_, colIndex) => + Math.max(...rows.map(row => (row[colIndex] || '').length)) + ) ?? [] ); } @@ -10,7 +18,7 @@ function formatRow( isHeader: boolean ): string { const cells = row.map((cell, colIndex) => - cell.padEnd(columnWidths[colIndex], ' ') + cell?.padEnd(columnWidths[colIndex] ?? 0, ' ') ); const rowString = `| ${cells.join(' | ')} |`; return isHeader @@ -30,3 +38,73 @@ export function formatTable(rows: string[][]): string { ); return formattedRows.join('\n'); } +export const isDelta = (value: unknown): value is { delta: DeltaInsert[] } => { + if (typeof value === 'object' && value !== null) { + return '$blocksuite:internal:text$' in value; + } + return false; +}; +type Table = { + headers: Column[]; + rows: Row[]; +}; +type Row = { + cells: Cell[]; +}; +type Cell = { + value: string | { delta: DeltaInsert[] }; +}; +export const processTable = ( + columns: Column[], + children: BlockSnapshot[], + cells: SerializedCells +): Table => { + const table: Table = { + headers: columns, + rows: [], + }; + children.forEach(v => { + const row: Row = { + cells: [], + }; + const title = v.props.text; + if (isDelta(title)) { + row.cells.push({ + value: title, + }); + } else { + row.cells.push({ + value: '', + }); + } + + columns.forEach(col => { + const property = databaseBlockModels[col.type]; + const cell = cells[v.id]?.[col.id]; + if (col.type === 'title') { + return; + } + if (!cell || !property) { + row.cells.push({ + value: '', + }); + return; + } + let value: string | { delta: DeltaInsert[] }; + if (isDelta(cell.value)) { + value = cell.value; + } else { + value = property.config.cellToString({ + value: cell.value, + data: col.data, + }); + } + row.cells.push({ + value, + }); + }); + table.rows.push(row); + }); + + return table; +}; diff --git a/blocksuite/affine/block-database/src/commands.ts b/blocksuite/affine/block-database/src/commands.ts index d051b3ab6..8369b5dd6 100644 --- a/blocksuite/affine/block-database/src/commands.ts +++ b/blocksuite/affine/block-database/src/commands.ts @@ -18,22 +18,24 @@ export const insertDatabaseBlockCommand: Command< : selectedModels[selectedModels.length - 1]; const service = std.getService('affine:database'); - if (!service) return; + if (!service || !targetModel) return; const result = std.store.addSiblingBlocks( targetModel, [{ flavour: 'affine:database' }], place ); - if (result.length === 0) return; + const string = result[0]; - service.initDatabaseBlock(std.store, targetModel, result[0], viewType, false); + if (string == null) return; + + service.initDatabaseBlock(std.store, targetModel, string, viewType, false); if (removeEmptyLine && targetModel.text?.length === 0) { std.store.deleteBlock(targetModel); } - next({ insertedDatabaseBlockId: result[0] }); + next({ insertedDatabaseBlockId: string }); }; export const commands: BlockCommands = { diff --git a/blocksuite/affine/block-database/src/data-source.ts b/blocksuite/affine/block-database/src/data-source.ts index 405b81a8f..eeadc45fe 100644 --- a/blocksuite/affine/block-database/src/data-source.ts +++ b/blocksuite/affine/block-database/src/data-source.ts @@ -18,6 +18,7 @@ import { } from '@blocksuite/data-view'; import { propertyPresets } from '@blocksuite/data-view/property-presets'; import { IS_MOBILE } from '@blocksuite/global/env'; +import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions'; import { assertExists } from '@blocksuite/global/utils'; import { type BlockModel, nanoid, Text } from '@blocksuite/store'; import { computed, type ReadonlySignal } from '@preact/signals-core'; @@ -28,7 +29,7 @@ import { databaseBlockPropertyList, databasePropertyConverts, } from './properties/index.js'; -import { titlePurePropertyConfig } from './properties/title/define.js'; +import { titlePropertyModelConfig } from './properties/title/define.js'; import { addProperty, applyCellsUpdate, @@ -180,12 +181,13 @@ export class DatabaseBlockDataSource extends DataSourceBase { propertyAdd(insertToPosition: InsertToPosition, type?: string): string { this.doc.captureSync(); + const property = this.propertyMetaGet( + type ?? propertyPresets.multiSelectPropertyConfig.type + ); const result = addProperty( this._model, insertToPosition, - databaseBlockAllPropertyMap[ - type ?? propertyPresets.multiSelectPropertyConfig.type - ].create(this.newPropertyName()) + property.create(this.newPropertyName()) ); applyPropertyUpdate(this._model); return result; @@ -251,7 +253,14 @@ export class DatabaseBlockDataSource extends DataSourceBase { } propertyMetaGet(type: string): PropertyMetaConfig { - return databaseBlockAllPropertyMap[type]; + const property = databaseBlockAllPropertyMap[type]; + if (!property) { + throw new BlockSuiteError( + ErrorCode.DatabaseBlockError, + `Unknown property type: ${type}` + ); + } + return property; } propertyNameGet(propertyId: string): string { @@ -298,7 +307,7 @@ export class DatabaseBlockDataSource extends DataSourceBase { currentCells as any ) ?? { - property: databaseBlockAllPropertyMap[toType].config.defaultData(), + property: this.propertyMetaGet(toType).config.defaultData(), cells: currentCells.map(() => undefined), }; this.doc.captureSync(); @@ -309,7 +318,10 @@ export class DatabaseBlockDataSource extends DataSourceBase { const cells: Record = {}; currentCells.forEach((value, i) => { if (value != null || result.cells[i] != null) { - cells[rows[i]] = result.cells[i]; + const rowId = rows[i]; + if (rowId) { + cells[rowId] = result.cells[i]; + } } }); updateCells(this._model, propertyId, cells); @@ -381,7 +393,14 @@ export class DatabaseBlockDataSource extends DataSourceBase { } viewMetaGet(type: string): ViewMeta { - return databaseBlockViewMap[type]; + const view = databaseBlockViewMap[type]; + if (!view) { + throw new BlockSuiteError( + ErrorCode.DatabaseBlockError, + `Unknown view type: ${type}` + ); + } + return view; } viewMetaGetById(viewId: string): ViewMeta { @@ -404,7 +423,7 @@ export const databaseViewInitEmpty = ( addProperty( model, 'start', - titlePurePropertyConfig.create(titlePurePropertyConfig.config.name) + titlePropertyModelConfig.create(titlePropertyModelConfig.config.name) ); databaseViewAddView(model, viewType); }; @@ -423,7 +442,7 @@ export const databaseViewInitTemplate = ( model: DatabaseBlockModel, viewType: string ) => { - const ids = [nanoid(), nanoid(), nanoid()]; + const ids = [nanoid(), nanoid(), nanoid()] as const; const statusId = addProperty( model, 'end', @@ -470,11 +489,12 @@ export const convertToDatabase = (host: EditorHost, viewType: string) => { }) .run(); const { selectedModels } = ctx; - if (!selectedModels || selectedModels.length === 0) return; + const firstModel = selectedModels?.[0]; + if (!firstModel) return; host.doc.captureSync(); - const parentModel = host.doc.getParent(selectedModels[0]); + const parentModel = host.doc.getParent(firstModel); if (!parentModel) { return; } @@ -483,7 +503,7 @@ export const convertToDatabase = (host: EditorHost, viewType: string) => { 'affine:database', {}, parentModel, - parentModel.children.indexOf(selectedModels[0]) + parentModel.children.indexOf(firstModel) ); const databaseModel = host.doc.getBlock(id)?.model as | DatabaseBlockModel diff --git a/blocksuite/affine/block-database/src/properties/converts.ts b/blocksuite/affine/block-database/src/properties/converts.ts index 149001f69..28d189a42 100644 --- a/blocksuite/affine/block-database/src/properties/converts.ts +++ b/blocksuite/affine/block-database/src/properties/converts.ts @@ -8,12 +8,12 @@ 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'; +import { richTextPropertyModelConfig } from './rich-text/define.js'; export const databasePropertyConverts = [ ...presetPropertyConverts, createPropertyConvert( - richTextColumnModelConfig, + richTextPropertyModelConfig, propertyModelPresets.selectPropertyModelConfig, (_property, cells) => { const options: Record = {}; @@ -43,7 +43,7 @@ export const databasePropertyConverts = [ } ), createPropertyConvert( - richTextColumnModelConfig, + richTextPropertyModelConfig, propertyModelPresets.multiSelectPropertyModelConfig, (_property, cells) => { const options: Record = {}; @@ -77,7 +77,7 @@ export const databasePropertyConverts = [ } ), createPropertyConvert( - richTextColumnModelConfig, + richTextPropertyModelConfig, propertyModelPresets.numberPropertyModelConfig, (_property, cells) => { return { @@ -93,7 +93,7 @@ export const databasePropertyConverts = [ } ), createPropertyConvert( - richTextColumnModelConfig, + richTextPropertyModelConfig, propertyModelPresets.progressPropertyModelConfig, (_property, cells) => { return { @@ -106,7 +106,7 @@ export const databasePropertyConverts = [ } ), createPropertyConvert( - richTextColumnModelConfig, + richTextPropertyModelConfig, propertyModelPresets.checkboxPropertyModelConfig, (_property, cells) => { const truthyValues = new Set(['yes', 'true']); @@ -120,7 +120,7 @@ export const databasePropertyConverts = [ ), createPropertyConvert( propertyModelPresets.checkboxPropertyModelConfig, - richTextColumnModelConfig, + richTextPropertyModelConfig, (_property, cells) => { return { property: {}, @@ -130,7 +130,7 @@ export const databasePropertyConverts = [ ), createPropertyConvert( propertyModelPresets.multiSelectPropertyModelConfig, - richTextColumnModelConfig, + richTextPropertyModelConfig, (property, cells) => { const optionMap = Object.fromEntries( property.options.map(v => [v.id, v]) @@ -146,7 +146,7 @@ export const databasePropertyConverts = [ ), createPropertyConvert( propertyModelPresets.numberPropertyModelConfig, - richTextColumnModelConfig, + richTextPropertyModelConfig, (_property, cells) => ({ property: {}, cells: cells.map(v => new Text(v?.toString()).yText), @@ -154,7 +154,7 @@ export const databasePropertyConverts = [ ), createPropertyConvert( propertyModelPresets.progressPropertyModelConfig, - richTextColumnModelConfig, + richTextPropertyModelConfig, (_property, cells) => ({ property: {}, cells: cells.map(v => new Text(v?.toString()).yText), @@ -162,7 +162,7 @@ export const databasePropertyConverts = [ ), createPropertyConvert( propertyModelPresets.selectPropertyModelConfig, - richTextColumnModelConfig, + richTextPropertyModelConfig, (property, cells) => { const optionMap = Object.fromEntries( property.options.map(v => [v.id, v]) diff --git a/blocksuite/affine/block-database/src/properties/link/cell-renderer.ts b/blocksuite/affine/block-database/src/properties/link/cell-renderer.ts index d43e22a27..df158dc3e 100644 --- a/blocksuite/affine/block-database/src/properties/link/cell-renderer.ts +++ b/blocksuite/affine/block-database/src/properties/link/cell-renderer.ts @@ -18,7 +18,7 @@ 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'; +import { linkPropertyModelConfig } from './define.js'; export class LinkCell extends BaseCellRenderer { static override styles = css` @@ -247,7 +247,7 @@ export class LinkCellEditing extends BaseCellRenderer { private accessor _container!: HTMLInputElement; } -export const linkColumnConfig = linkColumnModelConfig.createPropertyMeta({ +export const linkColumnConfig = linkPropertyModelConfig.createPropertyMeta({ icon: createIcon('LinkIcon'), cellRenderer: { view: createFromBaseCellRenderer(LinkCell), diff --git a/blocksuite/affine/block-database/src/properties/link/define.ts b/blocksuite/affine/block-database/src/properties/link/define.ts index 2c865f4a3..13da897ef 100644 --- a/blocksuite/affine/block-database/src/properties/link/define.ts +++ b/blocksuite/affine/block-database/src/properties/link/define.ts @@ -1,7 +1,7 @@ import { propertyType, t } from '@blocksuite/data-view'; export const linkColumnType = propertyType('link'); -export const linkColumnModelConfig = linkColumnType.modelConfig({ +export const linkPropertyModelConfig = linkColumnType.modelConfig({ name: 'Link', type: () => t.string.instance(), defaultData: () => ({}), diff --git a/blocksuite/affine/block-database/src/properties/model.ts b/blocksuite/affine/block-database/src/properties/model.ts new file mode 100644 index 000000000..1c292acc1 --- /dev/null +++ b/blocksuite/affine/block-database/src/properties/model.ts @@ -0,0 +1,20 @@ +import type { PropertyModel } from '@blocksuite/data-view'; +import { propertyModelPresets } from '@blocksuite/data-view/property-pure-presets'; + +import { linkPropertyModelConfig } from './link/define'; +import { richTextPropertyModelConfig } from './rich-text/define'; +import { titlePropertyModelConfig } from './title/define'; + +export const databaseBlockModels = Object.fromEntries( + [ + propertyModelPresets.checkboxPropertyModelConfig, + propertyModelPresets.datePropertyModelConfig, + propertyModelPresets.numberPropertyModelConfig, + propertyModelPresets.progressPropertyModelConfig, + propertyModelPresets.selectPropertyModelConfig, + propertyModelPresets.multiSelectPropertyModelConfig, + linkPropertyModelConfig, + richTextPropertyModelConfig, + titlePropertyModelConfig, + ].map(v => [v.type, v as PropertyModel]) +); diff --git a/blocksuite/affine/block-database/src/properties/rich-text/cell-renderer.ts b/blocksuite/affine/block-database/src/properties/rich-text/cell-renderer.ts index 2f4ea3fa0..619cfa631 100644 --- a/blocksuite/affine/block-database/src/properties/rich-text/cell-renderer.ts +++ b/blocksuite/affine/block-database/src/properties/rich-text/cell-renderer.ts @@ -31,7 +31,7 @@ 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'; +import { richTextPropertyModelConfig } from './define.js'; function toggleStyle( inlineEditor: AffineInlineEditor | null, @@ -581,7 +581,7 @@ declare global { } export const richTextColumnConfig = - richTextColumnModelConfig.createPropertyMeta({ + richTextPropertyModelConfig.createPropertyMeta({ icon: createIcon('TextIcon'), cellRenderer: { diff --git a/blocksuite/affine/block-database/src/properties/rich-text/define.ts b/blocksuite/affine/block-database/src/properties/rich-text/define.ts index 4302bc635..e1a736cc6 100644 --- a/blocksuite/affine/block-database/src/properties/rich-text/define.ts +++ b/blocksuite/affine/block-database/src/properties/rich-text/define.ts @@ -9,7 +9,7 @@ import { type RichTextCellType, toYText } from '../utils.js'; export const richTextColumnType = propertyType('rich-text'); -export const richTextColumnModelConfig = +export const richTextPropertyModelConfig = richTextColumnType.modelConfig({ name: 'Text', type: () => t.richText.instance(), diff --git a/blocksuite/affine/block-database/src/properties/title/cell-renderer.ts b/blocksuite/affine/block-database/src/properties/title/cell-renderer.ts index 5bf879899..645d88d7f 100644 --- a/blocksuite/affine/block-database/src/properties/title/cell-renderer.ts +++ b/blocksuite/affine/block-database/src/properties/title/cell-renderer.ts @@ -6,10 +6,10 @@ import { } from '@blocksuite/data-view'; import { TableSingleView } from '@blocksuite/data-view/view-presets'; -import { titlePurePropertyConfig } from './define.js'; +import { titlePropertyModelConfig } from './define.js'; import { HeaderAreaTextCell, HeaderAreaTextCellEditing } from './text.js'; -export const titleColumnConfig = titlePurePropertyConfig.createPropertyMeta({ +export const titleColumnConfig = titlePropertyModelConfig.createPropertyMeta({ icon: createIcon('TitleIcon'), cellRenderer: { view: uniMap( diff --git a/blocksuite/affine/block-database/src/properties/title/define.ts b/blocksuite/affine/block-database/src/properties/title/define.ts index 264765aa6..ba8ffca14 100644 --- a/blocksuite/affine/block-database/src/properties/title/define.ts +++ b/blocksuite/affine/block-database/src/properties/title/define.ts @@ -6,7 +6,7 @@ import { isLinkedDoc } from '../../utils/title-doc.js'; export const titleColumnType = propertyType('title'); -export const titlePurePropertyConfig = titleColumnType.modelConfig({ +export const titlePropertyModelConfig = titleColumnType.modelConfig({ name: 'Title', type: () => t.richText.instance(), defaultData: () => ({}), diff --git a/blocksuite/affine/block-database/src/utils/block-utils.ts b/blocksuite/affine/block-database/src/utils/block-utils.ts index 7b5f24eaa..0a02b3488 100644 --- a/blocksuite/affine/block-database/src/utils/block-utils.ts +++ b/blocksuite/affine/block-database/src/utils/block-utils.ts @@ -62,8 +62,8 @@ export function copyCellsByProperty( ) { model.doc.transact(() => { Object.keys(model.cells).forEach(rowId => { - const cell = model.cells[rowId][fromId]; - if (cell) { + const cell = model.cells[rowId]?.[fromId]; + if (cell && model.cells[rowId]) { model.cells[rowId][toId] = { ...cell, columnId: toId, @@ -186,14 +186,15 @@ export function updateCell( console.error('Invalid columnId'); return; } - const hasRow = rowId in model.cells; - if (!hasRow) { + if (!model.cells[rowId]) { model.cells[rowId] = Object.create(null); } - model.cells[rowId][columnId] = { - columnId: columnId, - value: cell.value, - }; + if (model.cells[rowId]) { + model.cells[rowId][columnId] = { + columnId: columnId, + value: cell.value, + }; + } }); } @@ -214,10 +215,12 @@ export function updateCells( if (!model.cells[rowId]) { model.cells[rowId] = Object.create(null); } - model.cells[rowId][columnId] = { - columnId, - value, - }; + if (model.cells[rowId]) { + model.cells[rowId][columnId] = { + columnId, + value, + }; + } }); }); } @@ -233,6 +236,9 @@ export function updateProperty( } model.doc.transact(() => { const column = model.columns[index]; + if (!column) { + return; + } const result = updater(column); model.columns[index] = { ...column, ...result }; }); diff --git a/blocksuite/affine/block-database/tsconfig.json b/blocksuite/affine/block-database/tsconfig.json index fd8c582c3..ffea569f5 100644 --- a/blocksuite/affine/block-database/tsconfig.json +++ b/blocksuite/affine/block-database/tsconfig.json @@ -3,7 +3,8 @@ "compilerOptions": { "rootDir": "./src", "outDir": "./dist", - "tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo" + "tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo", + "noUncheckedIndexedAccess": true }, "include": ["./src"], "references": [ diff --git a/blocksuite/affine/data-view/src/core/property/property-config.ts b/blocksuite/affine/data-view/src/core/property/property-config.ts index 540cfaa7f..b6545a501 100644 --- a/blocksuite/affine/data-view/src/core/property/property-config.ts +++ b/blocksuite/affine/data-view/src/core/property/property-config.ts @@ -31,7 +31,7 @@ type Create< }; export type PropertyModel< Type extends string = string, - PropertyData extends Record = Record, + PropertyData extends Record = Record, CellData = unknown, > = { type: Type; diff --git a/blocksuite/affine/data-view/src/core/property/types.ts b/blocksuite/affine/data-view/src/core/property/types.ts index 8793187db..b1a995dfe 100644 --- a/blocksuite/affine/data-view/src/core/property/types.ts +++ b/blocksuite/affine/data-view/src/core/property/types.ts @@ -38,12 +38,7 @@ export type PropertyConfig< value?: Value; }> ) => unknown[]; - cellToString: ( - config: WithCommonPropertyConfig<{ - value: Value; - data: Data; - }> - ) => string; + cellToString: (config: { value: Value; data: Data }) => string; cellFromString: ( config: WithCommonPropertyConfig<{ value: string; diff --git a/blocksuite/affine/data-view/src/core/view-manager/single-view.ts b/blocksuite/affine/data-view/src/core/view-manager/single-view.ts index de69c8b1d..0cf230bc7 100644 --- a/blocksuite/affine/data-view/src/core/view-manager/single-view.ts +++ b/blocksuite/affine/data-view/src/core/view-manager/single-view.ts @@ -286,7 +286,6 @@ export abstract class SingleViewBase< this.dataSource.propertyMetaGet(type).config.cellToString({ value: this.dataSource.cellValueGet(rowId, propertyId), data: this.propertyDataGet(propertyId), - dataSource: this.dataSource, }) ?? '' ); } diff --git a/blocksuite/affine/data-view/src/property-presets/date/define.ts b/blocksuite/affine/data-view/src/property-presets/date/define.ts index 27df9e771..770951755 100644 --- a/blocksuite/affine/data-view/src/property-presets/date/define.ts +++ b/blocksuite/affine/data-view/src/property-presets/date/define.ts @@ -1,3 +1,6 @@ +import { format } from 'date-fns/format'; +import { parse } from 'date-fns/parse'; + import { t } from '../../core/logical/type-presets.js'; import { propertyType } from '../../core/property/property-config.js'; @@ -6,12 +9,12 @@ export const datePropertyModelConfig = datePropertyType.modelConfig({ name: 'Date', type: () => t.date.instance(), defaultData: () => ({}), - cellToString: ({ value }) => value?.toString() ?? '', + cellToString: ({ value }) => format(value, 'yyyy-MM-dd'), cellFromString: ({ value }) => { - const isDateFormat = !isNaN(Date.parse(value)); + const date = parse(value, 'yyyy-MM-dd', new Date()); return { - value: isDateFormat ? +new Date(value) : null, + value: +date, }; }, cellToJson: ({ value }) => value ?? null, diff --git a/blocksuite/blocks/src/__tests__/adapters/html.unit.spec.ts b/blocksuite/blocks/src/__tests__/adapters/html.unit.spec.ts index d3c505995..93fe637de 100644 --- a/blocksuite/blocks/src/__tests__/adapters/html.unit.spec.ts +++ b/blocksuite/blocks/src/__tests__/adapters/html.unit.spec.ts @@ -1441,7 +1441,7 @@ describe('snapshot to html', () => { ], }; const html = template( - '
TitleStatusDateNumberProgressMultiSelectRichTextLinkCheckbox
Task 1TODO2023-12-15165test1,test2test2https://google.comtrue
Task 2In Progress2023-12-20test1
' + '
TitleStatusDateNumberProgressMultiSelectRichTextLinkCheckbox
Task 1TODO2023-12-15165test1,test2test2https://google.comTrue
Task 2In Progress2023-12-20test1
' ); const htmlAdapter = new HtmlAdapter(createJob(), provider); const target = await htmlAdapter.fromBlockSnapshot({ diff --git a/blocksuite/blocks/src/__tests__/adapters/markdown.unit.spec.ts b/blocksuite/blocks/src/__tests__/adapters/markdown.unit.spec.ts index 5654c1635..c0b7819d9 100644 --- a/blocksuite/blocks/src/__tests__/adapters/markdown.unit.spec.ts +++ b/blocksuite/blocks/src/__tests__/adapters/markdown.unit.spec.ts @@ -1684,7 +1684,7 @@ hhh const md = `\ | Title | Status | Date | Number | Progress | MultiSelect | RichText | Link | Checkbox | | ------ | ----------- | ---------- | ------ | -------- | ----------- | --------------------------- | ------------------ | -------- | -| Task 1 | TODO | 2023-12-15 | 1 | 65 | test1,test2 | [test2](https://google.com) | https://google.com | true | +| Task 1 | TODO | 2023-12-15 | 1 | 65 | test1,test2 | [test2](https://google.com) | https://google.com | True | | Task 2 | In Progress | 2023-12-20 | | | | test1 | | | `; const mdAdapter = new MarkdownAdapter(createJob(), provider); diff --git a/blocksuite/blocks/src/__tests__/adapters/plain-text.unit.spec.ts b/blocksuite/blocks/src/__tests__/adapters/plain-text.unit.spec.ts index ee951b255..2552034b2 100644 --- a/blocksuite/blocks/src/__tests__/adapters/plain-text.unit.spec.ts +++ b/blocksuite/blocks/src/__tests__/adapters/plain-text.unit.spec.ts @@ -1417,7 +1417,7 @@ describe('snapshot to plain text', () => { const plainText = `\ | Title | Status | Date | Number | Progress | MultiSelect | RichText | Link | Checkbox | | ------ | ----------- | ---------- | ------ | -------- | ----------- | ------------------------- | ------------------ | -------- | -| Task 1 | TODO | 2023-12-15 | 1 | 65 | test1,test2 | test2: https://google.com | https://google.com | true | +| Task 1 | TODO | 2023-12-15 | 1 | 65 | test1,test2 | test2: https://google.com | https://google.com | True | | Task 2 | In Progress | 2023-12-20 | | | | test1 | | | `; const plainTextAdapter = new PlainTextAdapter(createJob(), provider);