Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
fea4342
add base listing block component
nileshgulia1 Mar 17, 2026
2927d92
feat(QuerystringWidget): add Querystring Widget for seven
nileshgulia1 Mar 19, 2026
20f5feb
refactor: remove listing block edits
nileshgulia1 Mar 19, 2026
116ddbf
Merge branch 'seven' into seven-querystring
nileshgulia1 Mar 19, 2026
3cd04b5
fix: eslint
nileshgulia1 Mar 19, 2026
a7b584b
refactor: get Querystring options for criteria from backend
nileshgulia1 Mar 19, 2026
34559ed
Merge branch 'seven' into seven-querystring
nileshgulia1 Mar 20, 2026
76f3a57
Merge branch 'seven' into seven-querystring
nileshgulia1 May 18, 2026
93f1036
refactor: use Selects from quanta
nileshgulia1 May 18, 2026
73da4b8
chore: update changelog
nileshgulia1 May 18, 2026
971783d
feat: add Seven listing block
nileshgulia1 May 18, 2026
27d7075
Merge branch 'pr-8017' into seven-listing-block
nileshgulia1 May 18, 2026
c1bd20c
WIP: listing block
nileshgulia1 May 19, 2026
b7ae9a2
Merge branch 'seven' into seven-listing-block
nileshgulia1 May 19, 2026
c3c6226
Merge branch 'seven' into seven-querystring
nileshgulia1 May 19, 2026
a17be71
WIP: listing block
nileshgulia1 May 19, 2026
bf41603
fix placeholder in listing edit
nileshgulia1 May 19, 2026
f812e32
fix: use quanta components for button and input
nileshgulia1 May 19, 2026
40bfda7
fix sortableIndexes
nileshgulia1 May 19, 2026
a919337
fix make Criteria a searchable select
nileshgulia1 May 19, 2026
4c0bdfb
fix storybook for QuerystringWidget
nileshgulia1 May 19, 2026
45d3070
feat: add ComboBox.quanta and use it in Select for queryString
nileshgulia1 May 19, 2026
2d25f9c
Merge branch 'seven' into seven-querystring
nileshgulia1 May 19, 2026
56d25a0
Merge branch 'seven-querystring' into seven-listing-block
nileshgulia1 May 19, 2026
ef68b6d
fix Combobox.quanta import
nileshgulia1 May 19, 2026
f833bf6
Merge branch 'seven-querystring' into seven-listing-block
nileshgulia1 May 19, 2026
eeb54e6
refactor: add missing file querystringSearch
nileshgulia1 May 19, 2026
5a1ce36
Merge branch 'seven-querystring' into seven-listing-block
nileshgulia1 May 19, 2026
acc5311
chore: update changelog
nileshgulia1 May 19, 2026
57ca619
Merge branch 'seven' into seven-querystring
nileshgulia1 May 21, 2026
b080787
refactor: create useQuerystringResults hook, move search logic to it
nileshgulia1 May 21, 2026
c08a454
Merge branch 'seven-querystring' into seven-listing-block
nileshgulia1 May 21, 2026
202d8fe
fix: use textWidget with type 'number' instead of NumberField
nileshgulia1 Jun 1, 2026
f66df64
Merge branch 'seven' into seven-querystring
nileshgulia1 Jun 1, 2026
d583074
Merge branch 'seven-querystring' into seven-listing-block
nileshgulia1 Jun 1, 2026
4c3e765
Merge branch 'seven' into seven-querystring
nileshgulia1 Jun 3, 2026
168d93b
Merge branch 'seven-querystring' into seven-listing-block
nileshgulia1 Jun 3, 2026
167766c
Merge branch 'seven' into seven-listing-block
nileshgulia1 Jun 4, 2026
0794868
fix: use react-router from catalog, fix imports
nileshgulia1 Jun 4, 2026
541a788
refactor: add tests and clean up querystringWidget
nileshgulia1 Jun 8, 2026
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
48 changes: 48 additions & 0 deletions packages/blocks/Listing/ListingBlockEdit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useEffect } from 'react';
import type { BlockEditProps } from '@plone/types';

import ListingBlockView from './ListingBlockView';
import { useQuerystringResults } from './useQuerystringResults';

const hasQuery = (value: any): boolean => {
if (!value) return false;

if (typeof value === 'object' && 'query' in value) {
const query = value.query;
return Array.isArray(query) && query.length > 0;
}

return false;
};

