mirror of
https://github.com/docmost/docmost.git
synced 2026-08-26 00:07:04 +08:00
feat: browse label-filtered pages without a query
Selecting labels now lists their pages newest-first before any text is typed, on both the Postgres and Typesense drivers.
This commit is contained in:
@@ -106,6 +106,8 @@ export function SearchSpotlight({ spaceId }: SearchSpotlightProps) {
|
|||||||
}
|
}
|
||||||
}, [aiSearchError, t]);
|
}, [aiSearchError, t]);
|
||||||
|
|
||||||
|
const isLabelBrowse = (filters.labelIds?.length ?? 0) > 0;
|
||||||
|
|
||||||
// Determine result type for rendering
|
// Determine result type for rendering
|
||||||
const isAttachmentSearch =
|
const isAttachmentSearch =
|
||||||
filters.contentType === "attachment" && hasAttachmentIndexing;
|
filters.contentType === "attachment" && hasAttachmentIndexing;
|
||||||
@@ -227,17 +229,19 @@ export function SearchSpotlight({ spaceId }: SearchSpotlightProps) {
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{query.length === 0 && resultItems.length === 0 && (
|
{query.length === 0 && !isLabelBrowse && resultItems.length === 0 && (
|
||||||
<Spotlight.Empty>{t("Start typing to search...")}</Spotlight.Empty>
|
<Spotlight.Empty>{t("Start typing to search...")}</Spotlight.Empty>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{query.length > 0 && !isFetching && resultItems.length === 0 && (
|
{(query.length > 0 || isLabelBrowse) &&
|
||||||
|
!isFetching &&
|
||||||
|
resultItems.length === 0 && (
|
||||||
<Spotlight.Empty>{t("No results found...")}</Spotlight.Empty>
|
<Spotlight.Empty>{t("No results found...")}</Spotlight.Empty>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{resultItems.length > 0 && <>{resultItems}</>}
|
{resultItems.length > 0 && <>{resultItems}</>}
|
||||||
|
|
||||||
{query.length > 0 && isFetching && (
|
{(query.length > 0 || isLabelBrowse) && isFetching && (
|
||||||
<Spotlight.Empty>
|
<Spotlight.Empty>
|
||||||
<Text size="sm" style={{ marginTop: 10 }}>
|
<Text size="sm" style={{ marginTop: 10 }}>
|
||||||
{t("Searching...")}
|
{t("Searching...")}
|
||||||
|
|||||||
@@ -39,11 +39,11 @@ export function useUnifiedSearch(
|
|||||||
return await searchPage(backendParams);
|
return await searchPage(backendParams);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
enabled: !!params.query && enabled,
|
enabled: (!!params.query || (params.labelIds?.length ?? 0) > 0) && 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.length < 1) return undefined;
|
if (!params.query && !params.labelIds?.length) return undefined;
|
||||||
if (previousQuery && previousQuery.queryKey[1] !== searchType) {
|
if (previousQuery && previousQuery.queryKey[1] !== searchType) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import {
|
|||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
|
|
||||||
export class SearchDTO {
|
export class SearchDTO {
|
||||||
@IsNotEmpty()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
query: string;
|
query?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsUUID()
|
@IsUUID()
|
||||||
|
|||||||
@@ -29,24 +29,28 @@ export class SearchService {
|
|||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
},
|
},
|
||||||
): Promise<{ items: SearchResponseDto[] }> {
|
): Promise<{ items: SearchResponseDto[] }> {
|
||||||
const { query } = searchParams;
|
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;
|
||||||
|
|
||||||
if (query.length < 1) {
|
if (query.length < 1 && !browseByLabels) {
|
||||||
return { items: [] };
|
return { items: [] };
|
||||||
}
|
}
|
||||||
const searchQuery = tsquery(query.trim() + '*');
|
const searchQuery = tsquery(query + '*');
|
||||||
const labelIds = [...new Set(searchParams.labelIds ?? [])];
|
|
||||||
const titleOnly = searchParams.titleOnly === true;
|
const titleOnly = searchParams.titleOnly === true;
|
||||||
const titleQuery = query.trim();
|
const titleQuery = query;
|
||||||
|
|
||||||
const rankColumn = titleOnly
|
const rankColumn = browseByLabels
|
||||||
|
? sql<number>`0`.as('rank')
|
||||||
|
: 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(
|
||||||
'rank',
|
'rank',
|
||||||
)
|
)
|
||||||
: 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 = titleOnly
|
const highlightColumn = browseByLabels || 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',
|
||||||
@@ -66,14 +70,14 @@ export class SearchService {
|
|||||||
rankColumn,
|
rankColumn,
|
||||||
highlightColumn,
|
highlightColumn,
|
||||||
])
|
])
|
||||||
.$if(!titleOnly, (qb) =>
|
.$if(!browseByLabels && !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(titleOnly, (qb) =>
|
.$if(!browseByLabels && titleOnly, (qb) =>
|
||||||
qb.where((eb) =>
|
qb.where((eb) =>
|
||||||
eb(
|
eb(
|
||||||
sql`lower(f_unaccent(pages.title))`,
|
sql`lower(f_unaccent(pages.title))`,
|
||||||
@@ -102,7 +106,8 @@ export class SearchService {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.where('deletedAt', 'is', null)
|
.where('deletedAt', 'is', null)
|
||||||
.orderBy('rank', 'desc')
|
.$if(browseByLabels, (qb) => qb.orderBy('updatedAt', 'desc'))
|
||||||
|
.$if(!browseByLabels, (qb) => qb.orderBy('rank', 'desc'))
|
||||||
.limit(searchParams.limit || 25)
|
.limit(searchParams.limit || 25)
|
||||||
.offset(searchParams.offset || 0);
|
.offset(searchParams.offset || 0);
|
||||||
|
|
||||||
|
|||||||
+1
-1
Submodule apps/server/src/ee updated: 3c198edfa0...70976d9632
Reference in New Issue
Block a user