From 1dae88c899a23bace7273ab4fb0f7b196ff83185 Mon Sep 17 00:00:00 2001
From: Philipinho <16838612+Philipinho@users.noreply.github.com>
Date: Tue, 25 Aug 2026 00:11:04 +0100
Subject: [PATCH] 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.
---
.../search/components/search-spotlight.tsx | 9 +++++----
.../search/hooks/use-unified-search.ts | 9 +++++++--
apps/server/src/core/search/search.service.ts | 20 ++++++++++---------
apps/server/src/ee | 2 +-
4 files changed, 24 insertions(+), 16 deletions(-)
diff --git a/apps/client/src/features/search/components/search-spotlight.tsx b/apps/client/src/features/search/components/search-spotlight.tsx
index 8b41fa870..77d1cc293 100644
--- a/apps/client/src/features/search/components/search-spotlight.tsx
+++ b/apps/client/src/features/search/components/search-spotlight.tsx
@@ -106,7 +106,8 @@ export function SearchSpotlight({ spaceId }: SearchSpotlightProps) {
}
}, [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"
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 && (
{t("Start typing to search...")}
)}
- {(query.length > 0 || isLabelBrowse) &&
+ {(query.length > 0 || isFilterBrowse) &&
!isFetching &&
isQuerySettled &&
resultItems.length === 0 && (
@@ -244,7 +245,7 @@ export function SearchSpotlight({ spaceId }: SearchSpotlightProps) {
{resultItems.length > 0 && <>{resultItems}>}
- {(query.length > 0 || isLabelBrowse) && isFetching && (
+ {(query.length > 0 || isFilterBrowse) && isFetching && (
{t("Searching...")}
diff --git a/apps/client/src/features/search/hooks/use-unified-search.ts b/apps/client/src/features/search/hooks/use-unified-search.ts
index 500b2fd60..9270477b9 100644
--- a/apps/client/src/features/search/hooks/use-unified-search.ts
+++ b/apps/client/src/features/search/hooks/use-unified-search.ts
@@ -39,11 +39,16 @@ export function useUnifiedSearch(
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
// rendered as attachments (or vice versa) crash on missing fields
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) {
return undefined;
}
diff --git a/apps/server/src/core/search/search.service.ts b/apps/server/src/core/search/search.service.ts
index 900f81729..ae0a2c13c 100644
--- a/apps/server/src/core/search/search.service.ts
+++ b/apps/server/src/core/search/search.service.ts
@@ -31,17 +31,19 @@ export class SearchService {
): Promise<{ items: SearchResponseDto[] }> {
const query = searchParams.query?.trim() ?? '';
const labelIds = [...new Set(searchParams.labelIds ?? [])];
- // selected labels are browsable without a query
- const browseByLabels = query.length < 1 && labelIds.length > 0;
+ // selected filters (labels, creator) are browsable without a query
+ const browseByFilters =
+ query.length < 1 &&
+ (labelIds.length > 0 || Boolean(searchParams.creatorId));
- if (query.length < 1 && !browseByLabels) {
+ if (query.length < 1 && !browseByFilters) {
return { items: [] };
}
const searchQuery = tsquery(query + '*');
const titleOnly = searchParams.titleOnly === true;
const titleQuery = query;
- const rankColumn = browseByLabels
+ const rankColumn = browseByFilters
? sql`0`.as('rank')
: titleOnly
? sql`word_similarity(lower(f_unaccent(${titleQuery})), lower(f_unaccent(pages.title)))`.as(
@@ -50,7 +52,7 @@ export class SearchService {
: sql`ts_rank(tsv, to_tsquery('english', f_unaccent(${searchQuery})))`.as(
'rank',
);
- const highlightColumn = browseByLabels || titleOnly
+ const highlightColumn = browseByFilters || titleOnly
? sql`''`.as('highlight')
: sql`ts_headline('english', text_content, to_tsquery('english', f_unaccent(${searchQuery})),'MinWords=9, MaxWords=10, MaxFragments=3')`.as(
'highlight',
@@ -70,14 +72,14 @@ export class SearchService {
rankColumn,
highlightColumn,
])
- .$if(!browseByLabels && !titleOnly, (qb) =>
+ .$if(!browseByFilters && !titleOnly, (qb) =>
qb.where(
'tsv',
'@@',
sql`to_tsquery('english', f_unaccent(${searchQuery}))`,
),
)
- .$if(!browseByLabels && titleOnly, (qb) =>
+ .$if(!browseByFilters && titleOnly, (qb) =>
qb.where((eb) =>
eb(
sql`lower(f_unaccent(pages.title))`,
@@ -100,8 +102,8 @@ export class SearchService {
),
)
.where('deletedAt', 'is', null)
- .$if(browseByLabels, (qb) => qb.orderBy('updatedAt', 'desc'))
- .$if(!browseByLabels, (qb) => qb.orderBy('rank', 'desc'))
+ .$if(browseByFilters, (qb) => qb.orderBy('updatedAt', 'desc'))
+ .$if(!browseByFilters, (qb) => qb.orderBy('rank', 'desc'))
.limit(searchParams.limit || 25)
.offset(searchParams.offset || 0);
diff --git a/apps/server/src/ee b/apps/server/src/ee
index add0054e9..0be4a7617 160000
--- a/apps/server/src/ee
+++ b/apps/server/src/ee
@@ -1 +1 @@
-Subproject commit add0054e9640c089aaccdad9251c240b76e7053b
+Subproject commit 0be4a761754f673f7453f0a4e295168d181824c3