refactor(editor): getFirstContentBlock -> getFirstBlock & getLastContentBlock -> getLastBlock (#10809)
This commit is contained in:
@@ -0,0 +1,230 @@
|
|||||||
|
/**
|
||||||
|
* @vitest-environment happy-dom
|
||||||
|
*/
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { getFirstBlockCommand } from '../../../commands/block-crud/get-first-content-block';
|
||||||
|
import { affine } from '../../helpers/affine-template';
|
||||||
|
|
||||||
|
describe('commands/block-crud', () => {
|
||||||
|
describe('getFirstBlockCommand', () => {
|
||||||
|
it('should return null when root is not exists', () => {
|
||||||
|
const host = affine`<affine-page></affine-page>`;
|
||||||
|
|
||||||
|
const [_, { firstBlock }] = host.command.exec(getFirstBlockCommand, {
|
||||||
|
role: 'content',
|
||||||
|
root: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(firstBlock).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return first block with content role when found', () => {
|
||||||
|
const host = affine`
|
||||||
|
<affine-page>
|
||||||
|
<affine-note id="note-1">
|
||||||
|
<affine-paragraph id="paragraph-1-1">First Paragraph</affine-paragraph>
|
||||||
|
<affine-paragraph id="paragraph-1-2">Second Paragraph</affine-paragraph>
|
||||||
|
</affine-note>
|
||||||
|
<affine-note id="note-2">
|
||||||
|
<affine-paragraph id="paragraph-2-1">First Paragraph</affine-paragraph>
|
||||||
|
<affine-paragraph id="paragraph-2-2">Second Paragraph</affine-paragraph>
|
||||||
|
</affine-note>
|
||||||
|
</affine-page>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const [_, { firstBlock }] = host.command.exec(getFirstBlockCommand, {
|
||||||
|
role: 'hub',
|
||||||
|
root: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(firstBlock?.id).toBe('note-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return first block with any role in the array when found', () => {
|
||||||
|
const host = affine`
|
||||||
|
<affine-page>
|
||||||
|
<affine-note id="note-1">
|
||||||
|
<affine-paragraph id="paragraph-1-1">First Paragraph</affine-paragraph>
|
||||||
|
<affine-paragraph id="paragraph-1-2">Second Paragraph</affine-paragraph>
|
||||||
|
</affine-note>
|
||||||
|
<affine-note id="note-2">
|
||||||
|
<affine-paragraph id="paragraph-2-1">First Paragraph</affine-paragraph>
|
||||||
|
<affine-paragraph id="paragraph-2-2">Second Paragraph</affine-paragraph>
|
||||||
|
</affine-note>
|
||||||
|
</affine-page>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const [_, { firstBlock }] = host.command.exec(getFirstBlockCommand, {
|
||||||
|
role: ['hub', 'content'],
|
||||||
|
root: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(firstBlock?.id).toBe('note-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return first block with specified flavour when found', () => {
|
||||||
|
const host = affine`
|
||||||
|
<affine-page>
|
||||||
|
<affine-note id="note-1">
|
||||||
|
<affine-paragraph id="paragraph-1">Paragraph</affine-paragraph>
|
||||||
|
<affine-list id="list-1">List Item</affine-list>
|
||||||
|
</affine-note>
|
||||||
|
</affine-page>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const note = host.doc.getBlock('note-1')?.model;
|
||||||
|
|
||||||
|
const [_, { firstBlock }] = host.command.exec(getFirstBlockCommand, {
|
||||||
|
flavour: 'affine:list',
|
||||||
|
root: note,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(firstBlock?.id).toBe('list-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return first block with any flavour in the array when found', () => {
|
||||||
|
const host = affine`
|
||||||
|
<affine-page>
|
||||||
|
<affine-note id="note-1">
|
||||||
|
<affine-paragraph id="paragraph-1">Paragraph</affine-paragraph>
|
||||||
|
<affine-list id="list-1">List Item</affine-list>
|
||||||
|
</affine-note>
|
||||||
|
</affine-page>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const note = host.doc.getBlock('note-1')?.model;
|
||||||
|
|
||||||
|
const [_, { firstBlock }] = host.command.exec(getFirstBlockCommand, {
|
||||||
|
flavour: ['affine:list', 'affine:code'],
|
||||||
|
root: note,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(firstBlock?.id).toBe('list-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return first block matching both role and flavour when both specified', () => {
|
||||||
|
const host = affine`
|
||||||
|
<affine-page>
|
||||||
|
<affine-note id="note-1">
|
||||||
|
<affine-paragraph id="paragraph-1">Content Paragraph</affine-paragraph>
|
||||||
|
<affine-list id="list-1">Content List</affine-list>
|
||||||
|
<affine-paragraph id="paragraph-2">hub Paragraph</affine-paragraph>
|
||||||
|
</affine-note>
|
||||||
|
</affine-page>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const note = host.doc.getBlock('note-1')?.model;
|
||||||
|
const [_, { firstBlock }] = host.command.exec(getFirstBlockCommand, {
|
||||||
|
role: 'content',
|
||||||
|
flavour: 'affine:list',
|
||||||
|
root: note,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(firstBlock?.id).toBe('list-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return first block with default roles when role not specified', () => {
|
||||||
|
const host = affine`
|
||||||
|
<affine-page>
|
||||||
|
<affine-note id="note-1">
|
||||||
|
<affine-paragraph id="paragraph-1">hub Paragraph</affine-paragraph>
|
||||||
|
<affine-paragraph id="paragraph-2">Content Paragraph</affine-paragraph>
|
||||||
|
<affine-paragraph id="paragraph-3">Hub Paragraph</affine-paragraph>
|
||||||
|
</affine-note>
|
||||||
|
</affine-page>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const [_, { firstBlock }] = host.command.exec(getFirstBlockCommand, {
|
||||||
|
root: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(firstBlock?.id).toBe('note-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return first block with specified role when found', () => {
|
||||||
|
const host = affine`
|
||||||
|
<affine-page>
|
||||||
|
<affine-note id="note-1">
|
||||||
|
<affine-paragraph id="paragraph-1">Content Paragraph</affine-paragraph>
|
||||||
|
<affine-paragraph id="paragraph-2">hub Paragraph</affine-paragraph>
|
||||||
|
<affine-database id="database-1">Database</affine-database>
|
||||||
|
</affine-note>
|
||||||
|
</affine-page>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const note = host.doc.getBlock('note-1')?.model;
|
||||||
|
|
||||||
|
const [_, { firstBlock }] = host.command.exec(getFirstBlockCommand, {
|
||||||
|
role: 'hub',
|
||||||
|
root: note,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(firstBlock?.id).toBe('database-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null when no blocks with specified role are found in children', () => {
|
||||||
|
const host = affine`
|
||||||
|
<affine-page>
|
||||||
|
<affine-note id="note-1">
|
||||||
|
<affine-paragraph id="paragraph-1">Content Paragraph</affine-paragraph>
|
||||||
|
<affine-paragraph id="paragraph-2">Another Content Paragraph</affine-paragraph>
|
||||||
|
</affine-note>
|
||||||
|
</affine-page>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const note = host.doc.getBlock('note-1')?.model;
|
||||||
|
|
||||||
|
const [_, { firstBlock }] = host.command.exec(getFirstBlockCommand, {
|
||||||
|
role: 'hub',
|
||||||
|
root: note,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(firstBlock).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null when no blocks with specified flavour are found in children', () => {
|
||||||
|
const host = affine`
|
||||||
|
<affine-page>
|
||||||
|
<affine-note id="note-1">
|
||||||
|
<affine-paragraph id="paragraph-1">Paragraph</affine-paragraph>
|
||||||
|
<affine-paragraph id="paragraph-2">Another Paragraph</affine-paragraph>
|
||||||
|
</affine-note>
|
||||||
|
</affine-page>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const note = host.doc.getBlock('note-1')?.model;
|
||||||
|
|
||||||
|
const [_, { firstBlock }] = host.command.exec(getFirstBlockCommand, {
|
||||||
|
flavour: 'affine:list',
|
||||||
|
root: note,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(firstBlock).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return first block with specified role within specified root subtree', () => {
|
||||||
|
const host = affine`
|
||||||
|
<affine-page>
|
||||||
|
<affine-note id="note-1">
|
||||||
|
<affine-paragraph id="paragraph-1-1">1-1 Content</affine-paragraph>
|
||||||
|
<affine-paragraph id="paragraph-1-2">1-2 hub</affine-paragraph>
|
||||||
|
</affine-note>
|
||||||
|
<affine-note id="note-2">
|
||||||
|
<affine-paragraph id="paragraph-2-1">2-1 hub</affine-paragraph>
|
||||||
|
<affine-paragraph id="paragraph-2-2">2-2 Content</affine-paragraph>
|
||||||
|
</affine-note>
|
||||||
|
</affine-page>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const note = host.doc.getBlock('note-2')?.model;
|
||||||
|
|
||||||
|
const [_, { firstBlock }] = host.command.exec(getFirstBlockCommand, {
|
||||||
|
role: 'content',
|
||||||
|
root: note,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(firstBlock?.id).toBe('paragraph-2-1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
/**
|
|
||||||
* @vitest-environment happy-dom
|
|
||||||
*/
|
|
||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
|
|
||||||
import { getFirstContentBlockCommand } from '../../../commands/block-crud/get-first-content-block';
|
|
||||||
import { affine } from '../../helpers/affine-template';
|
|
||||||
|
|
||||||
describe('commands/block-crud', () => {
|
|
||||||
describe('getFirstContentBlockCommand', () => {
|
|
||||||
it('should return null when root is not provided and no note block exists', () => {
|
|
||||||
const host = affine`<affine-page></affine-page>`;
|
|
||||||
|
|
||||||
const [_, { firstBlock }] = host.command.exec(
|
|
||||||
getFirstContentBlockCommand,
|
|
||||||
{
|
|
||||||
root: undefined,
|
|
||||||
std: {
|
|
||||||
host,
|
|
||||||
} as any,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(firstBlock).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return first content block when found', () => {
|
|
||||||
const host = affine`
|
|
||||||
<affine-page>
|
|
||||||
<affine-note id="note-1">
|
|
||||||
<affine-paragraph id="paragraph-1">First Paragraph</affine-paragraph>
|
|
||||||
<affine-paragraph id="paragraph-2">Second Paragraph</affine-paragraph>
|
|
||||||
</affine-note>
|
|
||||||
</affine-page>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const [_, { firstBlock }] = host.command.exec(
|
|
||||||
getFirstContentBlockCommand,
|
|
||||||
{
|
|
||||||
root: undefined,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(firstBlock?.id).toBe('paragraph-1');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return null when no content blocks are found in children', () => {
|
|
||||||
const host = affine`
|
|
||||||
<affine-page>
|
|
||||||
<affine-note id="note-1">
|
|
||||||
</affine-note>
|
|
||||||
</affine-page>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const [_, { firstBlock }] = host.command.exec(
|
|
||||||
getFirstContentBlockCommand,
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(firstBlock).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return first content block within specified root subtree', () => {
|
|
||||||
const host = affine`
|
|
||||||
<affine-page>
|
|
||||||
<affine-note id="note-1">
|
|
||||||
<affine-paragraph id="paragraph-1-1">1-1 Paragraph</affine-paragraph>
|
|
||||||
<affine-paragraph id="paragraph-1-2">1-2 Paragraph</affine-paragraph>
|
|
||||||
</affine-note>
|
|
||||||
<affine-note id="note-2">
|
|
||||||
<affine-paragraph id="paragraph-2-1">2-1 Paragraph</affine-paragraph>
|
|
||||||
<affine-paragraph id="paragraph-2-2">2-2 Paragraph</affine-paragraph>
|
|
||||||
</affine-note>
|
|
||||||
</affine-page>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const noteBlock = host.doc.getBlock('note-2')?.model;
|
|
||||||
|
|
||||||
const [_, { firstBlock }] = host.command.exec(
|
|
||||||
getFirstContentBlockCommand,
|
|
||||||
{
|
|
||||||
root: noteBlock,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(firstBlock?.id).toBe('paragraph-2-1');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
/**
|
||||||
|
* @vitest-environment happy-dom
|
||||||
|
*/
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { getLastBlockCommand } from '../../../commands/block-crud/get-last-content-block';
|
||||||
|
import { affine } from '../../helpers/affine-template';
|
||||||
|
|
||||||
|
describe('commands/block-crud', () => {
|
||||||
|
describe('getLastBlockCommand', () => {
|
||||||
|
it('should return null when root is not exists', () => {
|
||||||
|
const host = affine`<affine-page></affine-page>`;
|
||||||
|
|
||||||
|
const [_, { lastBlock }] = host.command.exec(getLastBlockCommand, {
|
||||||
|
role: 'content',
|
||||||
|
root: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(lastBlock).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return last block with content role when found', () => {
|
||||||
|
const host = affine`
|
||||||
|
<affine-page>
|
||||||
|
<affine-note id="note-1">
|
||||||
|
<affine-paragraph id="paragraph-1-1">First Paragraph</affine-paragraph>
|
||||||
|
<affine-paragraph id="paragraph-1-2">Second Paragraph</affine-paragraph>
|
||||||
|
</affine-note>
|
||||||
|
<affine-note id="note-2">
|
||||||
|
<affine-paragraph id="paragraph-2-1">First Paragraph</affine-paragraph>
|
||||||
|
<affine-paragraph id="paragraph-2-2">Second Paragraph</affine-paragraph>
|
||||||
|
</affine-note>
|
||||||
|
</affine-page>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const [_, { lastBlock }] = host.command.exec(getLastBlockCommand, {
|
||||||
|
role: 'hub',
|
||||||
|
root: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(lastBlock?.id).toBe('note-2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return last block with any role in the array when found', () => {
|
||||||
|
const host = affine`
|
||||||
|
<affine-page>
|
||||||
|
<affine-note id="note-1">
|
||||||
|
<affine-paragraph id="paragraph-1-1">First Paragraph</affine-paragraph>
|
||||||
|
<affine-paragraph id="paragraph-1-2">Second Paragraph</affine-paragraph>
|
||||||
|
</affine-note>
|
||||||
|
<affine-note id="note-2">
|
||||||
|
<affine-paragraph id="paragraph-2-1">First Paragraph</affine-paragraph>
|
||||||
|
<affine-paragraph id="paragraph-2-2">Second Paragraph</affine-paragraph>
|
||||||
|
</affine-note>
|
||||||
|
</affine-page>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const [_, { lastBlock }] = host.command.exec(getLastBlockCommand, {
|
||||||
|
role: ['hub', 'content'],
|
||||||
|
root: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(lastBlock?.id).toBe('note-2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return last block with specified flavour when found', () => {
|
||||||
|
const host = affine`
|
||||||
|
<affine-page>
|
||||||
|
<affine-note id="note-1">
|
||||||
|
<affine-paragraph id="paragraph-1">Paragraph</affine-paragraph>
|
||||||
|
<affine-list id="list-1">List Item</affine-list>
|
||||||
|
</affine-note>
|
||||||
|
</affine-page>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const note = host.doc.getBlock('note-1')?.model;
|
||||||
|
|
||||||
|
const [_, { lastBlock }] = host.command.exec(getLastBlockCommand, {
|
||||||
|
flavour: 'affine:list',
|
||||||
|
root: note,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(lastBlock?.id).toBe('list-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return last block with any flavour in the array when found', () => {
|
||||||
|
const host = affine`
|
||||||
|
<affine-page>
|
||||||
|
<affine-note id="note-1">
|
||||||
|
<affine-paragraph id="paragraph-1">Paragraph</affine-paragraph>
|
||||||
|
<affine-list id="list-1">List Item</affine-list>
|
||||||
|
</affine-note>
|
||||||
|
</affine-page>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const note = host.doc.getBlock('note-1')?.model;
|
||||||
|
|
||||||
|
const [_, { lastBlock }] = host.command.exec(getLastBlockCommand, {
|
||||||
|
flavour: ['affine:list', 'affine:code'],
|
||||||
|
root: note,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(lastBlock?.id).toBe('list-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return last block matching both role and flavour when both specified', () => {
|
||||||
|
const host = affine`
|
||||||
|
<affine-page>
|
||||||
|
<affine-note id="note-1">
|
||||||
|
<affine-paragraph id="paragraph-1">Content Paragraph</affine-paragraph>
|
||||||
|
<affine-list id="list-1">Content List</affine-list>
|
||||||
|
<affine-paragraph id="paragraph-2">hub Paragraph</affine-paragraph>
|
||||||
|
</affine-note>
|
||||||
|
</affine-page>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const note = host.doc.getBlock('note-1')?.model;
|
||||||
|
const [_, { lastBlock }] = host.command.exec(getLastBlockCommand, {
|
||||||
|
role: 'content',
|
||||||
|
flavour: 'affine:list',
|
||||||
|
root: note,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(lastBlock?.id).toBe('list-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return last block with default roles when role not specified', () => {
|
||||||
|
const host = affine`
|
||||||
|
<affine-page>
|
||||||
|
<affine-note id="note-1">
|
||||||
|
<affine-paragraph id="paragraph-1">hub Paragraph</affine-paragraph>
|
||||||
|
<affine-paragraph id="paragraph-2">Content Paragraph</affine-paragraph>
|
||||||
|
<affine-paragraph id="paragraph-3">Hub Paragraph</affine-paragraph>
|
||||||
|
</affine-note>
|
||||||
|
</affine-page>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const [_, { lastBlock }] = host.command.exec(getLastBlockCommand, {
|
||||||
|
root: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(lastBlock?.id).toBe('note-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return last block with specified role when found', () => {
|
||||||
|
const host = affine`
|
||||||
|
<affine-page>
|
||||||
|
<affine-note id="note-1">
|
||||||
|
<affine-paragraph id="paragraph-1">Content Paragraph</affine-paragraph>
|
||||||
|
<affine-paragraph id="paragraph-2">hub Paragraph</affine-paragraph>
|
||||||
|
<affine-database id="database-1">Database</affine-database>
|
||||||
|
</affine-note>
|
||||||
|
</affine-page>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const note = host.doc.getBlock('note-1')?.model;
|
||||||
|
|
||||||
|
const [_, { lastBlock }] = host.command.exec(getLastBlockCommand, {
|
||||||
|
role: 'hub',
|
||||||
|
root: note,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(lastBlock?.id).toBe('database-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null when no blocks with specified role are found in children', () => {
|
||||||
|
const host = affine`
|
||||||
|
<affine-page>
|
||||||
|
<affine-note id="note-1">
|
||||||
|
<affine-paragraph id="paragraph-1">Content Paragraph</affine-paragraph>
|
||||||
|
<affine-paragraph id="paragraph-2">Another Content Paragraph</affine-paragraph>
|
||||||
|
</affine-note>
|
||||||
|
</affine-page>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const note = host.doc.getBlock('note-1')?.model;
|
||||||
|
|
||||||
|
const [_, { lastBlock }] = host.command.exec(getLastBlockCommand, {
|
||||||
|
role: 'hub',
|
||||||
|
root: note,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(lastBlock).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null when no blocks with specified flavour are found in children', () => {
|
||||||
|
const host = affine`
|
||||||
|
<affine-page>
|
||||||
|
<affine-note id="note-1">
|
||||||
|
<affine-paragraph id="paragraph-1">Paragraph</affine-paragraph>
|
||||||
|
<affine-paragraph id="paragraph-2">Another Paragraph</affine-paragraph>
|
||||||
|
</affine-note>
|
||||||
|
</affine-page>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const note = host.doc.getBlock('note-1')?.model;
|
||||||
|
|
||||||
|
const [_, { lastBlock }] = host.command.exec(getLastBlockCommand, {
|
||||||
|
flavour: 'affine:list',
|
||||||
|
root: note,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(lastBlock).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return last block with specified role within specified root subtree', () => {
|
||||||
|
const host = affine`
|
||||||
|
<affine-page>
|
||||||
|
<affine-note id="note-1">
|
||||||
|
<affine-paragraph id="paragraph-1-1">1-1 Content</affine-paragraph>
|
||||||
|
<affine-paragraph id="paragraph-1-2">1-2 hub</affine-paragraph>
|
||||||
|
</affine-note>
|
||||||
|
<affine-note id="note-2">
|
||||||
|
<affine-paragraph id="paragraph-2-1">2-1 hub</affine-paragraph>
|
||||||
|
<affine-paragraph id="paragraph-2-2">2-2 Content</affine-paragraph>
|
||||||
|
</affine-note>
|
||||||
|
</affine-page>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const note = host.doc.getBlock('note-2')?.model;
|
||||||
|
|
||||||
|
const [_, { lastBlock }] = host.command.exec(getLastBlockCommand, {
|
||||||
|
role: 'content',
|
||||||
|
root: note,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(lastBlock?.id).toBe('paragraph-2-2');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
/**
|
|
||||||
* @vitest-environment happy-dom
|
|
||||||
*/
|
|
||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
|
|
||||||
import { getLastContentBlockCommand } from '../../../commands/block-crud/get-last-content-block';
|
|
||||||
import { affine } from '../../helpers/affine-template';
|
|
||||||
|
|
||||||
describe('commands/block-crud', () => {
|
|
||||||
describe('getLastContentBlockCommand', () => {
|
|
||||||
it('should return null when root is not provided and no note block exists', () => {
|
|
||||||
const host = affine`<affine-page></affine-page>`;
|
|
||||||
|
|
||||||
const [_, { lastBlock }] = host.command.exec(getLastContentBlockCommand, {
|
|
||||||
root: undefined,
|
|
||||||
std: {
|
|
||||||
host,
|
|
||||||
} as any,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(lastBlock).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return last content block when found', () => {
|
|
||||||
const host = affine`
|
|
||||||
<affine-page>
|
|
||||||
<affine-note id="note-1">
|
|
||||||
<affine-paragraph id="paragraph-1">First Paragraph</affine-paragraph>
|
|
||||||
<affine-paragraph id="paragraph-2">Second Paragraph</affine-paragraph>
|
|
||||||
</affine-note>
|
|
||||||
</affine-page>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const [_, { lastBlock }] = host.command.exec(getLastContentBlockCommand, {
|
|
||||||
root: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(lastBlock?.id).toBe('paragraph-2');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return null when no content blocks are found in children', () => {
|
|
||||||
const host = affine`
|
|
||||||
<affine-page>
|
|
||||||
<affine-note id="note-1">
|
|
||||||
</affine-note>
|
|
||||||
</affine-page>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const [_, { lastBlock }] = host.command.exec(
|
|
||||||
getLastContentBlockCommand,
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(lastBlock).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return last content block within specified root subtree', () => {
|
|
||||||
const host = affine`
|
|
||||||
<affine-page>
|
|
||||||
<affine-note id="note-1">
|
|
||||||
<affine-paragraph id="paragraph-1-1">1-1 Paragraph</affine-paragraph>
|
|
||||||
<affine-paragraph id="paragraph-1-2">1-2 Paragraph</affine-paragraph>
|
|
||||||
</affine-note>
|
|
||||||
<affine-note id="note-2">
|
|
||||||
<affine-paragraph id="paragraph-2-1">2-1 Paragraph</affine-paragraph>
|
|
||||||
<affine-paragraph id="paragraph-2-2">2-2 Paragraph</affine-paragraph>
|
|
||||||
</affine-note>
|
|
||||||
</affine-page>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const noteBlock = host.doc.getBlock('note-2')?.model;
|
|
||||||
|
|
||||||
const [_, { lastBlock }] = host.command.exec(getLastContentBlockCommand, {
|
|
||||||
root: noteBlock,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(lastBlock?.id).toBe('paragraph-2-2');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
DatabaseBlockSchemaExtension,
|
||||||
ImageBlockSchemaExtension,
|
ImageBlockSchemaExtension,
|
||||||
ListBlockSchemaExtension,
|
ListBlockSchemaExtension,
|
||||||
NoteBlockSchemaExtension,
|
NoteBlockSchemaExtension,
|
||||||
@@ -18,6 +19,7 @@ const extensions = [
|
|||||||
ParagraphBlockSchemaExtension,
|
ParagraphBlockSchemaExtension,
|
||||||
ListBlockSchemaExtension,
|
ListBlockSchemaExtension,
|
||||||
ImageBlockSchemaExtension,
|
ImageBlockSchemaExtension,
|
||||||
|
DatabaseBlockSchemaExtension,
|
||||||
];
|
];
|
||||||
|
|
||||||
// Mapping from tag names to flavours
|
// Mapping from tag names to flavours
|
||||||
@@ -27,6 +29,7 @@ const tagToFlavour: Record<string, string> = {
|
|||||||
'affine-paragraph': 'affine:paragraph',
|
'affine-paragraph': 'affine:paragraph',
|
||||||
'affine-list': 'affine:list',
|
'affine-list': 'affine:list',
|
||||||
'affine-image': 'affine:image',
|
'affine-image': 'affine:image',
|
||||||
|
'affine-database': 'affine:database',
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
import {
|
|
||||||
BlockSchemaExtension,
|
|
||||||
defineBlockSchema,
|
|
||||||
type Store,
|
|
||||||
Text,
|
|
||||||
} from '@blocksuite/store';
|
|
||||||
import { TestWorkspace } from '@blocksuite/store/test';
|
|
||||||
import { type Element as HappyDOMElement, Window } from 'happy-dom';
|
|
||||||
|
|
||||||
// Define schema
|
|
||||||
const PageBlockSchema = defineBlockSchema({
|
|
||||||
flavour: 'affine:page',
|
|
||||||
props: () => ({}),
|
|
||||||
metadata: {
|
|
||||||
version: 1,
|
|
||||||
role: 'root',
|
|
||||||
children: ['affine:note'],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const NoteBlockSchema = defineBlockSchema({
|
|
||||||
flavour: 'affine:note',
|
|
||||||
props: () => ({}),
|
|
||||||
metadata: {
|
|
||||||
version: 1,
|
|
||||||
role: 'hub',
|
|
||||||
parent: ['affine:page'],
|
|
||||||
children: ['affine:paragraph'],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const ParagraphBlockSchema = defineBlockSchema({
|
|
||||||
flavour: 'affine:paragraph',
|
|
||||||
props: internal => ({
|
|
||||||
text: internal.Text(),
|
|
||||||
}),
|
|
||||||
metadata: {
|
|
||||||
version: 1,
|
|
||||||
role: 'content',
|
|
||||||
parent: ['affine:note'],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create schema extensions
|
|
||||||
const PageBlockSchemaExtension = BlockSchemaExtension(PageBlockSchema);
|
|
||||||
const NoteBlockSchemaExtension = BlockSchemaExtension(NoteBlockSchema);
|
|
||||||
const ParagraphBlockSchemaExtension =
|
|
||||||
BlockSchemaExtension(ParagraphBlockSchema);
|
|
||||||
|
|
||||||
// Extensions array
|
|
||||||
const extensions = [
|
|
||||||
PageBlockSchemaExtension,
|
|
||||||
NoteBlockSchemaExtension,
|
|
||||||
ParagraphBlockSchemaExtension,
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse HTML string and create document block structure
|
|
||||||
* @param node Current DOM node
|
|
||||||
* @param doc Document object
|
|
||||||
* @param parentId Parent block ID
|
|
||||||
* @returns Created block ID
|
|
||||||
*/
|
|
||||||
function processNode(
|
|
||||||
node: HappyDOMElement,
|
|
||||||
doc: Store,
|
|
||||||
parentId?: string
|
|
||||||
): string | undefined {
|
|
||||||
// Skip text nodes and comments
|
|
||||||
if (node.nodeType !== 1) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tagName = node.tagName.toLowerCase();
|
|
||||||
let blockId: string | undefined = undefined;
|
|
||||||
|
|
||||||
// Create appropriate block based on tag name
|
|
||||||
if (tagName === 'affine-page') {
|
|
||||||
blockId = doc.addBlock('affine:page', {}, parentId);
|
|
||||||
} else if (tagName === 'affine-note') {
|
|
||||||
blockId = doc.addBlock('affine:note', {}, parentId);
|
|
||||||
} else if (tagName === 'affine-paragraph') {
|
|
||||||
// Get paragraph text content
|
|
||||||
const textContent = node.textContent || '';
|
|
||||||
// Get attributes
|
|
||||||
const props: Record<string, any> = { text: new Text(textContent) };
|
|
||||||
|
|
||||||
// Process custom attributes
|
|
||||||
for (const attr of Array.from(node.attributes)) {
|
|
||||||
if (attr.name === 'type') {
|
|
||||||
props.type = attr.value;
|
|
||||||
} else if (attr.name === 'checked' && attr.value === 'true') {
|
|
||||||
props.checked = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
blockId = doc.addBlock('affine:paragraph', props, parentId);
|
|
||||||
} else {
|
|
||||||
console.warn(`Unknown tag name: ${tagName}`);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process child nodes
|
|
||||||
for (const childNode of Array.from(node.children) as HappyDOMElement[]) {
|
|
||||||
processNode(childNode, doc, blockId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return blockId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create document from HTML string
|
|
||||||
* @param template HTML template string
|
|
||||||
* @returns Created document object
|
|
||||||
*/
|
|
||||||
export function createDocFromHTML(template: string) {
|
|
||||||
const workspace = new TestWorkspace({});
|
|
||||||
workspace.meta.initialize();
|
|
||||||
|
|
||||||
const doc = workspace.createDoc({ id: 'test-doc', extensions });
|
|
||||||
|
|
||||||
doc.load(() => {
|
|
||||||
const window = new Window();
|
|
||||||
const document = window.document;
|
|
||||||
const container = document.createElement('div');
|
|
||||||
container.innerHTML = template;
|
|
||||||
|
|
||||||
// Process each child node of the root
|
|
||||||
for (const childNode of Array.from(container.children)) {
|
|
||||||
processNode(childNode, doc);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
@@ -1,35 +1,57 @@
|
|||||||
import type { Command } from '@blocksuite/block-std';
|
import type { Command } from '@blocksuite/block-std';
|
||||||
import type { BlockModel } from '@blocksuite/store';
|
import type { BlockModel } from '@blocksuite/store';
|
||||||
|
|
||||||
import { getFirstNoteBlock } from '../../utils';
|
type Role = 'content' | 'hub';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the first content block in the document
|
* Get the first block with specified roles and flavours in the document
|
||||||
*
|
*
|
||||||
* @param ctx - Command context
|
* @param ctx - Command context
|
||||||
* @param ctx.root - The root note block model
|
* @param ctx.root - The root note block model
|
||||||
|
* @param ctx.role - The roles to match, can be string or string array. If not provided, default to all supported roles.
|
||||||
|
* @param ctx.flavour - The flavours to match, can be string or string array. If not provided, match any flavour.
|
||||||
* @param next - Next handler function
|
* @param next - Next handler function
|
||||||
* @returns The first content block model or null
|
* @returns The first block model matched or null
|
||||||
*/
|
*/
|
||||||
export const getFirstContentBlockCommand: Command<
|
export const getFirstBlockCommand: Command<
|
||||||
{
|
{
|
||||||
root?: BlockModel;
|
root?: BlockModel;
|
||||||
|
role?: Role | Role[];
|
||||||
|
flavour?: string | string[];
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
firstBlock: BlockModel | null;
|
firstBlock: BlockModel | null;
|
||||||
}
|
}
|
||||||
> = (ctx, next) => {
|
> = (ctx, next) => {
|
||||||
const doc = ctx.std.host.doc;
|
const root = ctx.root || ctx.std.host.doc.root;
|
||||||
const noteBlock = ctx.root ?? getFirstNoteBlock(doc);
|
if (!root) {
|
||||||
if (!noteBlock) {
|
|
||||||
next({
|
next({
|
||||||
firstBlock: null,
|
firstBlock: null,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const child of noteBlock.children) {
|
const defaultRoles = ['content', 'hub'];
|
||||||
if (child.role === 'content') {
|
|
||||||
|
const rolesToMatch = ctx.role
|
||||||
|
? Array.isArray(ctx.role)
|
||||||
|
? ctx.role
|
||||||
|
: [ctx.role]
|
||||||
|
: defaultRoles;
|
||||||
|
|
||||||
|
const flavoursToMatch = ctx.flavour
|
||||||
|
? Array.isArray(ctx.flavour)
|
||||||
|
? ctx.flavour
|
||||||
|
: [ctx.flavour]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
for (const child of root.children) {
|
||||||
|
const roleMatches = rolesToMatch.includes(child.role);
|
||||||
|
|
||||||
|
const flavourMatches =
|
||||||
|
!flavoursToMatch || flavoursToMatch.includes(child.flavour);
|
||||||
|
|
||||||
|
if (roleMatches && flavourMatches) {
|
||||||
next({
|
next({
|
||||||
firstBlock: child,
|
firstBlock: child,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,35 +1,59 @@
|
|||||||
import type { Command } from '@blocksuite/block-std';
|
import type { Command } from '@blocksuite/block-std';
|
||||||
import type { BlockModel } from '@blocksuite/store';
|
import type { BlockModel } from '@blocksuite/store';
|
||||||
|
|
||||||
import { getLastNoteBlock } from '../../utils';
|
type Role = 'content' | 'hub';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the last content block in the document
|
* Get the last block with specified roles and flavours in the document
|
||||||
*
|
*
|
||||||
* @param ctx - Command context
|
* @param ctx - Command context
|
||||||
* @param ctx.root - The root note block model
|
* @param ctx.root - The root note block model
|
||||||
|
* @param ctx.role - The roles to match, can be string or string array. If not provided, default to all supported roles.
|
||||||
|
* @param ctx.flavour - The flavours to match, can be string or string array. If not provided, match any flavour.
|
||||||
* @param next - Next handler function
|
* @param next - Next handler function
|
||||||
* @returns The last content block model or null
|
* @returns The last block model matched or null
|
||||||
*/
|
*/
|
||||||
export const getLastContentBlockCommand: Command<
|
export const getLastBlockCommand: Command<
|
||||||
{
|
{
|
||||||
root?: BlockModel;
|
root?: BlockModel;
|
||||||
|
role?: Role | Role[];
|
||||||
|
flavour?: string | string[];
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
lastBlock: BlockModel | null;
|
lastBlock: BlockModel | null;
|
||||||
}
|
}
|
||||||
> = (ctx, next) => {
|
> = (ctx, next) => {
|
||||||
const noteBlock = ctx.root ?? getLastNoteBlock(ctx.std.host.doc);
|
const root = ctx.root || ctx.std.host.doc.root;
|
||||||
if (!noteBlock) {
|
if (!root) {
|
||||||
next({
|
next({
|
||||||
lastBlock: null,
|
lastBlock: null,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const children = noteBlock.children;
|
const defaultRoles = ['content', 'hub'];
|
||||||
|
|
||||||
|
const rolesToMatch = ctx.role
|
||||||
|
? Array.isArray(ctx.role)
|
||||||
|
? ctx.role
|
||||||
|
: [ctx.role]
|
||||||
|
: defaultRoles;
|
||||||
|
|
||||||
|
const flavoursToMatch = ctx.flavour
|
||||||
|
? Array.isArray(ctx.flavour)
|
||||||
|
? ctx.flavour
|
||||||
|
: [ctx.flavour]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const children = root.children;
|
||||||
for (let i = children.length - 1; i >= 0; i--) {
|
for (let i = children.length - 1; i >= 0; i--) {
|
||||||
if (children[i].role === 'content') {
|
const roleMatches = rolesToMatch.includes(children[i].role);
|
||||||
|
|
||||||
|
const flavourMatches =
|
||||||
|
!flavoursToMatch || flavoursToMatch.includes(children[i].flavour);
|
||||||
|
|
||||||
|
// Both role and flavour must match
|
||||||
|
if (roleMatches && flavourMatches) {
|
||||||
next({
|
next({
|
||||||
lastBlock: children[i],
|
lastBlock: children[i],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export { getBlockIndexCommand } from './get-block-index.js';
|
export { getBlockIndexCommand } from './get-block-index.js';
|
||||||
export { getFirstContentBlockCommand } from './get-first-content-block.js';
|
export { getFirstBlockCommand } from './get-first-content-block.js';
|
||||||
export { getLastContentBlockCommand } from './get-last-content-block.js';
|
export { getLastBlockCommand } from './get-last-content-block.js';
|
||||||
export { getNextBlockCommand } from './get-next-block.js';
|
export { getNextBlockCommand } from './get-next-block.js';
|
||||||
export { getPrevBlockCommand } from './get-prev-block.js';
|
export { getPrevBlockCommand } from './get-prev-block.js';
|
||||||
export { getSelectedBlocksCommand } from './get-selected-blocks.js';
|
export { getSelectedBlocksCommand } from './get-selected-blocks.js';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export {
|
export {
|
||||||
getBlockIndexCommand,
|
getBlockIndexCommand,
|
||||||
getFirstContentBlockCommand,
|
getFirstBlockCommand,
|
||||||
getLastContentBlockCommand,
|
getLastBlockCommand,
|
||||||
getNextBlockCommand,
|
getNextBlockCommand,
|
||||||
getPrevBlockCommand,
|
getPrevBlockCommand,
|
||||||
getSelectedBlocksCommand,
|
getSelectedBlocksCommand,
|
||||||
|
|||||||
Reference in New Issue
Block a user