const ListingEdit = (props: BlockEditProps) => {
const { data, setBlock } = props;
const hasListingQuery = hasQuery(data.querystring as any);
const { items } = useQuerystringResults(data.querystring as any);

useEffect(() => {
if (hasListingQuery && items.length > 0) {
setBlock({
...data,
items,
});
}
}, [items, hasListingQuery, data, setBlock]);

if (!hasListingQuery) {
return (
<div
className={[
'placeholder rounded-md border border-dashed border-quanta-azure bg-quanta-air',
'p-6 text-center text-quanta-iron',
].join(' ')}
>
<p>No Results Found</p>
</div>
);
}

return <ListingBlockView {...props} isEditMode />;
};

export default ListingEdit;
7 changes: 7 additions & 0 deletions packages/blocks/Listing/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import React from 'react';
import type { BlockConfigBase } from '@plone/types';
import { ListIcon } from '@plone/components/Icons';
import { ListingSchema } from './schema';

const ListingBlockInfo = {
id: 'listing',
title: 'Listing',
view: React.lazy(
() => import(/* webpackChunkName: "plone-blocks" */ './ListingBlockView'),
),
edit: React.lazy(
() => import(/* webpackChunkName: "plone-blocks" */ './ListingBlockEdit'),
),
blockSchema: ListingSchema,
icon: ListIcon,
category: 'common',
} satisfies Partial<BlockConfigBase>;

Expand Down
27 changes: 27 additions & 0 deletions packages/blocks/Listing/schema.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { JSONSchema } from '@plone/types';

export function ListingSchema(): JSONSchema {
return {
title: 'Listing',
fieldsets: [
{
id: 'default',
title: 'Default',
fields: ['headline', 'querystring'],
},
],
properties: {
headline: {
title: 'Headline',
},

querystring: {
title: 'Query',
description:
'Enter a querystring to filter the content items to be listed. For example: "Type: News Item" or "path: /news".',
widget: 'querystring',
},
},
required: ['querystring'],
};
}
30 changes: 30 additions & 0 deletions packages/blocks/Listing/useQuerystringResults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useEffect } from 'react';
import { useFetcher } from 'react-router';
import { useDebounceValue } from 'usehooks-ts';
import type { QuerystringValue } from '../../cmsui/components/QuerystringWidget/QuerystringWidgetContext';
import type { QuerystringSearchResult } from '../../cmsui/routes/querystringSearch';

