-
Notifications
You must be signed in to change notification settings - Fork 0
✨ feat(dialog): add search dialog functionality with input handling #412
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| package dev.slne.surf.api.minestom.dialog.search | ||
|
|
||
| import dev.slne.surf.api.core.messages.builder.SurfComponentBuilder | ||
| import dev.slne.surf.api.minestom.dialog.base | ||
| import dev.slne.surf.api.minestom.dialog.builder.DialogBodyBuilder | ||
| import dev.slne.surf.api.minestom.dialog.builder.DialogInputBuilder | ||
| import dev.slne.surf.api.minestom.dialog.dialog | ||
| import dev.slne.surf.api.minestom.dialog.type | ||
| import net.minestom.server.dialog.Dialog | ||
| import net.minestom.server.dialog.DialogAfterAction | ||
| import net.minestom.server.entity.Player | ||
|
|
||
| /** | ||
| * The label and tooltip of one of a search dialog's buttons. | ||
| */ | ||
| data class SearchDialogAction( | ||
| var label: SurfComponentBuilder.() -> Unit, | ||
| var tooltip: SurfComponentBuilder.() -> Unit | ||
| ) | ||
|
|
||
| /** | ||
| * The text field a search dialog collects its query in. | ||
| * | ||
| * @property key the key the query is reported back under | ||
| * @property label the label shown next to the field | ||
| * @property initialValue the query the field starts out with | ||
| * @property inputModifier applied to the field last, for anything the other properties do not cover | ||
| */ | ||
| data class SearchInput( | ||
| var key: String = "search", | ||
| var label: SurfComponentBuilder.() -> Unit = { | ||
| text("Suche") | ||
| }, | ||
| var initialValue: String = "", | ||
| var inputModifier: DialogInputBuilder.TextInputBuilder.() -> Unit = {} | ||
| ) | ||
|
|
||
| /** | ||
| * Builds a dialog that asks for a single query and hands it to [onSearch] or [onClose]. | ||
| * | ||
| * The dialog closes itself before either callback runs, so a callback is free to open an inventory | ||
| * or another dialog straight away. | ||
| * | ||
| * ``` | ||
| * player.showDialog( | ||
| * searchDialog( | ||
| * title = { primary("Search a player...") }, | ||
| * onSearch = { player, query -> showResults(player, query) }, | ||
| * onClose = { player, _ -> showOverview(player) }, | ||
| * searchInput = { initialValue = lastQuery }, | ||
| * ) | ||
| * ) | ||
| * ``` | ||
| * | ||
| * @param title the title shown above the dialog | ||
| * @param externalTitle the title shown wherever the dialog is listed, defaulting to [title] | ||
| * @param onSearch called with the query once the search button is pressed | ||
| * @param onClose called with the query once the cancel button is pressed | ||
| * @param body anything shown above the search field | ||
| * @param searchButton the button that runs the search | ||
| * @param cancelButton the button that abandons the search | ||
| * @param searchInput the text field the query is collected in | ||
| * @param canCloseWithEscape whether the dialog can be dismissed without pressing a button | ||
| * @param afterAction what the client does with the dialog once a button was pressed | ||
| */ | ||
| fun searchDialog( | ||
| title: SurfComponentBuilder.() -> Unit, | ||
| externalTitle: SurfComponentBuilder.() -> Unit = title, | ||
| onSearch: (player: Player, query: String) -> Unit, | ||
| onClose: (player: Player, query: String) -> Unit, | ||
| body: DialogBodyBuilder.() -> Unit = { }, | ||
| searchButton: SearchDialogAction.() -> Unit = { | ||
| label = { | ||
| text("Suchen") | ||
| } | ||
| tooltip = { | ||
| text("Klicke hier, um die Suche zu starten.") | ||
| } | ||
| }, | ||
| cancelButton: SearchDialogAction.() -> Unit = { | ||
| label = { | ||
| text("Abbrechen") | ||
| } | ||
| tooltip = { | ||
| text("Klicke hier, um die Suche abzubrechen.") | ||
| } | ||
| }, | ||
| searchInput: SearchInput.() -> Unit, | ||
| canCloseWithEscape: Boolean = false, | ||
| afterAction: DialogAfterAction = DialogAfterAction.NONE | ||
| ): Dialog = dialog { | ||
| val input = SearchInput().apply(searchInput) | ||
|
|
||
| val search = SearchDialogAction( | ||
| label = { }, | ||
| tooltip = { } | ||
| ).apply(searchButton) | ||
|
|
||
| val cancel = SearchDialogAction( | ||
| label = { }, | ||
| tooltip = { } | ||
| ).apply(cancelButton) | ||
|
|
||
| base { | ||
| title(title) | ||
| externalTitle(externalTitle) | ||
| canCloseWithEscape(canCloseWithEscape) | ||
| afterAction(afterAction) | ||
|
|
||
| body(body) | ||
|
|
||
| input { | ||
| text(input.key) { | ||
| label(input.label) | ||
| initial(input.initialValue) | ||
|
|
||
| input.inputModifier(this) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| type { | ||
| confirmation { | ||
| no { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
| label(search.label) | ||
| tooltip(search.tooltip) | ||
|
|
||
| action { | ||
| customPlayerClick { response, player -> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the returned Useful? React with 👍 / 👎. |
||
| player.closeDialog() | ||
|
|
||
| onSearch(player, response.getText(input.key) ?: "") | ||
| } | ||
| } | ||
| } | ||
|
|
||
| yes { | ||
| label(cancel.label) | ||
| tooltip(cancel.tooltip) | ||
|
|
||
| action { | ||
| customPlayerClick { response, player -> | ||
| player.closeDialog() | ||
|
|
||
| onClose(player, response.getText(input.key) ?: "") | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| package dev.slne.surf.api.minestom.dialog.search | ||
|
|
||
| import net.kyori.adventure.text.Component | ||
| import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer | ||
| import net.minestom.server.dialog.Dialog | ||
| import net.minestom.server.dialog.DialogAfterAction | ||
| import net.minestom.server.dialog.DialogBody | ||
| import net.minestom.server.dialog.DialogInput | ||
| import net.minestom.testing.EnvTest | ||
| import org.junit.jupiter.api.Test | ||
| import kotlin.test.assertEquals | ||
| import kotlin.test.assertFalse | ||
| import kotlin.test.assertIs | ||
|
|
||
| @EnvTest | ||
| class SearchDialogTest { | ||
|
|
||
| @Test | ||
| fun `a search dialog asks for a single query`() { | ||
| val dialog = searchDialog( | ||
| title = { text("Search") }, | ||
| onSearch = { _, _ -> }, | ||
| onClose = { _, _ -> }, | ||
| searchInput = { initialValue = "red" }, | ||
| ) | ||
|
|
||
| val confirmation = assertIs<Dialog.Confirmation>(dialog) | ||
| val input = assertIs<DialogInput.Text>(confirmation.metadata().inputs().single()) | ||
|
|
||
| assertEquals("search", input.key()) | ||
| assertEquals("red", input.initial()) | ||
| } | ||
|
|
||
| @Test | ||
| fun `a search dialog keeps its title and body`() { | ||
| val dialog = searchDialog( | ||
| title = { text("Search") }, | ||
| onSearch = { _, _ -> }, | ||
| onClose = { _, _ -> }, | ||
| body = { plainMessage(Component.text("Who are you looking for?")) }, | ||
| searchInput = { }, | ||
| ) | ||
|
|
||
| assertEquals("Search", plain(dialog.metadata().title())) | ||
| assertEquals( | ||
| listOf(DialogBody.PlainMessage(Component.text("Who are you looking for?"), 200)), | ||
| dialog.metadata().body() | ||
| ) | ||
| } | ||
|
|
||
| private fun plain(component: Component) = | ||
| PlainTextComponentSerializer.plainText().serialize(component) | ||
|
|
||
| @Test | ||
| fun `a search dialog stays open until a button is pressed`() { | ||
| val dialog = searchDialog( | ||
| title = { text("Search") }, | ||
| onSearch = { _, _ -> }, | ||
| onClose = { _, _ -> }, | ||
| searchInput = { }, | ||
| ) | ||
|
|
||
| assertFalse(dialog.metadata().canCloseWithEscape()) | ||
| assertEquals(DialogAfterAction.NONE, dialog.metadata().afterAction()) | ||
| } | ||
|
|
||
| @Test | ||
| fun `the query is collected under the key it was asked for`() { | ||
| val dialog = searchDialog( | ||
| title = { text("Search") }, | ||
| onSearch = { _, _ -> }, | ||
| onClose = { _, _ -> }, | ||
| searchInput = { | ||
| key = "player" | ||
| inputModifier = { maxLength(16) } | ||
| }, | ||
| ) | ||
|
|
||
| val input = assertIs<DialogInput.Text>(dialog.metadata().inputs().single()) | ||
|
|
||
| assertEquals("player", input.key()) | ||
| assertEquals(16, input.maxLength()) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a caller supplies either button block to customize only one property—for example, changing the cancel tooltip—the corresponding default-argument block is not executed, and these empty initializers erase the default label and tooltip. The resulting button can therefore have a blank label; initialize each
SearchDialogActionwith its documented default values before applying the caller's block, as is already done forSearchInput.Useful? React with 👍 / 👎.