Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 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
ed7ee3b
Merge branch 'main' into seven-listing-block
nileshgulia1 Jun 8, 2026
555f07d
refactor: add tests and clean up querystringWidget
nileshgulia1 Jun 8, 2026
e110112
fix: remove duplicate export
nileshgulia1 Jun 9, 2026
a1e408e
Merge branch 'main' into seven-listing-block
sneridagh Jun 14, 2026
8e35325
Fix defaultBlockWidth for ploneBlock listing.
sneridagh Jun 14, 2026
17a7a22
fix: add missing Select widget for valueType select
nileshgulia1 Jun 16, 2026
a7c2c52
refactor: make listing blockData more generic
nileshgulia1 Jun 16, 2026
0221d02
fix: simplify query criteria and move search logic to ListingView
nileshgulia1 Jun 17, 2026
61d1aea
fix sort_on and limit type
nileshgulia1 Jun 17, 2026
6cb547d
fix playright tests
nileshgulia1 Jun 17, 2026
64b103b
chore: update changelog
nileshgulia1 Jun 17, 2026
9e4b953
fix storybook for QuerystringWidget
nileshgulia1 Jun 18, 2026
f9252b3
Merge branch 'main' into seven-listing-block
nileshgulia1 Jul 10, 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
6 changes: 5 additions & 1 deletion apps/aurora/app/config/server.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/
import config from '@plone/registry';
import PloneClient from '@plone/client';
import { flattenToAppURL } from '@plone/helpers';
// eslint-disable-next-line import/no-unresolved
import applyAddonConfiguration from '../../.plone/registry.loader';
// eslint-disable-next-line import/no-unresolved
Expand Down Expand Up @@ -34,7 +35,10 @@ export default function install() {
const { id, block } = listingBlocks[i];
if (block.querystring) {
const results = await args.cli.querystringSearch(block.querystring);
args.content.blocks[id].items = results.data.items;
const flattened = flattenToAppURL(
results?.data ?? { items: [], items_total: 0 },
);
args.content.blocks[id].items = flattened.items;
}
}
},
Expand Down
1 change: 1 addition & 0 deletions apps/aurora/news/+listingblockData-flattenToAppUrl.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add flattenToAppUrl on listing data @nileshgulia1
36 changes: 36 additions & 0 deletions packages/blocks/Listing/ListingBlockEdit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { BlockEditProps } from '@plone/types';

import ListingBlockView from './ListingBlockView';

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 } = props;
const hasListingQuery = hasQuery(data.querystring as any);

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;
12 changes: 10 additions & 2 deletions packages/blocks/Listing/ListingBlockView.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { BlockViewProps, Brain, ListingBlockFormData } from '@plone/types';
import { useTranslation } from 'react-i18next';
import { useQuerystringResults } from './useQuerystringResults';

const hasQuery = (value: ListingBlockFormData['querystring']): boolean =>
Array.isArray(value?.query) && value.query.length > 0;

/**
* View listing block component.
Expand All @@ -11,6 +15,10 @@ const ListingBlockView = (props: BlockViewProps) => {
const HeadlineTag = data.headlineTag || 'h2';
const ItemTitleTag = data.headlineTag === 'h2' ? 'h3' : 'h4';
const { t } = useTranslation();
const hasListingQuery = hasQuery(data.querystring);
const { items, loaded } = useQuerystringResults(data.querystring as any);
const initialItems = props.isEditMode ? [] : (data.items ?? []);
const listingItems = hasListingQuery ? (loaded ? items : initialItems) : [];

const getPreviewImageUrl = (item: Brain) => {
const imageField = item.image_field;
Expand Down Expand Up @@ -51,10 +59,10 @@ const ListingBlockView = (props: BlockViewProps) => {
return (
<>
{data.headline ? <HeadlineTag>{data.headline}</HeadlineTag> : ''}
{!data.items || data.items?.length === 0 ? (
{listingItems.length === 0 ? (
<div>{t('blocks.listing.no-results')}</div>
) : (
data.items.map((item) =>
listingItems.map((item) =>
data.variation === 'summary'
? renderSummary(item)
: renderDefault(item),
Expand Down
8 changes: 8 additions & 0 deletions packages/blocks/Listing/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
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',
defaultBlockWidth: 'default',
} satisfies Partial<BlockConfigBase>;

export default ListingBlockInfo;
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'],
};
}
110 changes: 110 additions & 0 deletions packages/blocks/Listing/useQuerystringResults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { useEffect, useMemo, useRef, useState } 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';

/**
* Convert sort_order to string format.
* Handles both boolean (true = descending, false = ascending) and string values.
*/
function normalizeSortOrder(
sortOrder: string | boolean | undefined,
): 'ascending' | 'descending' | undefined {
if (sortOrder === undefined || sortOrder === null) return undefined;
if (typeof sortOrder === 'boolean') {
return sortOrder ? 'descending' : 'ascending';
}
if (sortOrder === 'ascending' || sortOrder === 'descending') {
return sortOrder;
}
}

