feat: honor OCR text and tag scope in search Chapters/Mangas views
The Chapters/Mangas aggregation views accept an OCR ?text= filter and the page search intersects tags, but the client dropped both: any text query collapsed every view into a flat page search that discarded the selected tag and grouping. Add text to AggregateOptions, thread it into listTaggedChapters/listTaggedMangas, and rework the search loader so a tag-scoped view runs the aggregation (optionally text-filtered) and the page search passes the selected tag. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mangalord-frontend",
|
||||
"version": "0.126.0",
|
||||
"version": "0.127.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -165,6 +165,16 @@ describe('page_tags api client', () => {
|
||||
expect(url).toMatch(/\/v1\/me\/page-tags\/mangas\?tag=funny$/);
|
||||
});
|
||||
|
||||
it('aggregation calls forward the OCR text filter when provided', async () => {
|
||||
fetchSpy.mockResolvedValueOnce(
|
||||
ok({ items: [], page: { limit: 50, offset: 0, total: 0 } })
|
||||
);
|
||||
await listTaggedChapters({ tag: 'funny', text: 'hello world' });
|
||||
const url = fetchSpy.mock.calls[0][0] as string;
|
||||
expect(url).toContain('tag=funny');
|
||||
expect(url).toContain('text=hello+world');
|
||||
});
|
||||
|
||||
it('searchPages serializes tags (CSV), text and content-warning filters', async () => {
|
||||
fetchSpy.mockResolvedValueOnce(
|
||||
ok({ items: [], page: { limit: 50, offset: 0, total: 0 } })
|
||||
|
||||
@@ -179,6 +179,9 @@ export type AggregateOptions = {
|
||||
order?: 'desc' | 'asc';
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
/** OCR full-text filter: when set, only pages whose analysis search_doc
|
||||
* matches are aggregated (and `match_count` reflects that). */
|
||||
text?: string;
|
||||
};
|
||||
|
||||
function aggregateQs(opts: AggregateOptions): string {
|
||||
@@ -187,6 +190,7 @@ function aggregateQs(opts: AggregateOptions): string {
|
||||
if (opts.order != null) params.set('order', opts.order);
|
||||
if (opts.limit != null) params.set('limit', String(opts.limit));
|
||||
if (opts.offset != null) params.set('offset', String(opts.offset));
|
||||
if (opts.text != null && opts.text !== '') params.set('text', opts.text);
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
|
||||
@@ -103,26 +103,44 @@ export const load: PageLoad = async ({ url }) => {
|
||||
// every tag / view / sort / text change instead of freezing the old list.
|
||||
const results = (async () => {
|
||||
try {
|
||||
// Content search takes precedence over tag browsing.
|
||||
// Chapters / Mangas are tag-scoped aggregation views. When a tag is
|
||||
// selected, run the aggregation — optionally filtered by the OCR
|
||||
// `text` — so the selected view AND tag are honoured. Previously any
|
||||
// `text` collapsed every view into a flat page search, discarding
|
||||
// both the tag and the chapters/mangas grouping.
|
||||
if (tag && view === 'chapters') {
|
||||
const r = await listTaggedChapters({
|
||||
tag,
|
||||
order,
|
||||
limit: 100,
|
||||
text: text || undefined
|
||||
});
|
||||
return { ...emptyResults, chapters: r.items, total: r.page.total ?? 0 };
|
||||
}
|
||||
if (tag && view === 'mangas') {
|
||||
const r = await listTaggedMangas({
|
||||
tag,
|
||||
order,
|
||||
limit: 100,
|
||||
text: text || undefined
|
||||
});
|
||||
return { ...emptyResults, mangas: r.items, total: r.page.total ?? 0 };
|
||||
}
|
||||
// Pages view (or a content search with no tag): a positive filter
|
||||
// runs the page search — intersecting the selected tag with the OCR
|
||||
// text / content-warning filters instead of ignoring the tag.
|
||||
if (contentSearch) {
|
||||
const r = await searchPages({
|
||||
text: text || undefined,
|
||||
tags: tag ? [tag] : undefined,
|
||||
cwInclude,
|
||||
cwExclude,
|
||||
limit: 100
|
||||
});
|
||||
return { ...emptyResults, results: r.items, total: r.page.total ?? 0 };
|
||||
}
|
||||
// No tag selected → just the chip cloud, no results.
|
||||
// No tag selected and no content filter → just the chip cloud.
|
||||
if (!tag) return emptyResults;
|
||||
if (view === 'chapters') {
|
||||
const r = await listTaggedChapters({ tag, order, limit: 100 });
|
||||
return { ...emptyResults, chapters: r.items, total: r.page.total ?? 0 };
|
||||
}
|
||||
if (view === 'mangas') {
|
||||
const r = await listTaggedMangas({ tag, order, limit: 100 });
|
||||
return { ...emptyResults, mangas: r.items, total: r.page.total ?? 0 };
|
||||
}
|
||||
const r = await listMyPageTags({ tag, limit: 100 });
|
||||
return { ...emptyResults, pages: r.items, total: r.page.total ?? 0 };
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user