export function useQuerystringResults(
querystring: QuerystringValue | undefined,
) {
const fetcher = useFetcher<QuerystringSearchResult>();

const querySignature = JSON.stringify(querystring?.query ?? []);
const [debouncedQuerySignature] = useDebounceValue(querySignature, 400);

useEffect(() => {
const criteria = JSON.parse(debouncedQuerySignature);
if (!criteria || criteria.length === 0) return;

fetcher.load(
`/@querystringSearch?query=${encodeURIComponent(debouncedQuerySignature)}`,
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedQuerySignature]);

const items = fetcher.data?.items ?? [];
const total = fetcher.data?.items_total ?? 0;
const loading = fetcher.state !== 'idle';

return { items, total, loading };
}
6 changes: 6 additions & 0 deletions packages/blocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ export default function install(config: ConfigType) {
widths: ['default'],
},
},
listing: {
blockWidth: {
defaultWidth: 'default',
widths: ['layout', 'default', 'narrow'],
},
},
toc: {
blockWidth: {
defaultWidth: 'default',
Expand Down
1 change: 1 addition & 0 deletions packages/blocks/news/40.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Aurora: Listing block @nileshgulia
4 changes: 3 additions & 1 deletion packages/blocks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@
"@plone/components": "workspace:*",
"@plone/registry": "workspace:*",
"clsx": "^2.1.1",
"react-i18next": "catalog:"
"react-i18next": "catalog:",
"react-router": "catalog:",
"usehooks-ts": "^3.1.1"
},
"devDependencies": {
"@plone/helpers": "workspace:*",
Expand Down
208 changes: 208 additions & 0 deletions packages/cmsui/acceptance/tests/listing-block.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import { expect, test } from '../../../tooling/playwright/test';
import { login } from '../../../tooling/playwright/login';
import { createContent } from '../../../tooling/playwright/content';
import { waitForPlateEditorReady } from '../../../tooling/playwright/plate';
import type { Page } from '@playwright/test';

const PAGE_ID = 'listing-block-page';

async function setupListingBlockPage(page: Page) {
await createContent(page, {
contentType: 'Document',
contentId: 'news-folder',
contentTitle: 'News Folder',
});

await createContent(page, {
contentType: 'Document',
contentId: 'news-item-1',
contentTitle: 'First News Article',
contentDescription: 'Description of the first article',
path: '/news-folder',
});

await createContent(page, {
contentType: 'Document',
contentId: 'news-item-2',
contentTitle: 'Second News Article',
contentDescription: 'Description of the second article',
path: '/news-folder',
});

await createContent(page, {
contentType: 'Document',
contentId: PAGE_ID,
contentTitle: 'Listing Block Page',
transition: 'publish',
bodyModifier: (body) => ({
...body,
blocks: {
__somersault__: {
'@type': '__somersault__',
value: [
{
type: 'title',
children: [{ text: 'Listing Block Page' }],
},
{
type: 'unknown',
'@type': 'listing',
headline: 'Latest News',
querystring: {
query: [
{
i: 'path',
o: 'plone.app.querystring.operation.string.path',
v: '/news-folder',
},
],
},
children: [{ text: '' }],
},
],
},
},
blocks_layout: {
items: ['__somersault__'],
},
}),
});

await page.goto(`/@@edit/${PAGE_ID}`);
await waitForPlateEditorReady(page);
}

test.describe('Listing block', () => {
test('displays listing block with headline and items in edit mode', async ({
page,
}) => {
await login(page);
await setupListingBlockPage(page);

// Check headline is visible
await expect(
page.getByRole('heading', { name: 'Latest News' }),
).toBeVisible();

// Check that items are displayed
await expect(page.getByText('First News Article')).toBeVisible();
await expect(page.getByText('Second News Article')).toBeVisible();

// Check descriptions are visible
await expect(
page.getByText('Description of the first article'),
).toBeVisible();
await expect(
page.getByText('Description of the second article'),
).toBeVisible();
});

test('shows placeholder when no query is configured', async ({ page }) => {
await login(page);
await createContent(page, {
contentType: 'Document',
contentId: 'empty-listing-page',
contentTitle: 'Empty Listing Page',
transition: 'publish',
bodyModifier: (body) => ({
...body,
blocks: {
__somersault__: {
'@type': '__somersault__',
value: [
{
type: 'title',
children: [{ text: 'Empty Listing Page' }],
},
{
type: 'unknown',
'@type': 'listing',
headline: 'No Query',
children: [{ text: '' }],
},
],
},
},
blocks_layout: {
items: ['__somersault__'],
},
}),
});

await page.goto('/@@edit/empty-listing-page');
await waitForPlateEditorReady(page);

await expect(page.getByText('No Results Found')).toBeVisible();
});

test('displays listing block items in view', async ({ page }) => {
await login(page);
await setupListingBlockPage(page);

// Wait for items to be fetched by the listing block edit component
await expect(page.getByText('First News Article')).toBeVisible({
timeout: 10000,
});
await expect(page.getByText('Second News Article')).toBeVisible({
timeout: 10000,
});

// Save using the toolbar button
const saveButton = page
.locator('#toolbar')
.getByRole('button', { name: /save/i })
.first();
await saveButton.click();
await page.waitForLoadState('networkidle');

// Navigate to the published view
await page.goto(`/${PAGE_ID}`, { waitUntil: 'networkidle' });

// Check headline is visible
await expect(
page.getByRole('heading', { name: 'Latest News' }),
).toBeVisible();

// Check that items are displayed
await expect(page.getByText('First News Article')).toBeVisible();
await expect(page.getByText('Second News Article')).toBeVisible();

// Check descriptions are visible
await expect(
page.getByText('Description of the first article'),
).toBeVisible();
await expect(
page.getByText('Description of the second article'),
).toBeVisible();
});

test('items are clickable in view', async ({ page }) => {
await login(page);
await setupListingBlockPage(page);

// Wait for items to be fetched
await expect(page.getByText('First News Article')).toBeVisible({
timeout: 10000,
});

// Save using the toolbar button
const saveButton = page
.locator('#toolbar')
.getByRole('button', { name: /save/i })
.first();
await saveButton.click();
await page.waitForLoadState('networkidle');

// Navigate to the published view
await page.goto(`/${PAGE_ID}`, { waitUntil: 'networkidle' });

// Click on the first article link
await page.getByRole('link', { name: 'First News Article' }).click();

// Should navigate to the article
await expect(page).toHaveURL(/\/news-folder\/news-item-1$/);
await expect(
page.getByRole('heading', { name: 'First News Article' }),
).toBeVisible();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ interface QuerystringWidgetStoryProps {
value?: QuerystringValue;
defaultValue?: QuerystringValue;
onChange?: (value: QuerystringValue) => void;
onPatchFormData?: (partial: Record<string, unknown>) => void;
}

const createQuerystringLoader = () => {
Expand Down Expand Up @@ -199,7 +198,6 @@ const meta = {
label: 'Search Criteria',
description: 'Define search criteria to filter content',
onChange: fn(),
onPatchFormData: fn(),
},
} satisfies Meta<QuerystringWidgetStoryProps>;

Expand Down
Loading
Loading