Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 14 additions & 11 deletions blocks/canvas/editor-utils/multi-block.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,16 @@ function normalize(name) {
return (name || '').toLowerCase().replace(/[\s_-]+/g, ' ').trim();
}

function variantBaseName(variant) {
if (variant?.variants) return variant.name || '';
const match = (variant?.name || '').match(/^(.*\S)\s*\(([^)]+)\)\s*$/);
return match ? match[1].trim() : (variant?.name || '');
function tableBlockName(table) {
const header = table.rows?.[0]?.cells?.[0]?.textContent || '';
return header.replace(/\s*\([^)]*\)\s*$/, '');
}

function templateRowIn(dom, target) {
if (!dom) return null;
const tables = dom.tagName === 'TABLE' ? [dom] : [...(dom.querySelectorAll?.('table') || [])];
const table = tables.find((t) => t.rows?.[1] && normalize(tableBlockName(t)) === target);
return table?.rows[1] ?? null;
}

/** Pure check: does the "editor" sheet mark `blockName` as a multi block? */
Expand All @@ -28,17 +34,14 @@ export async function isMultiBlock(org, site, blockName) {
return isMultiBlockConfigured(await loadBlockEditor(org, site), blockName);
}

/** The library block's first item row (the row after the header), or null. */
/** First item row (the row after the header) of a library table for `blockName`, or null. */
export async function findTemplateRow(blocks, blockName) {
const target = normalize(blockName);
const matches = await Promise.all((blocks || []).map(async (block) => {
const rows = await Promise.all((blocks || []).map(async (block) => {
const variants = (await block.loadVariants) || [];
return variants.find((v) => v.dom && normalize(variantBaseName(v)) === target) || null;
return variants.reduce((row, v) => row || templateRowIn(v.dom, target), null);
}));
const match = matches.find(Boolean);
if (!match) return null;
const table = match.dom.tagName === 'TABLE' ? match.dom : match.dom.querySelector?.('table');
return table?.rows?.[1] ?? null;
return rows.find(Boolean) ?? null;
}

export async function getMultiBlockTemplateRow(org, site, blockName) {
Expand Down
86 changes: 86 additions & 0 deletions test/unit/blocks/canvas/editor-utils/multi-block.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ setNx('/test/fixtures/nx', { hostname: 'example.com' });
const { createTestEditor, destroyEditor } = await import('../../edit/prose/test-helpers.js');
const { isMultiBlockConfigured, findTemplateRow } = await import('../../../../../blocks/canvas/editor-utils/multi-block.js');
const { appendBlockRow } = await import('../../../../../blocks/canvas/editor-utils/blocks.js');
const { getBlockVariants } = await import('../../../../../blocks/canvas/ew-panel-extensions/helpers.js');

const EDITOR_ROWS = [
{ block: 'section-metadata', property: 'kv' },
Expand Down Expand Up @@ -56,6 +57,91 @@ describe('findTemplateRow', () => {
});
});

describe('findTemplateRow with parsed library docs', () => {
let savedFetch;
beforeEach(() => { savedFetch = window.fetch; });
afterEach(() => { window.fetch = savedFetch; });

const CARDS = '<div class="cards"><div><div>img</div><div>card text</div></div><div><div>img2</div><div>card two</div></div></div>';

async function templateRowText(body, blockName = 'cards') {
window.fetch = () => Promise.resolve(new Response(`<body><div>${body}</div></body>`, { status: 200 }));
const blocks = [{ loadVariants: getBlockVariants('/mock-path') }];
const row = await findTemplateRow(blocks, blockName);
return row ? row.textContent : null;
}

it('finds the row of a standalone block named by its heading', async () => {
const text = await templateRowText(`<h2>Cards</h2>${CARDS}`);
expect(text).to.be.a('string').that.contains('card text');
});

it('finds the row when the heading label differs from the block class', async () => {
const text = await templateRowText(`<h2>Fancy Cards</h2>${CARDS}`);
expect(text).to.be.a('string').that.contains('card text');
});

it('finds the row when library metadata renames the block', async () => {
const text = await templateRowText(`${CARDS}<div class="library-metadata"><div><div>name</div><div>Promo</div></div></div>`);
expect(text).to.be.a('string').that.contains('card text');
});

it('finds the row inside a container group whose heading differs from the block', async () => {
const text = await templateRowText(`
<h2>Card Grid</h2>
<div class="library-container-start"></div>
${CARDS}
<div class="library-container-end"></div>`);
expect(text).to.be.a('string').that.contains('card text');
});

it('finds the row inside a container group without a heading', async () => {
const text = await templateRowText(`
<div class="library-container-start"></div>
${CARDS}
<div class="library-container-end"></div>`);
expect(text).to.be.a('string').that.contains('card text');
});

it('uses the matching block, not the first table, inside a container group', async () => {
const text = await templateRowText(`
<h2>Cards</h2>
<div class="library-container-start"></div>
<div class="columns"><div><div>col a</div><div>col b</div></div></div>
${CARDS}
<div class="library-container-end"></div>`);
expect(text).to.be.a('string').that.contains('card text');
expect(text).to.not.contain('col a');
});

it('skips a header-only block preceding the match inside a container group', async () => {
const text = await templateRowText(`
<h2>Cards</h2>
<div class="library-container-start"></div>
<div class="divider"></div>
${CARDS}
<div class="library-container-end"></div>`);
expect(text).to.be.a('string').that.contains('card text');
});

it('matches a block whose class carries variants', async () => {
const text = await templateRowText(`
<div class="library-container-start"></div>
<div class="cards dark wide"><div><div>img</div><div>card text</div></div></div>
<div class="library-container-end"></div>`);
expect(text).to.be.a('string').that.contains('card text');
});

it('does not match a group that lacks the block', async () => {
const text = await templateRowText(`
<h2>Cards</h2>
<div class="library-container-start"></div>
<div class="columns"><div><div>col a</div><div>col b</div></div></div>
<div class="library-container-end"></div>`);
expect(text).to.equal(null);
});
});

describe('appendBlockRow', () => {
let editor;
beforeEach(async () => { editor = await createTestEditor(); });
Expand Down
Loading