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",
|
"name": "mangalord-frontend",
|
||||||
"version": "0.126.0",
|
"version": "0.127.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -165,6 +165,16 @@ describe('page_tags api client', () => {
|
|||||||
expect(url).toMatch(/\/v1\/me\/page-tags\/mangas\?tag=funny$/);
|
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 () => {
|
it('searchPages serializes tags (CSV), text and content-warning filters', async () => {
|
||||||
fetchSpy.mockResolvedValueOnce(
|
fetchSpy.mockResolvedValueOnce(
|
||||||
ok({ items: [], page: { limit: 50, offset: 0, total: 0 } })
|
ok({ items: [], page: { limit: 50, offset: 0, total: 0 } })
|
||||||
|
|||||||
@@ -179,6 +179,9 @@ export type AggregateOptions = {
|
|||||||
order?: 'desc' | 'asc';
|
order?: 'desc' | 'asc';
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: 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 {
|
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.order != null) params.set('order', opts.order);
|
||||||
if (opts.limit != null) params.set('limit', String(opts.limit));
|
if (opts.limit != null) params.set('limit', String(opts.limit));
|
||||||
if (opts.offset != null) params.set('offset', String(opts.offset));
|
if (opts.offset != null) params.set('offset', String(opts.offset));
|
||||||
|
if (opts.text != null && opts.text !== '') params.set('text', opts.text);
|
||||||
return params.toString();
|
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.
|
// every tag / view / sort / text change instead of freezing the old list.
|
||||||
const results = (async () => {
|
const results = (async () => {
|
||||||
try {
|
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) {
|
if (contentSearch) {
|
||||||
const r = await searchPages({
|
const r = await searchPages({
|
||||||
text: text || undefined,
|
text: text || undefined,
|
||||||
|
tags: tag ? [tag] : undefined,
|
||||||
cwInclude,
|
cwInclude,
|
||||||
cwExclude,
|
cwExclude,
|
||||||
limit: 100
|
limit: 100
|
||||||
});
|
});
|
||||||
return { ...emptyResults, results: r.items, total: r.page.total ?? 0 };
|
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 (!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 });
|
const r = await listMyPageTags({ tag, limit: 100 });
|
||||||
return { ...emptyResults, pages: r.items, total: r.page.total ?? 0 };
|
return { ...emptyResults, pages: r.items, total: r.page.total ?? 0 };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user