/**
* Build query parameters object with all supported fields.
* Only includes fields that have values (following Volto's pattern).
*/
function buildQueryParams(querystring: QuerystringValue | undefined) {
if (!querystring) return null;

const params: {
query?: QuerystringValue['query'];
sort_on?: string;
sort_order?: string;
b_size?: string;
limit?: string;
} = {};

if (querystring.query && querystring.query.length > 0) {
params.query = querystring.query;
}

if (querystring.sort_on) {
params.sort_on = querystring.sort_on;
}

const normalizedSortOrder = normalizeSortOrder(querystring.sort_order);
if (normalizedSortOrder) {
params.sort_order = normalizedSortOrder;
}

if (querystring.b_size !== undefined && querystring.b_size !== null) {
params.b_size = String(querystring.b_size);
}

if (querystring.limit !== undefined && querystring.limit !== null) {
params.limit = String(querystring.limit);
}

return Object.keys(params).length > 0 ? params : null;
}

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

const criteria = querystring?.query ?? [];
const hasCriteria = criteria.length > 0;

// Build full params signature including all supported fields
const params = buildQueryParams(querystring);
const paramsSignature = JSON.stringify(params);
const [debouncedParamsSignature] = useDebounceValue(paramsSignature, 400);
const pendingParamsSignature = useRef<string | undefined>(undefined);
const [loadedParamsSignature, setLoadedParamsSignature] = useState<
string | undefined
>(undefined);

const queryUrl = useMemo(() => {
const currentParams = JSON.parse(debouncedParamsSignature);
if (!currentParams?.query?.length) return null;

// Encode the entire params object as a single query parameter
const queryString = JSON.stringify(currentParams);
const encodedQuery = encodeURIComponent(queryString);

return `/@querystringSearch?query=${encodedQuery}`;
}, [debouncedParamsSignature]);

useEffect(() => {
if (!queryUrl) return;

pendingParamsSignature.current = debouncedParamsSignature;
fetcher.load(queryUrl);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [queryUrl]);

useEffect(() => {
if (!fetcher.data || !pendingParamsSignature.current) return;
setLoadedParamsSignature(pendingParamsSignature.current);
}, [fetcher.data]);

const loaded =
!hasCriteria || loadedParamsSignature === debouncedParamsSignature;
const items = hasCriteria && loaded ? (fetcher.data?.items ?? []) : [];
const total = hasCriteria && loaded ? (fetcher.data?.items_total ?? 0) : 0;
const loading = hasCriteria && fetcher.state !== 'idle';

return { items, total, loading, loaded };
}
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
1 change: 1 addition & 0 deletions packages/client/news/+get-querystring.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
add queryParams to request body @nileshgulia1
30 changes: 24 additions & 6 deletions packages/client/src/restapi/querystring-search/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,45 @@ export type QuerystringSearchArgs = z.infer<typeof querystringSearchDataSchema>;

export async function querystringSearch(
this: PloneClient,
{ query, post }: QuerystringSearchArgs,
args: QuerystringSearchArgs,
): Promise<RequestResponse<QuerystringSearchResponse>> {
const validatedArgs = querystringSearchDataSchema.parse({
const {
query,
});
post,
sort_on,
sort_order,
b_size,
limit,
b_start,
fullobjects,
} = querystringSearchDataSchema.parse(args);

// Build the complete query object with all parameters
const queryObject = {
query,
...(sort_on && { sort_on }),
...(sort_order && { sort_order }),
...(b_size && { b_size }),
...(limit && { limit }),
...(b_start && { b_start }),
...(fullobjects !== undefined && { fullobjects }),
};

if (post) {
const options: ApiRequestParams = {
data: { query: validatedArgs.query },
data: queryObject,
config: this.config,
};

return apiRequest('post', '/@querystring-search', options);
} else {
const queryObject = { query: validatedArgs.query };
const querystring = JSON.stringify(queryObject);
const encodedQuery = encodeURIComponent(querystring);

const options: ApiRequestParams = {
config: this.config,
params: {
...(encodedQuery && { query: encodedQuery }),
query: encodedQuery,
},
};

Expand Down
Loading
Loading