Promote suggestion misses through the bulk-add modal, then delete them

Suggestion Misses' "Promote Selected" now opens the same bulk-add modal
shape as Suggestions' "Create Many" (editable phrases textarea +
lang/intent), prefilled with the selected misses' own text/language, and
submits via batchCreateSuggestions() so edited/added lines are honored.

Once that succeeds, the selected misses are deleted via the new
batchDeleteSuggestionMisses() (DELETE /admin/suggestion-misses/batch) so
they don't linger in the list now that they're suggestions. This is
best-effort: a failed cleanup doesn't undo the promotion, just surfaces a
separate warning notification.
This commit is contained in:
Nyan Lin Paing
2026-09-03 20:42:56 +07:00
parent 74578d10e3
commit 051b091ef8
4 changed files with 134 additions and 17 deletions
@@ -69,9 +69,25 @@ test('a failed dismiss surfaces the gateway detail message', function () {
->assertNotified('Failed to dismiss miss');
});
test('promote bulk action calls promoteSuggestionMisses with the selected ids and shows the result', function () {
test('promote bulk action prefills the bulk-add modal with the selected misses\' text and language', function () {
Http::fake(['bnfexpress.test/*' => Http::response([
['id' => 1, 'text_norm' => 'ev charging cost', 'lang' => 'my', 'syllables' => 3, 'was_used' => false, 'created_at' => now()->toIso8601String()],
['id' => 2, 'text_norm' => 'nearest charger', 'lang' => 'my', 'syllables' => 2, 'was_used' => false, 'created_at' => now()->toIso8601String()],
])]);
Livewire::test(ManageSuggestionMisses::class)
->loadTable()
->mountTableBulkAction('promote', [1, 2])
->assertTableBulkActionDataSet([
'items_raw' => "ev charging cost\nnearest charger",
'lang' => 'my',
]);
});
test('promote bulk action calls batchCreateSuggestions with the (possibly edited) phrases and shows the result', function () {
Http::fake(['bnfexpress.test/*' => fn (Request $request) => match (true) {
str_contains($request->url(), '/admin/suggestion-misses/promote') => Http::response(['created' => 2, 'skipped' => 0, 'trie_rebuilt' => true]),
str_contains($request->url(), '/admin/suggestions/batch') => Http::response(['created' => 2, 'skipped' => 0, 'trie_rebuilt' => true]),
str_contains($request->url(), '/admin/suggestion-misses/batch') => Http::response(['deleted' => 2, 'skipped' => 0]),
default => Http::response([
['id' => 1, 'text_norm' => 'a', 'lang' => 'en', 'syllables' => 1, 'was_used' => false, 'created_at' => now()->toIso8601String()],
['id' => 2, 'text_norm' => 'b', 'lang' => 'en', 'syllables' => 1, 'was_used' => false, 'created_at' => now()->toIso8601String()],
@@ -80,10 +96,54 @@ test('promote bulk action calls promoteSuggestionMisses with the selected ids an
Livewire::test(ManageSuggestionMisses::class)
->loadTable()
->callTableBulkAction('promote', [1, 2], data: ['lang' => 'my', 'intent' => null])
->callTableBulkAction('promote', [1, 2], data: ['items_raw' => "a\nb\nc", 'lang' => 'my', 'intent' => null])
->assertNotified('2 promoted, 0 skipped');
Http::assertSent(fn (Request $request) => str_contains($request->url(), '/admin/suggestion-misses/promote')
&& $request['miss_ids'] === [1, 2]
&& $request['lang'] === 'my');
Http::assertSent(fn (Request $request) => str_contains($request->url(), '/admin/suggestions/batch')
&& $request['items'] === [
['text' => 'a', 'lang' => 'my'],
['text' => 'b', 'lang' => 'my'],
['text' => 'c', 'lang' => 'my'],
]);
Http::assertSent(fn (Request $request) => $request->method() === 'DELETE'
&& str_contains($request->url(), '/admin/suggestion-misses/batch')
&& $request['miss_ids'] === [1, 2]);
});
test('promoting deletes the original misses after a successful promote', function () {
Http::fake(['bnfexpress.test/*' => fn (Request $request) => match (true) {
str_contains($request->url(), '/admin/suggestions/batch') => Http::response(['created' => 1, 'skipped' => 0, 'trie_rebuilt' => true]),
str_contains($request->url(), '/admin/suggestion-misses/batch') => Http::response(['deleted' => 1, 'skipped' => 0]),
default => Http::response([
['id' => 1, 'text_norm' => 'ev charging cost', 'lang' => 'en', 'syllables' => 3, 'was_used' => false, 'created_at' => now()->toIso8601String()],
]),
}]);
Livewire::test(ManageSuggestionMisses::class)
->loadTable()
->callTableBulkAction('promote', [1], data: ['items_raw' => 'ev charging cost', 'lang' => 'en', 'intent' => null])
->assertNotified('1 promoted, 0 skipped');
Http::assertSent(fn (Request $request) => $request->method() === 'DELETE'
&& str_contains($request->url(), '/admin/suggestion-misses/batch')
&& $request['miss_ids'] === [1]);
});
test('a failed cleanup delete after a successful promote surfaces a separate warning notification', function () {
Http::fake(['bnfexpress.test/*' => fn (Request $request) => match (true) {
str_contains($request->url(), '/admin/suggestions/batch') => Http::response(['created' => 1, 'skipped' => 0, 'trie_rebuilt' => true]),
str_contains($request->url(), '/admin/suggestion-misses/batch') => Http::response(['detail' => 'boom'], 500),
default => Http::response([
['id' => 1, 'text_norm' => 'ev charging cost', 'lang' => 'en', 'syllables' => 3, 'was_used' => false, 'created_at' => now()->toIso8601String()],
]),
}]);
// assertNotified() pulls (and clears) every queued notification on its
// first call, so only one chained call can see anything — check the
// warning here; the success title is covered by the other promote tests.
Livewire::test(ManageSuggestionMisses::class)
->loadTable()
->callTableBulkAction('promote', [1], data: ['items_raw' => 'ev charging cost', 'lang' => 'en', 'intent' => null])
->assertNotified('Promoted, but failed to remove the original misses');
});