Search the library in the import section

The search shows the matches from a story site that Bindery does not track yet. The button that lists every such book stays.
This commit is contained in:
tim
2026-10-04 02:04:16 +02:00
parent d28f69d93a
commit b73b2808ca
6 changed files with 117 additions and 15 deletions
+46 -2
View File
@@ -233,11 +233,11 @@ test('dropping a file on a closed epub import card opens it and fills the file f
test('library import sends only selected books and handles an empty candidate list', async () => {
await mountStories();
api.on('GET', 'library/candidates', []);
await click(main, 'Find stories in the library');
await click(main, 'Show all stories in the library');
assert.match(main.textContent, /tracked already/);
const books = [{ id: 8, uuid: 'a', title: 'A', authors: ['W'] }, { id: 9, uuid: 'b', title: 'B', authors: [] }];
api.on('GET', 'library/candidates', books);
await click(main, 'Find stories in the library');
await click(main, 'Show all stories in the library');
change(field(main, 'Select all'), false);
await click(main, 'Import selected');
assert.equal(writes().length, 0);
@@ -252,6 +252,50 @@ test('library import sends only selected books and handles an empty candidate li
assert.deepEqual(api.sent('POST', 'library/import')[0].body, { books: [{ id: 8, uuid: 'a', title: 'A' }], check: false });
});
test('library import search trims the query, selects no book at first and imports the ticked books', async () => {
await mountStories();
const card = section(main, 'Import from the library');
const form = nodes(card).find((el) => el.tagName === 'form');
assert.equal(form.className, 'library-search');
assert.equal(find(form, 'button', 'Search').attributes.type, 'submit');
emit(form, 'submit');
field(card, 'Search the library').value = ' ';
emit(form, 'submit');
await settle();
assert.equal(api.requests.some((r) => r.path.startsWith('library/candidates')), false);
field(card, 'Search the library').value = ' A & B ';
api.on('GET', 'library/candidates?q=A%20%26%20B', []);
emit(form, 'submit');
await settle();
assert.match(card.textContent, /No library books from a story site match/);
assert.equal(find(card, 'button', 'Import selected'), undefined);
const books = [{ id: 8, uuid: 'a', title: 'A', authors: ['W'] }, { id: 9, uuid: 'b', title: 'B', authors: [] }];
api.on('GET', 'library/candidates?q=A%20%26%20B', books);
emit(form, 'submit');
await settle();
assert.deepEqual(['Select all', 'A', 'B'].map((label) => field(card, label).checked), [false, false, false]);
await click(card, 'Import selected');
assert.equal(writes().length, 0);
change(field(card, 'B'), true);
api.on('POST', 'library/import', [{ id: 'job' }]);
await click(card, 'Import selected');
assert.deepEqual(api.sent('POST', 'library/import')[0].body, { books: [{ id: 9, uuid: 'b', title: 'B' }], check: true });
// The full list still selects each book.
api.on('GET', 'library/candidates', books);
await click(card, 'Show all stories in the library');
assert.deepEqual(['Select all', 'A', 'B'].map((label) => field(card, label).checked), [true, true, true]);
});
test('a failed library import search shows the error', async () => {
await mountStories();
const card = section(main, 'Import from the library');
field(card, 'Search the library').value = 'A';
api.on('GET', 'library/candidates?q=A', { error: 'CWA is offline' }, 502);
emit(nodes(card).find((el) => el.tagName === 'form'), 'submit');
await settle();
assert.equal(find(card, 'button', 'Import selected'), undefined);
});
test('rules edits, moves, removal and paragraph choices are saved in order', async () => {
api.on('GET', 'stories/1/rules', { ...emptyRules, rules: [rule, { ...rule, pattern: 'second' }] });
const editor = rulesEditor('stories/1/rules');
+13
View File
@@ -92,11 +92,24 @@ def test_library_search_link_import_and_unlink(client, story, monkeypatch):
monkeypatch.setattr(o, "cwa", lambda: cwa)
monkeypatch.setattr(config, "CWA_URL", "https://cwa.example")
assert client.get("/api/library/candidates").json[0]["id"] == 7
cwa.search.assert_not_called()
# A search for candidates shows only the story-site books that have an ePub.
cwa.search.return_value = [
book,
Book(8, "book-8", "Title in PDF", ["Writer"], "www.royalroad.com", {"pdf": "/book.pdf"}),
Book(9, "book-9", "Title of a novel", ["Writer"], "A Publisher", {"epub": "/novel.epub"}),
]
assert [b["id"] for b in client.get("/api/library/candidates?q=Title").json] == [7]
cwa.search.assert_called_once_with("Title")
assert cwa.site_books.call_count == 1
cwa.search.reset_mock()
cwa.search.return_value = [book]
assert client.get("/api/library/search?q=Title").json[0]["tracked_as"] is None
cwa.search.assert_called_once_with("Title")
linked = client.post(f"/api/stories/{sid}/link", json={"uuid": "book-7"}, headers=H)
assert linked.json["calibre_id"] == 7
assert client.get("/api/library/candidates").json == []
assert client.get("/api/library/candidates?q=Title").json == []
assert client.get("/api/library/search?q=Title").json[0]["tracked_as"] == "Story"
cwa.get_book.side_effect = CwaError("offline")
assert client.get(f"/api/stories/{sid}").json["library_error"] == "offline"
+10 -1
View File
@@ -151,9 +151,18 @@ def test_library_candidates_and_search(fake):
"formats": ["epub"],
}
]
# A search shows only the matches from a story site.
fake.books.append(FakeBook(2, "uuid-2", "Test Novel", publisher="A Publisher", epub=b"x"))
fake.books.append(FakeBook(4, "uuid-4", "Other Story", publisher="www.royalroad.com", epub=b"x"))
assert [b["id"] for b in orchestrator.search_library("test")] == [1, 2]
assert orchestrator.library_candidates("test") == candidates
assert [b["id"] for b in orchestrator.library_candidates("story")] == [1, 4]
assert orchestrator.library_candidates("nothing") == []
assert orchestrator.library_candidates(" ") == []
story_id = add_story()
orchestrator.link_story(story_id, "uuid-1")
assert orchestrator.library_candidates() == []
assert [b["id"] for b in orchestrator.library_candidates()] == [4]
assert orchestrator.library_candidates("test") == []
assert orchestrator.search_library("test")[0]["tracked_as"] == "Test Story"
with pytest.raises(KeyError):
orchestrator.link_story(story_id, "uuid-nope")