feat: browse creator-filtered results without a query

Selecting a creator with an empty query now lists their pages (or
attachments in attachment mode) newest-first, with the same space and
page permission filtering as typed search.
This commit is contained in:
Philipinho
2026-08-25 00:11:04 +01:00
parent 01b36db348
commit 1dae88c899
4 changed files with 24 additions and 16 deletions
@@ -106,7 +106,8 @@ export function SearchSpotlight({ spaceId }: SearchSpotlightProps) {
} }
}, [aiSearchError, t]); }, [aiSearchError, t]);
const isLabelBrowse = (filters.labelIds?.length ?? 0) > 0; const isFilterBrowse =
(filters.labelIds?.length ?? 0) > 0 || !!filters.creatorId;
// while the debounce is pending the empty list is not a settled "no results" // while the debounce is pending the empty list is not a settled "no results"
const isQuerySettled = query === debouncedSearchQuery; const isQuerySettled = query === debouncedSearchQuery;
@@ -231,11 +232,11 @@ export function SearchSpotlight({ spaceId }: SearchSpotlightProps) {
</> </>
) : ( ) : (
<> <>
{query.length === 0 && !isLabelBrowse && resultItems.length === 0 && ( {query.length === 0 && !isFilterBrowse && resultItems.length === 0 && (
<Spotlight.Empty>{t("Start typing to search...")}</Spotlight.Empty> <Spotlight.Empty>{t("Start typing to search...")}</Spotlight.Empty>
)} )}
{(query.length > 0 || isLabelBrowse) && {(query.length > 0 || isFilterBrowse) &&
!isFetching && !isFetching &&
isQuerySettled && isQuerySettled &&
resultItems.length === 0 && ( resultItems.length === 0 && (
@@ -244,7 +245,7 @@ export function SearchSpotlight({ spaceId }: SearchSpotlightProps) {
{resultItems.length > 0 && <>{resultItems}</>} {resultItems.length > 0 && <>{resultItems}</>}
{(query.length > 0 || isLabelBrowse) && isFetching && ( {(query.length > 0 || isFilterBrowse) && isFetching && (
<Spotlight.Empty> <Spotlight.Empty>
<Text size="sm" style={{ marginTop: 10 }}> <Text size="sm" style={{ marginTop: 10 }}>
{t("Searching...")} {t("Searching...")}
@@ -39,11 +39,16 @@ export function useUnifiedSearch(
return await searchPage(backendParams); return await searchPage(backendParams);
} }
}, },
enabled: (!!params.query || (params.labelIds?.length ?? 0) > 0) && enabled, enabled:
(!!params.query ||
(params.labelIds?.length ?? 0) > 0 ||
!!params.creatorId) &&
enabled,
// keep previous results only within the same search type; page results // keep previous results only within the same search type; page results
// rendered as attachments (or vice versa) crash on missing fields // rendered as attachments (or vice versa) crash on missing fields
placeholderData: (previousData, previousQuery) => { placeholderData: (previousData, previousQuery) => {
if (!params.query && !params.labelIds?.length) return undefined; if (!params.query && !params.labelIds?.length && !params.creatorId)
return undefined;
if (previousQuery && previousQuery.queryKey[1] !== searchType) { if (previousQuery && previousQuery.queryKey[1] !== searchType) {
return undefined; return undefined;
} }
+11 -9
View File
@@ -31,17 +31,19 @@ export class SearchService {
): Promise<{ items: SearchResponseDto[] }> { ): Promise<{ items: SearchResponseDto[] }> {
const query = searchParams.query?.trim() ?? ''; const query = searchParams.query?.trim() ?? '';
const labelIds = [...new Set(searchParams.labelIds ?? [])]; const labelIds = [...new Set(searchParams.labelIds ?? [])];
// selected labels are browsable without a query // selected filters (labels, creator) are browsable without a query
const browseByLabels = query.length < 1 && labelIds.length > 0; const browseByFilters =
query.length < 1 &&
(labelIds.length > 0 || Boolean(searchParams.creatorId));
if (query.length < 1 && !browseByLabels) { if (query.length < 1 && !browseByFilters) {
return { items: [] }; return { items: [] };
} }
const searchQuery = tsquery(query + '*'); const searchQuery = tsquery(query + '*');
const titleOnly = searchParams.titleOnly === true; const titleOnly = searchParams.titleOnly === true;
const titleQuery = query; const titleQuery = query;
const rankColumn = browseByLabels const rankColumn = browseByFilters
? sql<number>`0`.as('rank') ? sql<number>`0`.as('rank')
: titleOnly : titleOnly
? sql<number>`word_similarity(lower(f_unaccent(${titleQuery})), lower(f_unaccent(pages.title)))`.as( ? sql<number>`word_similarity(lower(f_unaccent(${titleQuery})), lower(f_unaccent(pages.title)))`.as(
@@ -50,7 +52,7 @@ export class SearchService {
: sql<number>`ts_rank(tsv, to_tsquery('english', f_unaccent(${searchQuery})))`.as( : sql<number>`ts_rank(tsv, to_tsquery('english', f_unaccent(${searchQuery})))`.as(
'rank', 'rank',
); );
const highlightColumn = browseByLabels || titleOnly const highlightColumn = browseByFilters || titleOnly
? sql<string>`''`.as('highlight') ? sql<string>`''`.as('highlight')
: sql<string>`ts_headline('english', text_content, to_tsquery('english', f_unaccent(${searchQuery})),'MinWords=9, MaxWords=10, MaxFragments=3')`.as( : sql<string>`ts_headline('english', text_content, to_tsquery('english', f_unaccent(${searchQuery})),'MinWords=9, MaxWords=10, MaxFragments=3')`.as(
'highlight', 'highlight',
@@ -70,14 +72,14 @@ export class SearchService {
rankColumn, rankColumn,
highlightColumn, highlightColumn,
]) ])
.$if(!browseByLabels && !titleOnly, (qb) => .$if(!browseByFilters && !titleOnly, (qb) =>
qb.where( qb.where(
'tsv', 'tsv',
'@@', '@@',
sql<string>`to_tsquery('english', f_unaccent(${searchQuery}))`, sql<string>`to_tsquery('english', f_unaccent(${searchQuery}))`,
), ),
) )
.$if(!browseByLabels && titleOnly, (qb) => .$if(!browseByFilters && titleOnly, (qb) =>
qb.where((eb) => qb.where((eb) =>
eb( eb(
sql`lower(f_unaccent(pages.title))`, sql`lower(f_unaccent(pages.title))`,
@@ -100,8 +102,8 @@ export class SearchService {
), ),
) )
.where('deletedAt', 'is', null) .where('deletedAt', 'is', null)
.$if(browseByLabels, (qb) => qb.orderBy('updatedAt', 'desc')) .$if(browseByFilters, (qb) => qb.orderBy('updatedAt', 'desc'))
.$if(!browseByLabels, (qb) => qb.orderBy('rank', 'desc')) .$if(!browseByFilters, (qb) => qb.orderBy('rank', 'desc'))
.limit(searchParams.limit || 25) .limit(searchParams.limit || 25)
.offset(searchParams.offset || 0); .offset(searchParams.offset || 0);