feat: add board detail create and filter toolbar UI
This commit is contained in:
@@ -27,6 +27,7 @@ import androidx.viewpager2.widget.ViewPager2
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.chip.Chip
|
||||
import java.text.DateFormat
|
||||
import java.time.LocalDate
|
||||
import java.util.ArrayDeque
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
@@ -50,6 +51,7 @@ import space.hackenslacker.kanbn4droid.app.boarddetail.BoardDetailDataSource
|
||||
import space.hackenslacker.kanbn4droid.app.boarddetail.BoardListDetail
|
||||
import space.hackenslacker.kanbn4droid.app.boarddetail.BoardTagSummary
|
||||
import space.hackenslacker.kanbn4droid.app.boarddetail.CardBatchMutationResult
|
||||
import space.hackenslacker.kanbn4droid.app.boarddetail.CreatedEntityRef
|
||||
import space.hackenslacker.kanbn4droid.app.auth.ApiKeyStore
|
||||
import space.hackenslacker.kanbn4droid.app.auth.SessionStore
|
||||
import space.hackenslacker.kanbn4droid.app.boards.BoardsApiResult
|
||||
@@ -307,6 +309,54 @@ class BoardDetailFlowTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun toolbarNormalMode_showsFilterAndSearchActions() {
|
||||
val scenario = launchBoardDetail()
|
||||
onView(withText("Card 1")).check(matches(isDisplayed()))
|
||||
|
||||
awaitCondition {
|
||||
var present = false
|
||||
scenario.onActivity { activity ->
|
||||
val menu = activity.findViewById<com.google.android.material.appbar.MaterialToolbar>(R.id.boardDetailToolbar).menu
|
||||
present = menu.findItem(R.id.actionFilterByTag) != null && menu.findItem(R.id.actionSearch) != null
|
||||
}
|
||||
present
|
||||
}
|
||||
|
||||
scenario.onActivity { activity ->
|
||||
val menu = activity.findViewById<com.google.android.material.appbar.MaterialToolbar>(R.id.boardDetailToolbar).menu
|
||||
assertNotNull(menu.findItem(R.id.actionFilterByTag))
|
||||
assertNotNull(menu.findItem(R.id.actionSearch))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun selectionMode_replacesFilterSearchWithSelectionActions() {
|
||||
val scenario = launchBoardDetail()
|
||||
|
||||
onView(withText("Card 1")).perform(longClick())
|
||||
|
||||
scenario.onActivity { activity ->
|
||||
val menu = activity.findViewById<com.google.android.material.appbar.MaterialToolbar>(R.id.boardDetailToolbar).menu
|
||||
assertNotNull(menu.findItem(R.id.actionSelectAll))
|
||||
assertNotNull(menu.findItem(R.id.actionMoveCards))
|
||||
assertNotNull(menu.findItem(R.id.actionDeleteCards))
|
||||
assertNull(menu.findItem(R.id.actionFilterByTag))
|
||||
assertNull(menu.findItem(R.id.actionSearch))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fab_isVisible_andOpensAddChooser() {
|
||||
launchBoardDetail()
|
||||
|
||||
onView(withId(R.id.boardDetailCreateFab)).check(matches(isDisplayed()))
|
||||
onView(withId(R.id.boardDetailCreateFab)).perform(click())
|
||||
|
||||
onView(withText("Add new list")).inRoot(isDialog()).check(matches(isDisplayed()))
|
||||
onView(withText("Add new card")).inRoot(isDialog()).check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun missingBoardIdShowsBlockingDialogAndFinishes() {
|
||||
val scenario = launchBoardDetail(boardId = null)
|
||||
@@ -636,6 +686,20 @@ class BoardDetailFlowTest {
|
||||
var lastDeleteCardIds: Set<String> = emptySet()
|
||||
var lastMoveTargetListId: String? = null
|
||||
|
||||
override suspend fun createList(boardPublicId: String, title: String): BoardsApiResult<CreatedEntityRef> {
|
||||
return BoardsApiResult.Failure("Not implemented in test fake")
|
||||
}
|
||||
|
||||
override suspend fun createCard(
|
||||
listPublicId: String,
|
||||
title: String,
|
||||
description: String?,
|
||||
dueDate: LocalDate?,
|
||||
tagPublicIds: Collection<String>,
|
||||
): BoardsApiResult<CreatedEntityRef> {
|
||||
return BoardsApiResult.Failure("Not implemented in test fake")
|
||||
}
|
||||
|
||||
override suspend fun getBoardDetail(boardId: String): BoardsApiResult<BoardDetail> {
|
||||
loadGate?.await()
|
||||
return if (loadResults.isNotEmpty()) {
|
||||
|
||||
@@ -2,20 +2,32 @@ package space.hackenslacker.kanbn4droid.app.boarddetail
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.CheckBox
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Button
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.launch
|
||||
import space.hackenslacker.kanbn4droid.app.MainActivity
|
||||
@@ -42,8 +54,14 @@ class BoardDetailActivity : AppCompatActivity() {
|
||||
private lateinit var fullScreenErrorContainer: View
|
||||
private lateinit var fullScreenErrorText: TextView
|
||||
private lateinit var retryButton: Button
|
||||
private lateinit var createFab: FloatingActionButton
|
||||
|
||||
private var inlineTitleErrorMessage: String? = null
|
||||
private var fabChooserDialog: AlertDialog? = null
|
||||
private var addListDialog: AlertDialog? = null
|
||||
private var addCardDialog: AlertDialog? = null
|
||||
private var filterDialog: AlertDialog? = null
|
||||
private var searchDialog: AlertDialog? = null
|
||||
private var moveDialog: AlertDialog? = null
|
||||
private var deleteSecondConfirmationDialog: AlertDialog? = null
|
||||
private var dismissMoveDialogWhenMutationEnds: Boolean = false
|
||||
@@ -105,6 +123,42 @@ class BoardDetailActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
renderToolbarMenu(menu, viewModel.uiState.value)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
||||
renderToolbarMenu(menu, viewModel.uiState.value)
|
||||
return super.onPrepareOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.actionFilterByTag -> {
|
||||
viewModel.onTagFilterIconTapped()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.actionSearch -> {
|
||||
viewModel.onSearchIconTapped()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.actionSelectAll,
|
||||
R.id.actionMoveCards,
|
||||
R.id.actionDeleteCards,
|
||||
-> handleSelectionAction(item)
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPostResume() {
|
||||
super.onPostResume()
|
||||
renderSelectionActions(viewModel.uiState.value)
|
||||
}
|
||||
|
||||
private fun bindViews() {
|
||||
toolbar = findViewById(R.id.boardDetailToolbar)
|
||||
pager = findViewById(R.id.boardDetailPager)
|
||||
@@ -113,6 +167,7 @@ class BoardDetailActivity : AppCompatActivity() {
|
||||
fullScreenErrorContainer = findViewById(R.id.boardDetailFullScreenErrorContainer)
|
||||
fullScreenErrorText = findViewById(R.id.boardDetailFullScreenErrorText)
|
||||
retryButton = findViewById(R.id.boardDetailRetryButton)
|
||||
createFab = findViewById(R.id.boardDetailCreateFab)
|
||||
}
|
||||
|
||||
private fun setupToolbar() {
|
||||
@@ -125,6 +180,9 @@ class BoardDetailActivity : AppCompatActivity() {
|
||||
retryButton.setOnClickListener {
|
||||
viewModel.retryLoad()
|
||||
}
|
||||
createFab.setOnClickListener {
|
||||
viewModel.openFabChooser()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupPager() {
|
||||
@@ -260,7 +318,10 @@ class BoardDetailActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
renderSelectionActions(state)
|
||||
invalidateOptionsMenu()
|
||||
renderOpenDialogs(state)
|
||||
createFab.isEnabled = !state.isMutating
|
||||
createFab.visibility = if (state.selectedCardIds.isEmpty()) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
private fun showBlockingStartupErrorAndFinish(message: String) {
|
||||
@@ -275,6 +336,69 @@ class BoardDetailActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun renderOpenDialogs(state: BoardDetailUiState) {
|
||||
if (state.isFabChooserOpen && fabChooserDialog == null) {
|
||||
showFabChooserDialog(state)
|
||||
} else if (!state.isFabChooserOpen) {
|
||||
fabChooserDialog?.dismiss()
|
||||
fabChooserDialog = null
|
||||
}
|
||||
|
||||
if (state.isAddListDialogOpen && addListDialog == null) {
|
||||
showAddListDialog(state)
|
||||
}
|
||||
addListDialog?.let { dialog ->
|
||||
val titleLayout = dialog.findViewById<TextInputLayout>(R.id.addListTitleLayout)
|
||||
val titleInput = dialog.findViewById<TextInputEditText>(R.id.addListTitleInput)
|
||||
titleLayout?.error = state.addListTitleError
|
||||
titleInput?.isEnabled = !state.isMutating
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.isEnabled = !state.isMutating
|
||||
dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.isEnabled = !state.isMutating
|
||||
if (!state.isAddListDialogOpen) {
|
||||
dialog.dismiss()
|
||||
addListDialog = null
|
||||
}
|
||||
}
|
||||
|
||||
if (state.isAddCardDialogOpen && addCardDialog == null) {
|
||||
showAddCardDialog(state)
|
||||
}
|
||||
addCardDialog?.let { dialog ->
|
||||
val titleLayout = dialog.findViewById<TextInputLayout>(R.id.addCardTitleLayout)
|
||||
val titleInput = dialog.findViewById<TextInputEditText>(R.id.addCardTitleInput)
|
||||
val descriptionInput = dialog.findViewById<TextInputEditText>(R.id.addCardDescriptionInput)
|
||||
titleLayout?.error = state.addCardTitleError
|
||||
titleInput?.isEnabled = !state.isMutating
|
||||
descriptionInput?.isEnabled = !state.isMutating
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.isEnabled = !state.isMutating
|
||||
dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.isEnabled = !state.isMutating
|
||||
if (!state.isAddCardDialogOpen) {
|
||||
dialog.dismiss()
|
||||
addCardDialog = null
|
||||
}
|
||||
}
|
||||
|
||||
if (state.isFilterDialogOpen && filterDialog == null) {
|
||||
showFilterDialog(state)
|
||||
}
|
||||
if (!state.isFilterDialogOpen) {
|
||||
filterDialog?.dismiss()
|
||||
filterDialog = null
|
||||
}
|
||||
|
||||
if (state.isSearchDialogOpen && searchDialog == null) {
|
||||
showSearchDialog(state)
|
||||
}
|
||||
searchDialog?.let { dialog ->
|
||||
val input = dialog.findViewById<TextInputEditText>(R.id.searchTitleInput)
|
||||
input?.isEnabled = !state.isMutating
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.isEnabled = !state.isMutating
|
||||
dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.isEnabled = !state.isMutating
|
||||
if (!state.isSearchDialogOpen) {
|
||||
dialog.dismiss()
|
||||
searchDialog = null
|
||||
}
|
||||
}
|
||||
|
||||
val activeMoveDialog = moveDialog
|
||||
if (activeMoveDialog != null) {
|
||||
val lists = state.boardDetail?.lists.orEmpty()
|
||||
@@ -322,18 +446,43 @@ class BoardDetailActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun renderSelectionActions(state: BoardDetailUiState) {
|
||||
val inSelection = state.selectedCardIds.isNotEmpty()
|
||||
toolbar.menu.clear()
|
||||
if (!inSelection) {
|
||||
// Kept for compatibility with existing call sites; menu rendering is delegated to
|
||||
// framework options menu callbacks.
|
||||
}
|
||||
|
||||
private fun renderToolbarMenu(menu: Menu, state: BoardDetailUiState) {
|
||||
menu.clear()
|
||||
if (state.selectedCardIds.isNotEmpty()) {
|
||||
menu.add(Menu.NONE, R.id.actionSelectAll, Menu.NONE, getString(R.string.select_all)).apply {
|
||||
setIcon(R.drawable.ic_select_all_grid_24)
|
||||
setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
|
||||
tooltipText = getString(R.string.select_all)
|
||||
}
|
||||
menu.add(Menu.NONE, R.id.actionMoveCards, Menu.NONE, getString(R.string.move_cards)).apply {
|
||||
setIcon(R.drawable.ic_move_cards_horizontal_24)
|
||||
setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
|
||||
tooltipText = getString(R.string.move_cards)
|
||||
}
|
||||
menu.add(Menu.NONE, R.id.actionDeleteCards, Menu.NONE, getString(R.string.delete_cards)).apply {
|
||||
setIcon(R.drawable.ic_delete_24)
|
||||
setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
|
||||
tooltipText = getString(R.string.delete_cards)
|
||||
}
|
||||
return
|
||||
}
|
||||
toolbar.inflateMenu(R.menu.menu_board_detail_selection)
|
||||
toolbar.menu.findItem(R.id.actionSelectAll)?.tooltipText = getString(R.string.select_all)
|
||||
toolbar.menu.findItem(R.id.actionMoveCards)?.tooltipText = getString(R.string.move_cards)
|
||||
toolbar.menu.findItem(R.id.actionDeleteCards)?.tooltipText = getString(R.string.delete_cards)
|
||||
toolbar.setOnMenuItemClickListener { item ->
|
||||
handleSelectionAction(item)
|
||||
|
||||
val filterItem = menu.add(Menu.NONE, R.id.actionFilterByTag, Menu.NONE, getString(R.string.filter_by_tag)).apply {
|
||||
setIcon(R.drawable.ic_filter_list_24)
|
||||
setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
|
||||
tooltipText = getString(R.string.filter_by_tag)
|
||||
}
|
||||
val searchItem = menu.add(Menu.NONE, R.id.actionSearch, Menu.NONE, getString(R.string.search)).apply {
|
||||
setIcon(R.drawable.ic_search_24)
|
||||
setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
|
||||
tooltipText = getString(R.string.search)
|
||||
}
|
||||
tintMainMenuIcon(filterItem, state.activeTagFilterIds.isNotEmpty())
|
||||
tintMainMenuIcon(searchItem, state.activeTitleQuery.isNotBlank())
|
||||
}
|
||||
|
||||
private fun handleSelectionAction(item: MenuItem): Boolean {
|
||||
@@ -357,6 +506,189 @@ class BoardDetailActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun tintMainMenuIcon(item: MenuItem?, isActive: Boolean) {
|
||||
val drawableRes = when (item?.itemId) {
|
||||
R.id.actionFilterByTag -> R.drawable.ic_filter_list_24
|
||||
R.id.actionSearch -> R.drawable.ic_search_24
|
||||
else -> null
|
||||
} ?: return
|
||||
val icon = AppCompatResources.getDrawable(this, drawableRes)?.mutate() ?: return
|
||||
val wrapped = DrawableCompat.wrap(icon)
|
||||
val tintColor = if (isActive) {
|
||||
MaterialColors.getColor(this, com.google.android.material.R.attr.colorPrimary, ContextCompat.getColor(this, android.R.color.holo_blue_light))
|
||||
} else {
|
||||
MaterialColors.getColor(this, com.google.android.material.R.attr.colorOnSurfaceVariant, ContextCompat.getColor(this, android.R.color.black))
|
||||
}
|
||||
DrawableCompat.setTint(wrapped, tintColor)
|
||||
item?.icon = wrapped
|
||||
}
|
||||
|
||||
private fun showFabChooserDialog(state: BoardDetailUiState) {
|
||||
val root = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
setPadding(48, 24, 48, 24)
|
||||
}
|
||||
val addListButton = MaterialButton(this).apply {
|
||||
text = getString(R.string.add_new_list)
|
||||
setOnClickListener {
|
||||
viewModel.openAddListDialog()
|
||||
}
|
||||
}
|
||||
val addCardButton = MaterialButton(this).apply {
|
||||
text = getString(R.string.add_new_card)
|
||||
isEnabled = state.canAddCard
|
||||
setOnClickListener {
|
||||
viewModel.openAddCardDialog()
|
||||
}
|
||||
}
|
||||
val helperText = TextView(this).apply {
|
||||
text = getString(R.string.create_a_list_first_to_add_cards)
|
||||
visibility = if (state.canAddCard) View.GONE else View.VISIBLE
|
||||
setPadding(8, 4, 8, 0)
|
||||
}
|
||||
root.addView(addListButton)
|
||||
root.addView(addCardButton)
|
||||
root.addView(helperText)
|
||||
|
||||
val dialog = MaterialAlertDialogBuilder(this)
|
||||
.setView(root)
|
||||
.setOnDismissListener {
|
||||
if (fabChooserDialog != null) {
|
||||
viewModel.closeFabChooser()
|
||||
}
|
||||
fabChooserDialog = null
|
||||
}
|
||||
.create()
|
||||
fabChooserDialog = dialog
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
private fun showAddListDialog(state: BoardDetailUiState) {
|
||||
val dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_add_list, null)
|
||||
val titleLayout: TextInputLayout = dialogView.findViewById(R.id.addListTitleLayout)
|
||||
val titleInput: TextInputEditText = dialogView.findViewById(R.id.addListTitleInput)
|
||||
titleInput.setText(state.addListTitleDraft)
|
||||
titleInput.doAfterTextChanged { viewModel.updateAddListTitle(it?.toString().orEmpty()) }
|
||||
|
||||
val dialog = MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.add_new_list)
|
||||
.setView(dialogView)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.add_list, null)
|
||||
.create()
|
||||
dialog.setOnShowListener {
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setOnClickListener {
|
||||
titleLayout.error = null
|
||||
viewModel.createList()
|
||||
}
|
||||
}
|
||||
dialog.setOnDismissListener {
|
||||
if (addListDialog != null && viewModel.uiState.value.isAddListDialogOpen) {
|
||||
viewModel.cancelAddListDialog()
|
||||
}
|
||||
addListDialog = null
|
||||
}
|
||||
addListDialog = dialog
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
private fun showAddCardDialog(state: BoardDetailUiState) {
|
||||
val dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_add_card, null)
|
||||
val titleInput: TextInputEditText = dialogView.findViewById(R.id.addCardTitleInput)
|
||||
val descriptionInput: TextInputEditText = dialogView.findViewById(R.id.addCardDescriptionInput)
|
||||
titleInput.setText(state.addCardTitleDraft)
|
||||
descriptionInput.setText(state.addCardDescriptionDraft)
|
||||
titleInput.doAfterTextChanged { viewModel.updateAddCardTitle(it?.toString().orEmpty()) }
|
||||
descriptionInput.doAfterTextChanged { viewModel.updateAddCardDescription(it?.toString().orEmpty()) }
|
||||
|
||||
val dialog = MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.add_new_card)
|
||||
.setView(dialogView)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.add_card, null)
|
||||
.create()
|
||||
dialog.setOnShowListener {
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setOnClickListener {
|
||||
viewModel.createCard()
|
||||
}
|
||||
}
|
||||
dialog.setOnDismissListener {
|
||||
if (addCardDialog != null && viewModel.uiState.value.isAddCardDialogOpen) {
|
||||
viewModel.cancelAddCardDialog()
|
||||
}
|
||||
addCardDialog = null
|
||||
}
|
||||
addCardDialog = dialog
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
private fun showFilterDialog(state: BoardDetailUiState) {
|
||||
val dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_filter_tags, null)
|
||||
val container: LinearLayout = dialogView.findViewById(R.id.filterTagsContainer)
|
||||
val tags = state.boardDetail
|
||||
?.lists
|
||||
.orEmpty()
|
||||
.flatMap { it.cards }
|
||||
.flatMap { it.tags }
|
||||
.distinctBy { it.id }
|
||||
.sortedBy { it.name.lowercase() }
|
||||
|
||||
tags.forEach { tag ->
|
||||
val checkBox = CheckBox(this).apply {
|
||||
text = tag.name
|
||||
isChecked = state.pendingTagFilterIds.contains(tag.id)
|
||||
setOnCheckedChangeListener { _, _ ->
|
||||
val selected = mutableSetOf<String>()
|
||||
for (i in 0 until container.childCount) {
|
||||
val child = container.getChildAt(i)
|
||||
if (child is CheckBox && child.isChecked) {
|
||||
selected.add(tags[i].id)
|
||||
}
|
||||
}
|
||||
viewModel.updatePendingTagFilterIds(selected)
|
||||
}
|
||||
}
|
||||
container.addView(checkBox)
|
||||
}
|
||||
|
||||
val dialog = MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.filter_by_tag)
|
||||
.setView(dialogView)
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> viewModel.cancelFilterDialog() }
|
||||
.setPositiveButton(R.string.filter) { _, _ -> viewModel.applyFilterDialog() }
|
||||
.create()
|
||||
dialog.setOnDismissListener {
|
||||
if (filterDialog != null && viewModel.uiState.value.isFilterDialogOpen) {
|
||||
viewModel.cancelFilterDialog()
|
||||
}
|
||||
filterDialog = null
|
||||
}
|
||||
filterDialog = dialog
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
private fun showSearchDialog(state: BoardDetailUiState) {
|
||||
val dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_search_title, null)
|
||||
val input: TextInputEditText = dialogView.findViewById(R.id.searchTitleInput)
|
||||
input.setText(state.pendingTitleQuery)
|
||||
input.doAfterTextChanged { viewModel.updatePendingTitleQuery(it?.toString().orEmpty()) }
|
||||
|
||||
val dialog = MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.search_by_title)
|
||||
.setView(dialogView)
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> viewModel.cancelSearchDialog() }
|
||||
.setPositiveButton(R.string.search) { _, _ -> viewModel.applySearchDialog() }
|
||||
.create()
|
||||
dialog.setOnDismissListener {
|
||||
if (searchDialog != null && viewModel.uiState.value.isSearchDialogOpen) {
|
||||
viewModel.cancelSearchDialog()
|
||||
}
|
||||
searchDialog = null
|
||||
}
|
||||
searchDialog = dialog
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
private fun showMoveCardsDialog() {
|
||||
val lists = viewModel.uiState.value.boardDetail?.lists.orEmpty()
|
||||
if (lists.isEmpty()) {
|
||||
|
||||
12
app/src/main/res/drawable/ic_filter_list_24.xml
Normal file
12
app/src/main/res/drawable/ic_filter_list_24.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M4,7h16v2H4zM7,11h10v2H7zM10,15h4v2h-4z" />
|
||||
|
||||
</vector>
|
||||
12
app/src/main/res/drawable/ic_search_24.xml
Normal file
12
app/src/main/res/drawable/ic_search_24.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M15.5,14h-0.79l-0.28,-0.27a6,6 0,1 0,-1.41,1.41l0.27,0.28v0.79L20,21.5L21.5,20zM10,14a4,4 0,1 1,0 -8a4,4 0,0 1,0 8z" />
|
||||
|
||||
</vector>
|
||||
@@ -68,4 +68,13 @@
|
||||
android:text="@string/retry" />
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/boardDetailCreateFab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp"
|
||||
android:contentDescription="@string/board_detail_add"
|
||||
app:srcCompat="@android:drawable/ic_input_add" />
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
44
app/src/main/res/layout/dialog_add_card.xml
Normal file
44
app/src/main/res/layout/dialog_add_card.xml
Normal file
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingEnd="24dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/addCardTitleLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/card_title">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/addCardTitleInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textCapSentences|text"
|
||||
android:maxLines="1" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:hint="@string/description">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/addCardDescriptionInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="top|start"
|
||||
android:inputType="textMultiLine|textCapSentences"
|
||||
android:minLines="3" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
25
app/src/main/res/layout/dialog_add_list.xml
Normal file
25
app/src/main/res/layout/dialog_add_list.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingEnd="24dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/addListTitleLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/list_title_hint">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/addListTitleInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textCapSentences|text"
|
||||
android:maxLines="1" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
29
app/src/main/res/layout/dialog_filter_tags.xml
Normal file
29
app/src/main/res/layout/dialog_filter_tags.xml
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingEnd="24dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/filterTagsPlaceholderText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/filter_tags_placeholder"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/filterTagsContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:orientation="vertical" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
25
app/src/main/res/layout/dialog_search_title.xml
Normal file
25
app/src/main/res/layout/dialog_search_title.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingEnd="24dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/searchTitleLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/search">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/searchTitleInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text"
|
||||
android:maxLines="1" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
16
app/src/main/res/menu/menu_board_detail_main.xml
Normal file
16
app/src/main/res/menu/menu_board_detail_main.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/actionFilterByTag"
|
||||
android:icon="@drawable/ic_filter_list_24"
|
||||
android:title="@string/filter_by_tag"
|
||||
app:showAsAction="always" />
|
||||
|
||||
<item
|
||||
android:id="@+id/actionSearch"
|
||||
android:icon="@drawable/ic_search_24"
|
||||
android:title="@string/search"
|
||||
app:showAsAction="always" />
|
||||
</menu>
|
||||
@@ -50,4 +50,18 @@
|
||||
<string name="card_detail_placeholder_subtitle">Card detail view is coming soon.</string>
|
||||
<string name="board_detail_unable_to_open_board">Unable to open board.</string>
|
||||
<string name="board_detail_session_expired">Session expired. Please sign in again.</string>
|
||||
<string name="board_detail_add">Add</string>
|
||||
<string name="filter_by_tag">Filter by tag</string>
|
||||
<string name="search">Search</string>
|
||||
<string name="search_by_title">Search by title</string>
|
||||
<string name="filter">Filter</string>
|
||||
<string name="add_new_list">Add new list</string>
|
||||
<string name="add_new_card">Add new card</string>
|
||||
<string name="add_list">Add list</string>
|
||||
<string name="add_card">Add card</string>
|
||||
<string name="create_a_list_first_to_add_cards">Create a list first to add cards.</string>
|
||||
<string name="card_title">Card title</string>
|
||||
<string name="card_title_required">Card title is required</string>
|
||||
<string name="description">Description</string>
|
||||
<string name="filter_tags_placeholder">Tag selector will be wired in the next task.</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user