mirror of
https://github.com/anyproto/anytype-kotlin.git
synced 2025-06-08 13:57:10 +09:00
Wire action toolbar with middleware (#115)
* Implemented block duplication * Focusing new paragraph when created * Refactored tests, also added new tests for block deletion operations
This commit is contained in:
parent
c8d09dcef1
commit
d5864a0d6d
32 changed files with 910 additions and 243 deletions
|
@ -1,9 +1,7 @@
|
|||
package com.agileburo.anytype.di.feature
|
||||
|
||||
import com.agileburo.anytype.core_utils.di.scope.PerScreen
|
||||
import com.agileburo.anytype.domain.block.interactor.CreateBlock
|
||||
import com.agileburo.anytype.domain.block.interactor.UpdateBlock
|
||||
import com.agileburo.anytype.domain.block.interactor.UpdateCheckbox
|
||||
import com.agileburo.anytype.domain.block.interactor.*
|
||||
import com.agileburo.anytype.domain.block.repo.BlockRepository
|
||||
import com.agileburo.anytype.domain.event.interactor.ObserveEvents
|
||||
import com.agileburo.anytype.domain.page.ClosePage
|
||||
|
@ -40,14 +38,18 @@ class PageModule {
|
|||
updateBlock: UpdateBlock,
|
||||
createBlock: CreateBlock,
|
||||
observeEvents: ObserveEvents,
|
||||
updateCheckbox: UpdateCheckbox
|
||||
updateCheckbox: UpdateCheckbox,
|
||||
unlinkBlocks: UnlinkBlocks,
|
||||
duplicateBlock: DuplicateBlock
|
||||
): PageViewModelFactory = PageViewModelFactory(
|
||||
openPage = openPage,
|
||||
closePage = closePage,
|
||||
updateBlock = updateBlock,
|
||||
createBlock = createBlock,
|
||||
observeEvents = observeEvents,
|
||||
updateCheckbox = updateCheckbox
|
||||
updateCheckbox = updateCheckbox,
|
||||
unlinkBlocks = unlinkBlocks,
|
||||
duplicateBlock = duplicateBlock
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
@ -106,4 +108,20 @@ class PageModule {
|
|||
): UpdateCheckbox = UpdateCheckbox(
|
||||
repo = repo
|
||||
)
|
||||
|
||||
@Provides
|
||||
@PerScreen
|
||||
fun provideUnlinkBlocksUseCase(
|
||||
repo: BlockRepository
|
||||
): UnlinkBlocks = UnlinkBlocks(
|
||||
repo = repo
|
||||
)
|
||||
|
||||
@Provides
|
||||
@PerScreen
|
||||
fun provideDuplicateBlockUseCase(
|
||||
repo: BlockRepository
|
||||
): DuplicateBlock = DuplicateBlock(
|
||||
repo = repo
|
||||
)
|
||||
}
|
|
@ -13,6 +13,8 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
import com.agileburo.anytype.R
|
||||
import com.agileburo.anytype.core_ui.features.page.BlockAdapter
|
||||
import com.agileburo.anytype.core_ui.state.ControlPanelState
|
||||
import com.agileburo.anytype.core_ui.widgets.toolbar.ActionToolbarWidget.ActionConfig.ACTION_DELETE
|
||||
import com.agileburo.anytype.core_ui.widgets.toolbar.ActionToolbarWidget.ActionConfig.ACTION_DUPLICATE
|
||||
import com.agileburo.anytype.core_ui.widgets.toolbar.ColorToolbarWidget
|
||||
import com.agileburo.anytype.core_ui.widgets.toolbar.OptionToolbarWidget.Option
|
||||
import com.agileburo.anytype.core_ui.widgets.toolbar.OptionToolbarWidget.OptionConfig.OPTION_LIST_BULLETED_LIST
|
||||
|
@ -35,6 +37,7 @@ import kotlinx.coroutines.delay
|
|||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
|
@ -50,13 +53,9 @@ class PageFragment : NavigationFragment(R.layout.fragment_page) {
|
|||
marks = editable.extractMarks()
|
||||
)
|
||||
},
|
||||
onSelectionChanged = { id, selection ->
|
||||
vm.onSelectionChanged(
|
||||
id = id,
|
||||
selection = selection
|
||||
)
|
||||
},
|
||||
onCheckboxClicked = vm::onCheckboxClicked
|
||||
onSelectionChanged = vm::onSelectionChanged,
|
||||
onCheckboxClicked = vm::onCheckboxClicked,
|
||||
onFocusChanged = vm::onBlockFocusChanged
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -104,6 +103,11 @@ class PageFragment : NavigationFragment(R.layout.fragment_page) {
|
|||
.onEach { vm.onAddBlockToolbarClicked() }
|
||||
.launchIn(lifecycleScope)
|
||||
|
||||
toolbar
|
||||
.actionClicks()
|
||||
.onEach { vm.onActionToolbarClicked() }
|
||||
.launchIn(lifecycleScope)
|
||||
|
||||
markupToolbar
|
||||
.markupClicks()
|
||||
.onEach { vm.onMarkupActionClicked(it) }
|
||||
|
@ -156,6 +160,16 @@ class PageFragment : NavigationFragment(R.layout.fragment_page) {
|
|||
}
|
||||
.launchIn(lifecycleScope)
|
||||
|
||||
actionToolbar
|
||||
.actionClicks()
|
||||
.onEach { action ->
|
||||
when (action.type) {
|
||||
ACTION_DELETE -> vm.onActionDeleteClicked()
|
||||
ACTION_DUPLICATE -> vm.onActionDuplicateClicked()
|
||||
else -> toast(NOT_IMPLEMENTED_MESSAGE)
|
||||
}
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
private fun showColorToolbar() {
|
||||
|
@ -194,6 +208,7 @@ class PageFragment : NavigationFragment(R.layout.fragment_page) {
|
|||
}
|
||||
|
||||
private fun render(state: ControlPanelState) {
|
||||
Timber.d("Rendering new control panel state:\n$state")
|
||||
markupToolbar.setState(state.markupToolbar)
|
||||
toolbar.setState(state.blockToolbar)
|
||||
with(state.colorToolbar) {
|
||||
|
@ -216,6 +231,16 @@ class PageFragment : NavigationFragment(R.layout.fragment_page) {
|
|||
} else
|
||||
hideOptionToolbar()
|
||||
}
|
||||
with(state.actionToolbar) {
|
||||
if (isVisible) {
|
||||
hideSoftInput()
|
||||
lifecycleScope.launch {
|
||||
delay(300)
|
||||
actionToolbar.show()
|
||||
}
|
||||
} else
|
||||
actionToolbar.hide()
|
||||
}
|
||||
}
|
||||
|
||||
override fun injectDependencies() {
|
||||
|
|
|
@ -65,6 +65,13 @@
|
|||
android:layout_gravity="bottom"
|
||||
android:visibility="invisible" />
|
||||
|
||||
<com.agileburo.anytype.core_ui.widgets.toolbar.ActionToolbarWidget
|
||||
android:id="@+id/actionToolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:visibility="invisible" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -37,7 +37,8 @@ class BlockAdapter(
|
|||
private var blocks: List<BlockView>,
|
||||
private val onTextChanged: (String, Editable) -> Unit,
|
||||
private val onSelectionChanged: (String, IntRange) -> Unit,
|
||||
private val onCheckboxClicked: (String) -> Unit
|
||||
private val onCheckboxClicked: (String) -> Unit,
|
||||
private val onFocusChanged: (String, Boolean) -> Unit
|
||||
) : RecyclerView.Adapter<BlockViewHolder>() {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BlockViewHolder {
|
||||
|
@ -252,31 +253,36 @@ class BlockAdapter(
|
|||
holder.bind(
|
||||
item = blocks[position] as BlockView.Text,
|
||||
onTextChanged = onTextChanged,
|
||||
onSelectionChanged = onSelectionChanged
|
||||
onSelectionChanged = onSelectionChanged,
|
||||
onFocusChanged = onFocusChanged
|
||||
)
|
||||
}
|
||||
is BlockViewHolder.Title -> {
|
||||
holder.bind(
|
||||
item = blocks[position] as BlockView.Title,
|
||||
onTextChanged = onTextChanged
|
||||
onTextChanged = onTextChanged,
|
||||
onFocusChanged = onFocusChanged
|
||||
)
|
||||
}
|
||||
is BlockViewHolder.HeaderOne -> {
|
||||
holder.bind(
|
||||
item = blocks[position] as BlockView.HeaderOne,
|
||||
onTextChanged = onTextChanged
|
||||
onTextChanged = onTextChanged,
|
||||
onFocusChanged = onFocusChanged
|
||||
)
|
||||
}
|
||||
is BlockViewHolder.HeaderTwo -> {
|
||||
holder.bind(
|
||||
item = blocks[position] as BlockView.HeaderTwo,
|
||||
onTextChanged = onTextChanged
|
||||
onTextChanged = onTextChanged,
|
||||
onFocusChanged = onFocusChanged
|
||||
)
|
||||
}
|
||||
is BlockViewHolder.HeaderThree -> {
|
||||
holder.bind(
|
||||
item = blocks[position] as BlockView.HeaderThree,
|
||||
onTextChanged = onTextChanged
|
||||
onTextChanged = onTextChanged,
|
||||
onFocusChanged = onFocusChanged
|
||||
)
|
||||
}
|
||||
is BlockViewHolder.Code -> {
|
||||
|
@ -289,7 +295,8 @@ class BlockAdapter(
|
|||
item = blocks[position] as BlockView.Checkbox,
|
||||
onTextChanged = onTextChanged,
|
||||
onCheckboxClicked = onCheckboxClicked,
|
||||
onSelectionChanged = onSelectionChanged
|
||||
onSelectionChanged = onSelectionChanged,
|
||||
onFocusChanged = onFocusChanged
|
||||
)
|
||||
}
|
||||
is BlockViewHolder.Task -> {
|
||||
|
@ -301,7 +308,8 @@ class BlockAdapter(
|
|||
holder.bind(
|
||||
item = blocks[position] as BlockView.Bulleted,
|
||||
onTextChanged = onTextChanged,
|
||||
onSelectionChanged = onSelectionChanged
|
||||
onSelectionChanged = onSelectionChanged,
|
||||
onFocusChanged = onFocusChanged
|
||||
)
|
||||
}
|
||||
is BlockViewHolder.Numbered -> {
|
||||
|
@ -342,7 +350,8 @@ class BlockAdapter(
|
|||
is BlockViewHolder.Highlight -> {
|
||||
holder.bind(
|
||||
item = blocks[position] as BlockView.Highlight,
|
||||
onTextChanged = onTextChanged
|
||||
onTextChanged = onTextChanged,
|
||||
onFocusChanged = onFocusChanged
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,10 +64,15 @@ sealed class BlockViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
|||
fun bind(
|
||||
item: BlockView.Text,
|
||||
onTextChanged: (String, Editable) -> Unit,
|
||||
onSelectionChanged: (String, IntRange) -> Unit
|
||||
onSelectionChanged: (String, IntRange) -> Unit,
|
||||
onFocusChanged: (String, Boolean) -> Unit
|
||||
) {
|
||||
logOnBind()
|
||||
|
||||
content.setOnFocusChangeListener { _, hasFocus ->
|
||||
onFocusChanged(item.id, hasFocus)
|
||||
}
|
||||
|
||||
if (item.marks.isNotEmpty())
|
||||
content.setText(item.toSpannable(), TextView.BufferType.SPANNABLE)
|
||||
else
|
||||
|
@ -123,9 +128,15 @@ sealed class BlockViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
|||
|
||||
fun bind(
|
||||
item: BlockView.Title,
|
||||
onTextChanged: (String, Editable) -> Unit
|
||||
onTextChanged: (String, Editable) -> Unit,
|
||||
onFocusChanged: (String, Boolean) -> Unit
|
||||
) {
|
||||
title.setOnFocusChangeListener { _, hasFocus ->
|
||||
onFocusChanged(item.id, hasFocus)
|
||||
}
|
||||
|
||||
title.setText(item.text)
|
||||
|
||||
title.addTextChangedListener(
|
||||
DefaultTextWatcher { text ->
|
||||
onTextChanged(item.id, text)
|
||||
|
@ -140,8 +151,12 @@ sealed class BlockViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
|||
|
||||
fun bind(
|
||||
item: BlockView.HeaderOne,
|
||||
onTextChanged: (String, Editable) -> Unit
|
||||
onTextChanged: (String, Editable) -> Unit,
|
||||
onFocusChanged: (String, Boolean) -> Unit
|
||||
) {
|
||||
header.setOnFocusChangeListener { _, hasFocus ->
|
||||
onFocusChanged(item.id, hasFocus)
|
||||
}
|
||||
header.setText(item.text)
|
||||
header.addTextChangedListener(
|
||||
DefaultTextWatcher { text ->
|
||||
|
@ -157,8 +172,12 @@ sealed class BlockViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
|||
|
||||
fun bind(
|
||||
item: BlockView.HeaderTwo,
|
||||
onTextChanged: (String, Editable) -> Unit
|
||||
onTextChanged: (String, Editable) -> Unit,
|
||||
onFocusChanged: (String, Boolean) -> Unit
|
||||
) {
|
||||
header.setOnFocusChangeListener { _, hasFocus ->
|
||||
onFocusChanged(item.id, hasFocus)
|
||||
}
|
||||
header.setText(item.text)
|
||||
header.addTextChangedListener(
|
||||
DefaultTextWatcher { text ->
|
||||
|
@ -174,8 +193,12 @@ sealed class BlockViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
|||
|
||||
fun bind(
|
||||
item: BlockView.HeaderThree,
|
||||
onTextChanged: (String, Editable) -> Unit
|
||||
onTextChanged: (String, Editable) -> Unit,
|
||||
onFocusChanged: (String, Boolean) -> Unit
|
||||
) {
|
||||
header.setOnFocusChangeListener { _, hasFocus ->
|
||||
onFocusChanged(item.id, hasFocus)
|
||||
}
|
||||
header.setText(item.text)
|
||||
header.addTextChangedListener(
|
||||
DefaultTextWatcher { text ->
|
||||
|
@ -203,8 +226,12 @@ sealed class BlockViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
|||
item: BlockView.Checkbox,
|
||||
onTextChanged: (String, Editable) -> Unit,
|
||||
onCheckboxClicked: (String) -> Unit,
|
||||
onSelectionChanged: (String, IntRange) -> Unit
|
||||
onSelectionChanged: (String, IntRange) -> Unit,
|
||||
onFocusChanged: (String, Boolean) -> Unit
|
||||
) {
|
||||
content.setOnFocusChangeListener { _, hasFocus ->
|
||||
onFocusChanged(item.id, hasFocus)
|
||||
}
|
||||
|
||||
checkbox.isSelected = item.checked
|
||||
|
||||
|
@ -283,10 +310,15 @@ sealed class BlockViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
|||
fun bind(
|
||||
item: BlockView.Bulleted,
|
||||
onTextChanged: (String, Editable) -> Unit,
|
||||
onSelectionChanged: (String, IntRange) -> Unit
|
||||
onSelectionChanged: (String, IntRange) -> Unit,
|
||||
onFocusChanged: (String, Boolean) -> Unit
|
||||
) {
|
||||
Timber.d("Binding bullet")
|
||||
|
||||
content.setOnFocusChangeListener { _, hasFocus ->
|
||||
onFocusChanged(item.id, hasFocus)
|
||||
}
|
||||
|
||||
if (item.marks.isNotEmpty())
|
||||
content.setText(item.toSpannable(), TextView.BufferType.SPANNABLE)
|
||||
else
|
||||
|
@ -440,8 +472,12 @@ sealed class BlockViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
|||
|
||||
fun bind(
|
||||
item: BlockView.Highlight,
|
||||
onTextChanged: (String, Editable) -> Unit
|
||||
onTextChanged: (String, Editable) -> Unit,
|
||||
onFocusChanged: (String, Boolean) -> Unit
|
||||
) {
|
||||
content.setOnFocusChangeListener { _, hasFocus ->
|
||||
onFocusChanged(item.id, hasFocus)
|
||||
}
|
||||
content.setText(item.text)
|
||||
content.addTextChangedListener(
|
||||
DefaultTextWatcher { text ->
|
||||
|
@ -449,7 +485,6 @@ sealed class BlockViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
|||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -3,17 +3,22 @@ package com.agileburo.anytype.core_ui.state
|
|||
/**
|
||||
* Control panels are UI-elements that allow user to interact with blocks on a page.
|
||||
* Each panel is currently represented as a toolbar.
|
||||
* @property focus block currently associated with the control panel (if not present, control panel is not active)
|
||||
* @property blockToolbar block-toolbar state (main toolbar state)
|
||||
* @property markupToolbar markup toolbar state
|
||||
* @property colorToolbar color toolbar state
|
||||
* @property addBlockToolbar add-block toolbar state
|
||||
* @property actionToolbar action-toolbar state
|
||||
*/
|
||||
data class ControlPanelState(
|
||||
val focus: Focus? = null,
|
||||
val blockToolbar: Toolbar.Block,
|
||||
val markupToolbar: Toolbar.Markup,
|
||||
val colorToolbar: Toolbar.Color,
|
||||
val addBlockToolbar: Toolbar.AddBlock
|
||||
val addBlockToolbar: Toolbar.AddBlock,
|
||||
val actionToolbar: Toolbar.BlockAction
|
||||
) {
|
||||
|
||||
sealed class Toolbar {
|
||||
|
||||
/**
|
||||
|
@ -64,21 +69,27 @@ data class ControlPanelState(
|
|||
override val isVisible: Boolean
|
||||
) : Toolbar()
|
||||
|
||||
/**
|
||||
* Basic action (delete, duplicate, undo, redo, etc.) toolbar state
|
||||
*/
|
||||
data class BlockAction(
|
||||
override val isVisible: Boolean
|
||||
) : Toolbar()
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
data class TurnInto(
|
||||
val isVisible: Boolean
|
||||
)
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
data class BlockAction(
|
||||
val isVisible: Boolean
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Block currently associated with this panel.
|
||||
* @property id id of the focused block
|
||||
*/
|
||||
data class Focus(val id: String)
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
|
@ -98,7 +109,11 @@ data class ControlPanelState(
|
|||
),
|
||||
addBlockToolbar = Toolbar.AddBlock(
|
||||
isVisible = false
|
||||
)
|
||||
),
|
||||
actionToolbar = Toolbar.BlockAction(
|
||||
isVisible = false
|
||||
),
|
||||
focus = null
|
||||
)
|
||||
}
|
||||
}
|
|
@ -5,10 +5,14 @@ import android.util.AttributeSet
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.widget.LinearLayout
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.agileburo.anytype.core_ui.R
|
||||
import com.agileburo.anytype.core_ui.extensions.invisible
|
||||
import com.agileburo.anytype.core_ui.extensions.visible
|
||||
import com.agileburo.anytype.core_ui.layout.SpacingItemDecoration
|
||||
import com.agileburo.anytype.core_ui.widgets.toolbar.ActionToolbarWidget.Action
|
||||
import com.agileburo.anytype.core_ui.widgets.toolbar.ActionToolbarWidget.ActionAdapter
|
||||
|
@ -21,6 +25,10 @@ import com.agileburo.anytype.core_ui.widgets.toolbar.ActionToolbarWidget.ActionC
|
|||
import kotlinx.android.synthetic.main.item_toolbar_action.view.*
|
||||
import kotlinx.android.synthetic.main.item_toolbar_action.view.title
|
||||
import kotlinx.android.synthetic.main.widget_action_toolbar.view.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.sendBlocking
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.consumeAsFlow
|
||||
|
||||
/**
|
||||
* This toolbar widget provides user with different types of actions applicable to blocks.
|
||||
|
@ -30,6 +38,8 @@ import kotlinx.android.synthetic.main.widget_action_toolbar.view.*
|
|||
*/
|
||||
class ActionToolbarWidget : LinearLayout {
|
||||
|
||||
private val channel = Channel<Action>()
|
||||
|
||||
constructor(
|
||||
context: Context
|
||||
) : this(context, null)
|
||||
|
@ -48,6 +58,7 @@ class ActionToolbarWidget : LinearLayout {
|
|||
}
|
||||
|
||||
private fun inflate() {
|
||||
// TODO remove redundant linear layout
|
||||
LayoutInflater.from(context).inflate(R.layout.widget_action_toolbar, this)
|
||||
setupAdapter()
|
||||
}
|
||||
|
@ -89,13 +100,24 @@ class ActionToolbarWidget : LinearLayout {
|
|||
Action(ACTION_UNDO),
|
||||
Action(ACTION_REDO)
|
||||
),
|
||||
onActionClicked = { action ->
|
||||
// do nothing
|
||||
}
|
||||
onActionClicked = channel::sendBlocking
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun actionClicks(): Flow<Action> = channel.consumeAsFlow()
|
||||
|
||||
fun show() {
|
||||
layoutParams = LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
||||
visible()
|
||||
}
|
||||
|
||||
fun hide() {
|
||||
layoutParams = LayoutParams(MATCH_PARENT, 0)
|
||||
invisible()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adapter for rendering list of actions
|
||||
* @property actions immutable list of actions
|
||||
|
|
|
@ -8,6 +8,7 @@ import com.agileburo.anytype.core_ui.R
|
|||
import com.agileburo.anytype.core_ui.reactive.clicks
|
||||
import com.agileburo.anytype.core_ui.state.ControlPanelState
|
||||
import com.agileburo.anytype.core_ui.state.ControlPanelState.Toolbar.Block.Action.ADD
|
||||
import com.agileburo.anytype.core_ui.state.ControlPanelState.Toolbar.Block.Action.BLOCK_ACTION
|
||||
import kotlinx.android.synthetic.main.widget_block_toolbar.view.*
|
||||
|
||||
class BlockToolbarWidget : ConstraintLayout {
|
||||
|
@ -35,8 +36,10 @@ class BlockToolbarWidget : ConstraintLayout {
|
|||
|
||||
fun keyboardClicks() = keyboard.clicks()
|
||||
fun addButtonClicks() = add.clicks()
|
||||
fun actionClicks() = actions.clicks()
|
||||
|
||||
fun setState(state: ControlPanelState.Toolbar.Block) {
|
||||
add.isSelected = state.selectedAction == ADD
|
||||
actions.isSelected = state.selectedAction == BLOCK_ACTION
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
<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="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M19,5H15V9H19V5ZM15,3C13.8954,3 13,3.8954 13,5V9C13,10.1046 13.8954,11 15,11H19C20.1046,11 21,10.1046 21,9V5C21,3.8954 20.1046,3 19,3H15Z" />
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M9,5H5V9H9V5ZM5,3C3.8954,3 3,3.8954 3,5V9C3,10.1046 3.8954,11 5,11H9C10.1046,11 11,10.1046 11,9V5C11,3.8954 10.1046,3 9,3H5Z" />
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M19,15.0115H15V19.0115H19V15.0115ZM15,13.0115C13.8954,13.0115 13,13.9069 13,15.0115V19.0115C13,20.116 13.8954,21.0115 15,21.0115H19C20.1046,21.0115 21,20.116 21,19.0115V15.0115C21,13.9069 20.1046,13.0115 19,13.0115H15Z" />
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M9,15.0115H5V19.0115H9V15.0115ZM5,13.0115C3.8954,13.0115 3,13.9069 3,15.0115V19.0115C3,20.116 3.8954,21.0115 5,21.0115H9C10.1046,21.0115 11,20.116 11,19.0115V15.0115C11,13.9069 10.1046,13.0115 9,13.0115H5Z" />
|
||||
</vector>
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/ic_toolbar_actions" android:state_selected="false" />
|
||||
<item android:drawable="@drawable/ic_toolbar_actions_selected" android:state_selected="true" />
|
||||
</selector>
|
|
@ -17,13 +17,19 @@
|
|||
android:textSize="15sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/actionRecycler"
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="13dp"
|
||||
android:layout_marginBottom="13dp"
|
||||
tools:itemCount="1"
|
||||
tools:listitem="@layout/item_toolbar_action" />
|
||||
android:layout_marginBottom="13dp">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/actionRecycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:itemCount="1"
|
||||
tools:listitem="@layout/item_toolbar_action" />
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
</LinearLayout>
|
|
@ -25,6 +25,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:contentDescription="@string/content_description_hide_keyboard_button"
|
||||
android:src="@drawable/ic_toolbar_keyboard"
|
||||
android:stateListAnimator="@animator/scale_shrink"
|
||||
|
@ -38,8 +39,9 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:contentDescription="@string/content_description_show_actions"
|
||||
android:src="@drawable/ic_toolbar_actions"
|
||||
android:src="@drawable/ic_toolbar_actions_selector"
|
||||
android:stateListAnimator="@animator/scale_shrink"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/keyboard"
|
||||
|
@ -51,6 +53,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:contentDescription="@string/content_description_show_colors"
|
||||
android:src="@drawable/ic_toolbar_color"
|
||||
android:stateListAnimator="@animator/scale_shrink"
|
||||
|
|
|
@ -219,6 +219,16 @@ fun Command.Dnd.toEntity(): CommandEntity.Dnd {
|
|||
)
|
||||
}
|
||||
|
||||
fun Command.Unlink.toEntity(): CommandEntity.Unlink = CommandEntity.Unlink(
|
||||
context = context,
|
||||
targets = targets
|
||||
)
|
||||
|
||||
fun Command.Duplicate.toEntity(): CommandEntity.Duplicate = CommandEntity.Duplicate(
|
||||
context = context,
|
||||
original = original
|
||||
)
|
||||
|
||||
fun Position.toEntity(): PositionEntity {
|
||||
return PositionEntity.valueOf(name)
|
||||
}
|
||||
|
@ -249,6 +259,11 @@ fun EventEntity.toDomain(): Event {
|
|||
children = children
|
||||
)
|
||||
}
|
||||
is EventEntity.Command.DeleteBlock -> {
|
||||
Event.Command.DeleteBlock(
|
||||
target = target
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
package com.agileburo.anytype.data.auth.model
|
||||
|
||||
/**
|
||||
* For documentation, please refer to domain models description.
|
||||
*/
|
||||
class CommandEntity {
|
||||
|
||||
class UpdateText(
|
||||
|
@ -29,4 +32,14 @@ class CommandEntity {
|
|||
val blockIds: List<String>,
|
||||
val position: PositionEntity
|
||||
)
|
||||
|
||||
class Duplicate(
|
||||
val context: String,
|
||||
val original: String
|
||||
)
|
||||
|
||||
class Unlink(
|
||||
val context: String,
|
||||
val targets: List<String>
|
||||
)
|
||||
}
|
|
@ -23,5 +23,9 @@ sealed class EventEntity {
|
|||
val id: String,
|
||||
val children: List<String>
|
||||
) : Command()
|
||||
|
||||
data class DeleteBlock(
|
||||
val target: String
|
||||
) : Command()
|
||||
}
|
||||
}
|
|
@ -52,4 +52,11 @@ class BlockDataRepository(
|
|||
override suspend fun dnd(command: Command.Dnd) {
|
||||
factory.remote.dnd(command.toEntity())
|
||||
}
|
||||
|
||||
override suspend fun duplicate(command: Command.Duplicate) =
|
||||
factory.remote.duplicate(command.toEntity())
|
||||
|
||||
override suspend fun unlink(command: Command.Unlink) {
|
||||
factory.remote.unlink(command.toEntity())
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import com.agileburo.anytype.data.auth.model.BlockEntity
|
|||
import com.agileburo.anytype.data.auth.model.CommandEntity
|
||||
import com.agileburo.anytype.data.auth.model.ConfigEntity
|
||||
import com.agileburo.anytype.data.auth.model.EventEntity
|
||||
import com.agileburo.anytype.domain.common.Id
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface BlockDataStore {
|
||||
|
@ -11,6 +12,8 @@ interface BlockDataStore {
|
|||
suspend fun updateText(command: CommandEntity.UpdateText)
|
||||
suspend fun updateCheckbox(command: CommandEntity.UpdateCheckbox)
|
||||
suspend fun dnd(command: CommandEntity.Dnd)
|
||||
suspend fun duplicate(command: CommandEntity.Duplicate): Id
|
||||
suspend fun unlink(command: CommandEntity.Unlink)
|
||||
suspend fun getConfig(): ConfigEntity
|
||||
suspend fun createPage(parentId: String): String
|
||||
suspend fun openPage(id: String)
|
||||
|
|
|
@ -4,6 +4,7 @@ import com.agileburo.anytype.data.auth.model.BlockEntity
|
|||
import com.agileburo.anytype.data.auth.model.CommandEntity
|
||||
import com.agileburo.anytype.data.auth.model.ConfigEntity
|
||||
import com.agileburo.anytype.data.auth.model.EventEntity
|
||||
import com.agileburo.anytype.domain.common.Id
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface BlockRemote {
|
||||
|
@ -11,6 +12,8 @@ interface BlockRemote {
|
|||
suspend fun updateText(command: CommandEntity.UpdateText)
|
||||
suspend fun updateCheckbox(command: CommandEntity.UpdateCheckbox)
|
||||
suspend fun dnd(command: CommandEntity.Dnd)
|
||||
suspend fun duplicate(command: CommandEntity.Duplicate): Id
|
||||
suspend fun unlink(command: CommandEntity.Unlink)
|
||||
suspend fun getConfig(): ConfigEntity
|
||||
suspend fun createPage(parentId: String): String
|
||||
suspend fun openPage(id: String)
|
||||
|
|
|
@ -42,4 +42,10 @@ class BlockRemoteDataStore(private val remote: BlockRemote) : BlockDataStore {
|
|||
override suspend fun dnd(command: CommandEntity.Dnd) {
|
||||
remote.dnd(command)
|
||||
}
|
||||
|
||||
override suspend fun duplicate(command: CommandEntity.Duplicate) = remote.duplicate(command)
|
||||
|
||||
override suspend fun unlink(command: CommandEntity.Unlink) {
|
||||
remote.unlink(command)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package com.agileburo.anytype.domain.block.interactor
|
||||
|
||||
import com.agileburo.anytype.domain.base.BaseUseCase
|
||||
import com.agileburo.anytype.domain.base.Either
|
||||
import com.agileburo.anytype.domain.block.model.Command
|
||||
import com.agileburo.anytype.domain.block.repo.BlockRepository
|
||||
import com.agileburo.anytype.domain.common.Id
|
||||
|
||||
/**
|
||||
* Use-case for block duplication.
|
||||
* Should return id of the new block.
|
||||
*/
|
||||
class DuplicateBlock(private val repo: BlockRepository) :
|
||||
BaseUseCase<Id, DuplicateBlock.Params>() {
|
||||
|
||||
override suspend fun run(params: Params) = try {
|
||||
repo.duplicate(
|
||||
command = Command.Duplicate(
|
||||
context = params.context,
|
||||
original = params.original
|
||||
)
|
||||
).let {
|
||||
Either.Right(it)
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
Either.Left(t)
|
||||
}
|
||||
|
||||
/**
|
||||
* @property context context id
|
||||
* @property original id of the original block id, which we need to duplicate
|
||||
*/
|
||||
data class Params(
|
||||
val context: Id,
|
||||
val original: Id
|
||||
)
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package com.agileburo.anytype.domain.block.interactor
|
||||
|
||||
import com.agileburo.anytype.domain.base.BaseUseCase
|
||||
import com.agileburo.anytype.domain.base.Either
|
||||
import com.agileburo.anytype.domain.block.model.Command
|
||||
import com.agileburo.anytype.domain.block.repo.BlockRepository
|
||||
import com.agileburo.anytype.domain.common.Id
|
||||
|
||||
/**
|
||||
* Use-case for unlinking blocks from its context.
|
||||
* Unlinking is a remplacement for delete operations.
|
||||
*/
|
||||
class UnlinkBlocks(private val repo: BlockRepository) : BaseUseCase<Unit, UnlinkBlocks.Params>() {
|
||||
|
||||
override suspend fun run(params: Params) = try {
|
||||
repo.unlink(
|
||||
command = Command.Unlink(
|
||||
context = params.context,
|
||||
targets = params.targets
|
||||
)
|
||||
).let {
|
||||
Either.Right(it)
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
Either.Left(t)
|
||||
}
|
||||
|
||||
/**
|
||||
* Params for unlinking a set of blocks from its context (i.e. page)
|
||||
* @property context context id
|
||||
* @property targets ids of the blocks, which we need to unlink from the [context]
|
||||
*/
|
||||
data class Params(
|
||||
val context: Id,
|
||||
val targets: List<Id>
|
||||
)
|
||||
}
|
|
@ -1,39 +1,72 @@
|
|||
package com.agileburo.anytype.domain.block.model
|
||||
|
||||
import com.agileburo.anytype.domain.common.Id
|
||||
|
||||
sealed class Command {
|
||||
|
||||
/**
|
||||
* @property contextId context id
|
||||
* @property blockId target block id
|
||||
* @property text updated text
|
||||
* @property marks marks of the updated text
|
||||
*/
|
||||
class UpdateText(
|
||||
val contextId: String,
|
||||
val blockId: String,
|
||||
val contextId: Id,
|
||||
val blockId: Id,
|
||||
val text: String,
|
||||
val marks: List<Block.Content.Text.Mark>
|
||||
)
|
||||
|
||||
/**
|
||||
* @property context context id
|
||||
* @property target id of the target checkbox block
|
||||
* @property isChecked new checked/unchecked state for this checkbox block
|
||||
*/
|
||||
class UpdateCheckbox(
|
||||
val context: String,
|
||||
val target: String,
|
||||
val context: Id,
|
||||
val target: Id,
|
||||
val isChecked: Boolean
|
||||
)
|
||||
|
||||
/**
|
||||
* Params for creating a block
|
||||
* Command for creating a block
|
||||
* @property contextId id of the context of the block (i.e. page, dashboard or something else)
|
||||
* @property targetId id of the block associated with the block we need to create
|
||||
* @property position position of the block that we need to create in relation with the target block
|
||||
* @property prototype a prototype of the block we would like to create
|
||||
*/
|
||||
class Create(
|
||||
val contextId: String,
|
||||
val targetId: String,
|
||||
val contextId: Id,
|
||||
val targetId: Id,
|
||||
val position: Position,
|
||||
val prototype: Block.Prototype
|
||||
)
|
||||
|
||||
class Dnd(
|
||||
val contextId: String,
|
||||
val targetId: String,
|
||||
val targetContextId: String,
|
||||
val contextId: Id,
|
||||
val targetId: Id,
|
||||
val targetContextId: Id,
|
||||
val blockIds: List<String>,
|
||||
val position: Position
|
||||
)
|
||||
|
||||
/**
|
||||
* Command for block duplication
|
||||
* @property context context id
|
||||
* @property original id of the original block, which we need to duplicate
|
||||
*/
|
||||
class Duplicate(
|
||||
val context: Id,
|
||||
val original: Id
|
||||
)
|
||||
|
||||
/**
|
||||
* Command for unlinking a set of blocks from its context (i.e. page)
|
||||
* @property context context id
|
||||
* @property targets ids of the blocks, which we need to unlink from its [context]
|
||||
*/
|
||||
class Unlink(
|
||||
val context: Id,
|
||||
val targets: List<Id>
|
||||
)
|
||||
}
|
|
@ -9,6 +9,8 @@ import kotlinx.coroutines.flow.Flow
|
|||
|
||||
interface BlockRepository {
|
||||
suspend fun dnd(command: Command.Dnd)
|
||||
suspend fun duplicate(command: Command.Duplicate): Id
|
||||
suspend fun unlink(command: Command.Unlink)
|
||||
suspend fun create(command: Command.Create)
|
||||
suspend fun updateText(command: Command.UpdateText)
|
||||
suspend fun updateCheckbox(command: Command.UpdateCheckbox)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.agileburo.anytype.domain.event.model
|
||||
|
||||
import com.agileburo.anytype.domain.block.model.Block
|
||||
import com.agileburo.anytype.domain.common.Id
|
||||
|
||||
sealed class Event {
|
||||
|
||||
|
@ -15,6 +16,10 @@ sealed class Event {
|
|||
val blocks: List<Block>
|
||||
) : Command()
|
||||
|
||||
data class DeleteBlock(
|
||||
val target: Id
|
||||
) : Command()
|
||||
|
||||
data class UpdateBlockText(
|
||||
val id: String,
|
||||
val text: String
|
||||
|
|
|
@ -24,7 +24,8 @@ class BlockMiddleware(
|
|||
Events.Event.Message.ValueCase.BLOCKSHOW,
|
||||
Events.Event.Message.ValueCase.BLOCKADD,
|
||||
Events.Event.Message.ValueCase.BLOCKSETTEXT,
|
||||
Events.Event.Message.ValueCase.BLOCKSETCHILDRENIDS
|
||||
Events.Event.Message.ValueCase.BLOCKSETCHILDRENIDS,
|
||||
Events.Event.Message.ValueCase.BLOCKDELETE
|
||||
)
|
||||
|
||||
private val supportedTextStyles = listOf(
|
||||
|
@ -180,6 +181,11 @@ class BlockMiddleware(
|
|||
text = event.blockSetText.text.value
|
||||
)
|
||||
}
|
||||
Events.Event.Message.ValueCase.BLOCKDELETE -> {
|
||||
EventEntity.Command.DeleteBlock(
|
||||
target = event.blockDelete.blockId
|
||||
)
|
||||
}
|
||||
Events.Event.Message.ValueCase.BLOCKSETCHILDRENIDS -> {
|
||||
EventEntity.Command.UpdateStructure(
|
||||
id = event.blockSetChildrenIds.id,
|
||||
|
@ -348,6 +354,13 @@ class BlockMiddleware(
|
|||
middleware.dnd(command)
|
||||
}
|
||||
|
||||
override suspend fun duplicate(command: CommandEntity.Duplicate): String =
|
||||
middleware.duplicate(command)
|
||||
|
||||
override suspend fun unlink(command: CommandEntity.Unlink) {
|
||||
middleware.unlink(command)
|
||||
}
|
||||
|
||||
private fun extractDashboard(block: Models.Block): BlockEntity.Content.Dashboard {
|
||||
return BlockEntity.Content.Dashboard(
|
||||
type = when {
|
||||
|
|
|
@ -45,7 +45,6 @@ public class Middleware {
|
|||
}
|
||||
|
||||
public CreateAccountResponse createAccount(String name, String path) throws Exception {
|
||||
|
||||
Account.Create.Request request;
|
||||
|
||||
if (path != null) {
|
||||
|
@ -178,7 +177,6 @@ public class Middleware {
|
|||
String text,
|
||||
List<Models.Block.Content.Text.Mark> marks
|
||||
) throws Exception {
|
||||
|
||||
Timber.d("Updating block with the follwing text:\n%s", text);
|
||||
|
||||
Models.Block.Content.Text.Marks markup = Models.Block.Content.Text.Marks
|
||||
|
@ -220,7 +218,6 @@ public class Middleware {
|
|||
PositionEntity position,
|
||||
BlockEntity.Prototype prototype
|
||||
) throws Exception {
|
||||
|
||||
Models.Block.Content.Text contentModel = null;
|
||||
|
||||
if (prototype instanceof BlockEntity.Prototype.Text) {
|
||||
|
@ -345,7 +342,6 @@ public class Middleware {
|
|||
}
|
||||
|
||||
public void dnd(CommandEntity.Dnd command) throws Exception {
|
||||
|
||||
Models.Block.Position positionModel = null;
|
||||
|
||||
switch (command.getPosition()) {
|
||||
|
@ -379,4 +375,32 @@ public class Middleware {
|
|||
|
||||
service.blockListMove(request);
|
||||
}
|
||||
|
||||
public String duplicate(CommandEntity.Duplicate command) throws Exception {
|
||||
Block.Duplicate.Request request = Block.Duplicate.Request
|
||||
.newBuilder()
|
||||
.setContextId(command.getContext())
|
||||
.setTargetId(command.getOriginal())
|
||||
.setBlockId(command.getOriginal())
|
||||
.setPosition(Models.Block.Position.Bottom)
|
||||
.build();
|
||||
|
||||
Timber.d("Duplicating blocks with the following request:\n%s", request.toString());
|
||||
|
||||
Block.Duplicate.Response response = service.blockDuplicate(request);
|
||||
|
||||
return response.getBlockId();
|
||||
}
|
||||
|
||||
public void unlink(CommandEntity.Unlink command) throws Exception {
|
||||
Block.Unlink.Request request = Block.Unlink.Request
|
||||
.newBuilder()
|
||||
.setContextId(command.getContext())
|
||||
.addAllBlockIds(command.getTargets())
|
||||
.build();
|
||||
|
||||
Timber.d("Unlinking blocks with the following request:\n%s", request.toString());
|
||||
|
||||
service.blockUnlink(request);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -152,4 +152,26 @@ public class DefaultMiddlewareService implements MiddlewareService {
|
|||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Block.Unlink.Response blockUnlink(Block.Unlink.Request request) throws Exception {
|
||||
byte[] encoded = Lib.blockUnlink(request.toByteArray());
|
||||
Block.Unlink.Response response = Block.Unlink.Response.parseFrom(encoded);
|
||||
if (response.getError() != null && response.getError().getCode() != Block.Unlink.Response.Error.Code.NULL) {
|
||||
throw new Exception(response.getError().getDescription());
|
||||
} else {
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Block.Duplicate.Response blockDuplicate(Block.Duplicate.Request request) throws Exception {
|
||||
byte[] encoded = Lib.blockDuplicate(request.toByteArray());
|
||||
Block.Duplicate.Response response = Block.Duplicate.Response.parseFrom(encoded);
|
||||
if (response.getError() != null && response.getError().getCode() != Block.Duplicate.Response.Error.Code.NULL) {
|
||||
throw new Exception(response.getError().getDescription());
|
||||
} else {
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,4 +36,8 @@ public interface MiddlewareService {
|
|||
Block.Set.Text.Checked.Response blockSetTextChecked(Block.Set.Text.Checked.Request request) throws Exception;
|
||||
|
||||
BlockList.Move.Response blockListMove(BlockList.Move.Request request) throws Exception;
|
||||
|
||||
Block.Unlink.Response blockUnlink(Block.Unlink.Request request) throws Exception;
|
||||
|
||||
Block.Duplicate.Response blockDuplicate(Block.Duplicate.Request request) throws Exception;
|
||||
}
|
||||
|
|
|
@ -9,12 +9,11 @@ import com.agileburo.anytype.core_ui.state.ControlPanelState.Toolbar
|
|||
import com.agileburo.anytype.core_utils.common.EventWrapper
|
||||
import com.agileburo.anytype.core_utils.ext.withLatestFrom
|
||||
import com.agileburo.anytype.core_utils.ui.ViewStateViewModel
|
||||
import com.agileburo.anytype.domain.block.interactor.CreateBlock
|
||||
import com.agileburo.anytype.domain.block.interactor.UpdateBlock
|
||||
import com.agileburo.anytype.domain.block.interactor.UpdateCheckbox
|
||||
import com.agileburo.anytype.domain.block.interactor.*
|
||||
import com.agileburo.anytype.domain.block.model.Block
|
||||
import com.agileburo.anytype.domain.block.model.Block.Prototype
|
||||
import com.agileburo.anytype.domain.block.model.Position
|
||||
import com.agileburo.anytype.domain.common.Id
|
||||
import com.agileburo.anytype.domain.event.interactor.ObserveEvents
|
||||
import com.agileburo.anytype.domain.event.model.Event
|
||||
import com.agileburo.anytype.domain.ext.addMark
|
||||
|
@ -40,7 +39,9 @@ class PageViewModel(
|
|||
private val updateBlock: UpdateBlock,
|
||||
private val createBlock: CreateBlock,
|
||||
private val observeEvents: ObserveEvents,
|
||||
private val updateCheckbox: UpdateCheckbox
|
||||
private val updateCheckbox: UpdateCheckbox,
|
||||
private val unlinkBlocks: UnlinkBlocks,
|
||||
private val duplicateBlock: DuplicateBlock
|
||||
) : ViewStateViewModel<PageViewModel.ViewState>(),
|
||||
SupportNavigation<EventWrapper<AppNavigation.Command>> {
|
||||
|
||||
|
@ -50,13 +51,13 @@ class PageViewModel(
|
|||
private val renderingChannel = Channel<List<Block>>()
|
||||
private val renderings = renderingChannel.consumeAsFlow()
|
||||
|
||||
private val focusChannel = ConflatedBroadcastChannel(DEFAULT_FOCUS_ID)
|
||||
private val focusChannel = ConflatedBroadcastChannel(EMPTY_FOCUS_ID)
|
||||
private val focusChanges = focusChannel.asFlow()
|
||||
|
||||
private val textChannel = Channel<Triple<String, String, List<Block.Content.Text.Mark>>>()
|
||||
private val textChannel = Channel<Triple<Id, String, List<Block.Content.Text.Mark>>>()
|
||||
private val textChanges = textChannel.consumeAsFlow()
|
||||
|
||||
private val selectionChannel = Channel<Pair<String, IntRange>>()
|
||||
private val selectionChannel = Channel<Pair<Id, IntRange>>()
|
||||
private val selectionsChanges = selectionChannel.consumeAsFlow()
|
||||
|
||||
private val markupActionChannel = Channel<MarkupAction>()
|
||||
|
@ -66,7 +67,12 @@ class PageViewModel(
|
|||
* Currently opened page id.
|
||||
*/
|
||||
private var pageId: String = ""
|
||||
|
||||
/**
|
||||
* Current set of blocks on this page.
|
||||
*/
|
||||
var blocks: List<Block> = emptyList()
|
||||
private set
|
||||
|
||||
override val navigation = MutableLiveData<EventWrapper<AppNavigation.Command>>()
|
||||
|
||||
|
@ -86,7 +92,8 @@ class PageViewModel(
|
|||
|
||||
private fun startProcessingControlPanelViewState() {
|
||||
viewModelScope.launch {
|
||||
controlPanelInteractor.state().collect { controlPanelViewState.postValue(it) }
|
||||
controlPanelInteractor.state().distinctUntilChanged()
|
||||
.collect { controlPanelViewState.postValue(it) }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -110,14 +117,22 @@ class PageViewModel(
|
|||
)
|
||||
}
|
||||
is Event.Command.AddBlock -> {
|
||||
blocks = blocks + event.blocks
|
||||
viewModelScope.launch {
|
||||
focusChannel.send(event.blocks.last().id)
|
||||
renderingChannel.send(blocks)
|
||||
}
|
||||
}
|
||||
is Event.Command.UpdateStructure -> {
|
||||
blocks = blocks.map { block ->
|
||||
if (block.id == pageId)
|
||||
block.copy(
|
||||
children = block.children + event.blocks.map { it.id }
|
||||
)
|
||||
if (block.id == event.id)
|
||||
block.copy(children = event.children)
|
||||
else
|
||||
block
|
||||
} + event.blocks
|
||||
}
|
||||
}
|
||||
is Event.Command.DeleteBlock -> {
|
||||
blocks = blocks.filter { it.id != event.target }
|
||||
viewModelScope.launch { renderingChannel.send(blocks) }
|
||||
}
|
||||
}
|
||||
|
@ -131,7 +146,6 @@ class PageViewModel(
|
|||
.filter { (_, selection) -> selection.first != selection.last }
|
||||
) { a, b -> Pair(a, b) }
|
||||
.onEach { (action, selection) ->
|
||||
focusChannel.send(selection.first)
|
||||
applyMarkup(selection, action)
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
@ -212,7 +226,6 @@ class PageViewModel(
|
|||
textChanges
|
||||
.debounce(TEXT_CHANGES_DEBOUNCE_DURATION)
|
||||
.map { (id, text, marks) ->
|
||||
|
||||
val update = blocks.map { block ->
|
||||
if (block.id == id) {
|
||||
block.copy(
|
||||
|
@ -239,9 +252,7 @@ class PageViewModel(
|
|||
}
|
||||
|
||||
private fun proceedWithUpdatingBlock(params: UpdateBlock.Params) {
|
||||
|
||||
Timber.d("Starting updating block with params: $params")
|
||||
|
||||
updateBlock.invoke(viewModelScope, params) { result ->
|
||||
result.either(
|
||||
fnL = { Timber.e(it, "Error while updating text: $params") },
|
||||
|
@ -281,6 +292,10 @@ class PageViewModel(
|
|||
controlPanelInteractor.onEvent(ControlPanelMachine.Event.OnSelectionChanged(selection))
|
||||
}
|
||||
|
||||
fun onBlockFocusChanged(id: String, hasFocus: Boolean) {
|
||||
if (hasFocus) viewModelScope.launch { focusChannel.send(id) }
|
||||
}
|
||||
|
||||
fun onMarkupActionClicked(markup: Markup.Type) {
|
||||
viewModelScope.launch {
|
||||
markupActionChannel.send(MarkupAction(type = markup))
|
||||
|
@ -303,6 +318,48 @@ class PageViewModel(
|
|||
controlPanelInteractor.onEvent(ControlPanelMachine.Event.OnMarkupToolbarColorClicked)
|
||||
}
|
||||
|
||||
fun onActionDeleteClicked() {
|
||||
viewModelScope.launch {
|
||||
focusChanges
|
||||
.take(1)
|
||||
.collect { focus ->
|
||||
unlinkBlocks.invoke(
|
||||
scope = this,
|
||||
params = UnlinkBlocks.Params(
|
||||
context = pageId,
|
||||
targets = listOf(focus)
|
||||
)
|
||||
) { result ->
|
||||
result.either(
|
||||
fnL = { Timber.e(it, "Error while unlinking block with id: $focus") },
|
||||
fnR = { Timber.d("Succesfully unlinked block with id: $focus") }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onActionDuplicateClicked() {
|
||||
viewModelScope.launch {
|
||||
focusChanges
|
||||
.take(1)
|
||||
.collect { focus ->
|
||||
duplicateBlock.invoke(
|
||||
scope = this,
|
||||
params = DuplicateBlock.Params(
|
||||
context = pageId,
|
||||
original = focus
|
||||
)
|
||||
) { result ->
|
||||
result.either(
|
||||
fnL = { Timber.e(it, "Error while duplicating block with id: $focus") },
|
||||
fnR = { Timber.d("Succesfully duplicated block with id: $focus") }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onAddTextBlockClicked(style: Block.Content.Text.Style) {
|
||||
controlPanelInteractor.onEvent(ControlPanelMachine.Event.OnOptionSelected)
|
||||
createBlock.invoke(
|
||||
|
@ -341,6 +398,10 @@ class PageViewModel(
|
|||
controlPanelInteractor.onEvent(ControlPanelMachine.Event.OnAddBlockToolbarClicked)
|
||||
}
|
||||
|
||||
fun onActionToolbarClicked() {
|
||||
controlPanelInteractor.onEvent(ControlPanelMachine.Event.OnActionToolbarClicked)
|
||||
}
|
||||
|
||||
sealed class ViewState {
|
||||
object Loading : ViewState()
|
||||
data class Success(val blocks: List<BlockView>) : ViewState()
|
||||
|
@ -348,7 +409,7 @@ class PageViewModel(
|
|||
}
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_FOCUS_ID = ""
|
||||
const val EMPTY_FOCUS_ID = ""
|
||||
const val TEXT_CHANGES_DEBOUNCE_DURATION = 500L
|
||||
}
|
||||
|
||||
|
@ -411,6 +472,11 @@ class PageViewModel(
|
|||
* Represents an event when user selected a text color on [Toolbar.Color] toolbar.
|
||||
*/
|
||||
object OnTextColorSelected : Event()
|
||||
|
||||
/**
|
||||
* Represents an event when user selected an action toolbar on [Toolbar.Block]
|
||||
*/
|
||||
object OnActionToolbarClicked : Event()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -419,13 +485,35 @@ class PageViewModel(
|
|||
class Reducer : StateReducer<ControlPanelState, Event> {
|
||||
|
||||
override val function: suspend (ControlPanelState, Event) -> ControlPanelState
|
||||
get() = { state, event -> reduce(state, event) }
|
||||
get() = { state, event ->
|
||||
reduce(
|
||||
state,
|
||||
event
|
||||
).also { Timber.d("Reducing event:\n$event") }
|
||||
}
|
||||
|
||||
override suspend fun reduce(state: ControlPanelState, event: Event) = when (event) {
|
||||
is Event.OnSelectionChanged -> state.copy(
|
||||
markupToolbar = state.markupToolbar.copy(
|
||||
isVisible = event.selection.first != event.selection.last
|
||||
)
|
||||
isVisible = event.selection.first != event.selection.last,
|
||||
selectedAction = if (event.selection.first != event.selection.last)
|
||||
state.markupToolbar.selectedAction
|
||||
else
|
||||
null
|
||||
),
|
||||
blockToolbar = state.blockToolbar.copy(
|
||||
selectedAction = null
|
||||
),
|
||||
actionToolbar = state.actionToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
addBlockToolbar = state.addBlockToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
colorToolbar = if (event.selection.first != event.selection.last)
|
||||
state.colorToolbar.copy()
|
||||
else
|
||||
state.colorToolbar.copy(isVisible = false)
|
||||
)
|
||||
is Event.OnMarkupToolbarColorClicked -> state.copy(
|
||||
colorToolbar = state.colorToolbar.copy(
|
||||
|
@ -455,6 +543,9 @@ class PageViewModel(
|
|||
Toolbar.Block.Action.ADD
|
||||
else
|
||||
null
|
||||
),
|
||||
actionToolbar = state.actionToolbar.copy(
|
||||
isVisible = false
|
||||
)
|
||||
)
|
||||
is Event.OnOptionSelected -> state.copy(
|
||||
|
@ -465,6 +556,22 @@ class PageViewModel(
|
|||
selectedAction = null
|
||||
)
|
||||
)
|
||||
is Event.OnActionToolbarClicked -> state.copy(
|
||||
colorToolbar = state.colorToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
addBlockToolbar = state.addBlockToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
actionToolbar = state.actionToolbar.copy(
|
||||
isVisible = !state.actionToolbar.isVisible
|
||||
),
|
||||
blockToolbar = state.blockToolbar.copy(
|
||||
selectedAction = if (state.actionToolbar.isVisible)
|
||||
null
|
||||
else Toolbar.Block.Action.BLOCK_ACTION
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,7 @@ package com.agileburo.anytype.presentation.page
|
|||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.agileburo.anytype.domain.block.interactor.CreateBlock
|
||||
import com.agileburo.anytype.domain.block.interactor.UpdateBlock
|
||||
import com.agileburo.anytype.domain.block.interactor.UpdateCheckbox
|
||||
import com.agileburo.anytype.domain.block.interactor.*
|
||||
import com.agileburo.anytype.domain.event.interactor.ObserveEvents
|
||||
import com.agileburo.anytype.domain.page.ClosePage
|
||||
import com.agileburo.anytype.domain.page.OpenPage
|
||||
|
@ -15,7 +13,9 @@ class PageViewModelFactory(
|
|||
private val updateBlock: UpdateBlock,
|
||||
private val createBlock: CreateBlock,
|
||||
private val observeEvents: ObserveEvents,
|
||||
private val updateCheckbox: UpdateCheckbox
|
||||
private val updateCheckbox: UpdateCheckbox,
|
||||
private val unlinkBlocks: UnlinkBlocks,
|
||||
private val duplicateBlock: DuplicateBlock
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
|
@ -26,7 +26,9 @@ class PageViewModelFactory(
|
|||
updateBlock = updateBlock,
|
||||
createBlock = createBlock,
|
||||
observeEvents = observeEvents,
|
||||
updateCheckbox = updateCheckbox
|
||||
updateCheckbox = updateCheckbox,
|
||||
duplicateBlock = duplicateBlock,
|
||||
unlinkBlocks = unlinkBlocks
|
||||
) as T
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package com.agileburo.anytype.presentation
|
||||
|
||||
import MockDataFactory
|
||||
import com.agileburo.anytype.domain.block.model.Block
|
||||
|
||||
object MockBlockFactory {
|
||||
|
||||
fun makeOnePageWithOneTextBlock(
|
||||
root: String,
|
||||
child: String,
|
||||
style: Block.Content.Text.Style = Block.Content.Text.Style.P
|
||||
) = listOf(
|
||||
Block(
|
||||
id = root,
|
||||
fields = Block.Fields(emptyMap()),
|
||||
content = Block.Content.Page(
|
||||
style = Block.Content.Page.Style.SET
|
||||
),
|
||||
children = listOf(child)
|
||||
),
|
||||
Block(
|
||||
id = child,
|
||||
fields = Block.Fields(emptyMap()),
|
||||
content = Block.Content.Text(
|
||||
text = MockDataFactory.randomString(),
|
||||
marks = emptyList(),
|
||||
style = style
|
||||
),
|
||||
children = emptyList()
|
||||
)
|
||||
)
|
||||
|
||||
fun makeOnePageWithTwoTextBlocks(
|
||||
root: String,
|
||||
firstChild: String,
|
||||
firstChildStyle: Block.Content.Text.Style = Block.Content.Text.Style.P,
|
||||
secondChild: String,
|
||||
secondChildStyle: Block.Content.Text.Style = Block.Content.Text.Style.P
|
||||
) = listOf(
|
||||
Block(
|
||||
id = root,
|
||||
fields = Block.Fields(emptyMap()),
|
||||
content = Block.Content.Page(
|
||||
style = Block.Content.Page.Style.SET
|
||||
),
|
||||
children = listOf(firstChild, secondChild)
|
||||
),
|
||||
Block(
|
||||
id = firstChild,
|
||||
fields = Block.Fields(emptyMap()),
|
||||
content = Block.Content.Text(
|
||||
text = MockDataFactory.randomString(),
|
||||
marks = emptyList(),
|
||||
style = firstChildStyle
|
||||
),
|
||||
children = emptyList()
|
||||
),
|
||||
Block(
|
||||
id = secondChild,
|
||||
fields = Block.Fields(emptyMap()),
|
||||
content = Block.Content.Text(
|
||||
text = MockDataFactory.randomString(),
|
||||
marks = emptyList(),
|
||||
style = secondChildStyle
|
||||
),
|
||||
children = emptyList()
|
||||
)
|
||||
)
|
||||
}
|
|
@ -5,14 +5,13 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
|||
import com.agileburo.anytype.core_ui.common.Markup
|
||||
import com.agileburo.anytype.core_ui.state.ControlPanelState
|
||||
import com.agileburo.anytype.domain.base.Either
|
||||
import com.agileburo.anytype.domain.block.interactor.CreateBlock
|
||||
import com.agileburo.anytype.domain.block.interactor.UpdateBlock
|
||||
import com.agileburo.anytype.domain.block.interactor.UpdateCheckbox
|
||||
import com.agileburo.anytype.domain.block.interactor.*
|
||||
import com.agileburo.anytype.domain.block.model.Block
|
||||
import com.agileburo.anytype.domain.event.interactor.ObserveEvents
|
||||
import com.agileburo.anytype.domain.event.model.Event
|
||||
import com.agileburo.anytype.domain.page.ClosePage
|
||||
import com.agileburo.anytype.domain.page.OpenPage
|
||||
import com.agileburo.anytype.presentation.MockBlockFactory
|
||||
import com.agileburo.anytype.presentation.mapper.toView
|
||||
import com.agileburo.anytype.presentation.navigation.AppNavigation
|
||||
import com.agileburo.anytype.presentation.page.PageViewModel.ViewState
|
||||
|
@ -57,6 +56,12 @@ class PageViewModelTest {
|
|||
@Mock
|
||||
lateinit var updateCheckbox: UpdateCheckbox
|
||||
|
||||
@Mock
|
||||
lateinit var duplicateBlock: DuplicateBlock
|
||||
|
||||
@Mock
|
||||
lateinit var unlinkBlocks: UnlinkBlocks
|
||||
|
||||
private lateinit var vm: PageViewModel
|
||||
|
||||
@Before
|
||||
|
@ -302,6 +307,13 @@ class PageViewModelTest {
|
|||
)
|
||||
)
|
||||
delay(100)
|
||||
emit(
|
||||
Event.Command.UpdateStructure(
|
||||
context = root,
|
||||
id = root,
|
||||
children = listOf(child, added.id)
|
||||
)
|
||||
)
|
||||
emit(
|
||||
Event.Command.AddBlock(
|
||||
blocks = listOf(added)
|
||||
|
@ -322,7 +334,7 @@ class PageViewModelTest {
|
|||
|
||||
coroutineTestRule.advanceTime(200)
|
||||
|
||||
val expected = ViewState.Success(listOf(page.last().toView(), added.toView()))
|
||||
val expected = ViewState.Success(listOf(page.last().toView(), added.toView(focused = true)))
|
||||
|
||||
vm.state.test().assertValue(expected)
|
||||
}
|
||||
|
@ -479,6 +491,11 @@ class PageViewModelTest {
|
|||
val firstTimeRange = 0..3
|
||||
val firstTimeMarkup = Markup.Type.BOLD
|
||||
|
||||
vm.onBlockFocusChanged(
|
||||
hasFocus = true,
|
||||
id = paragraph.id
|
||||
)
|
||||
|
||||
vm.onSelectionChanged(
|
||||
id = paragraph.id,
|
||||
selection = firstTimeRange
|
||||
|
@ -579,20 +596,18 @@ class PageViewModelTest {
|
|||
paragraph
|
||||
)
|
||||
|
||||
observeEvents.stub {
|
||||
onBlocking { build() } doReturn flow {
|
||||
delay(100)
|
||||
emit(
|
||||
Event.Command.ShowBlock(
|
||||
rootId = root,
|
||||
blocks = blocks
|
||||
)
|
||||
val events = flow {
|
||||
delay(100)
|
||||
emit(
|
||||
Event.Command.ShowBlock(
|
||||
rootId = root,
|
||||
blocks = blocks
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
stubObserveEvents(events)
|
||||
stubOpenPage()
|
||||
|
||||
buildViewModel()
|
||||
|
||||
vm.open(root)
|
||||
|
@ -602,6 +617,11 @@ class PageViewModelTest {
|
|||
val firstTimeRange = 0..3
|
||||
val firstTimeMarkup = Markup.Type.BOLD
|
||||
|
||||
vm.onBlockFocusChanged(
|
||||
hasFocus = true,
|
||||
id = paragraph.id
|
||||
)
|
||||
|
||||
vm.onSelectionChanged(
|
||||
id = paragraph.id,
|
||||
selection = firstTimeRange
|
||||
|
@ -707,20 +727,18 @@ class PageViewModelTest {
|
|||
paragraph
|
||||
)
|
||||
|
||||
observeEvents.stub {
|
||||
onBlocking { build() } doReturn flow {
|
||||
delay(100)
|
||||
emit(
|
||||
Event.Command.ShowBlock(
|
||||
rootId = root,
|
||||
blocks = blocks
|
||||
)
|
||||
val events = flow {
|
||||
delay(100)
|
||||
emit(
|
||||
Event.Command.ShowBlock(
|
||||
rootId = root,
|
||||
blocks = blocks
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
stubObserveEvents(events)
|
||||
stubOpenPage()
|
||||
|
||||
buildViewModel()
|
||||
|
||||
vm.open(root)
|
||||
|
@ -790,20 +808,18 @@ class PageViewModelTest {
|
|||
paragraph
|
||||
)
|
||||
|
||||
observeEvents.stub {
|
||||
onBlocking { build() } doReturn flow {
|
||||
delay(100)
|
||||
emit(
|
||||
Event.Command.ShowBlock(
|
||||
rootId = root,
|
||||
blocks = blocks
|
||||
)
|
||||
val events = flow {
|
||||
delay(100)
|
||||
emit(
|
||||
Event.Command.ShowBlock(
|
||||
rootId = root,
|
||||
blocks = blocks
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
stubObserveEvents(events)
|
||||
stubOpenPage()
|
||||
|
||||
buildViewModel()
|
||||
|
||||
val testObserver = vm.state.test()
|
||||
|
@ -881,18 +897,17 @@ class PageViewModelTest {
|
|||
paragraph
|
||||
)
|
||||
|
||||
observeEvents.stub {
|
||||
onBlocking { build() } doReturn flow {
|
||||
delay(100)
|
||||
emit(
|
||||
Event.Command.ShowBlock(
|
||||
rootId = root,
|
||||
blocks = blocks
|
||||
)
|
||||
val events = flow {
|
||||
delay(100)
|
||||
emit(
|
||||
Event.Command.ShowBlock(
|
||||
rootId = root,
|
||||
blocks = blocks
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
stubObserveEvents(events)
|
||||
stubOpenPage()
|
||||
|
||||
buildViewModel()
|
||||
|
@ -1035,6 +1050,9 @@ class PageViewModelTest {
|
|||
),
|
||||
markupToolbar = ControlPanelState.Toolbar.Markup(
|
||||
isVisible = false
|
||||
),
|
||||
actionToolbar = ControlPanelState.Toolbar.BlockAction(
|
||||
isVisible = false
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -1062,6 +1080,9 @@ class PageViewModelTest {
|
|||
),
|
||||
markupToolbar = ControlPanelState.Toolbar.Markup(
|
||||
isVisible = false
|
||||
),
|
||||
actionToolbar = ControlPanelState.Toolbar.BlockAction(
|
||||
isVisible = false
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -1093,6 +1114,9 @@ class PageViewModelTest {
|
|||
),
|
||||
markupToolbar = ControlPanelState.Toolbar.Markup(
|
||||
isVisible = expectedVisibility
|
||||
),
|
||||
actionToolbar = ControlPanelState.Toolbar.BlockAction(
|
||||
isVisible = false
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -1124,6 +1148,9 @@ class PageViewModelTest {
|
|||
),
|
||||
markupToolbar = ControlPanelState.Toolbar.Markup(
|
||||
isVisible = expectedVisibility
|
||||
),
|
||||
actionToolbar = ControlPanelState.Toolbar.BlockAction(
|
||||
isVisible = false
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -1158,6 +1185,9 @@ class PageViewModelTest {
|
|||
markupToolbar = ControlPanelState.Toolbar.Markup(
|
||||
isVisible = true,
|
||||
selectedAction = ControlPanelState.Toolbar.Markup.Action.COLOR
|
||||
),
|
||||
actionToolbar = ControlPanelState.Toolbar.BlockAction(
|
||||
isVisible = false
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -1169,33 +1199,7 @@ class PageViewModelTest {
|
|||
|
||||
val root = MockDataFactory.randomUuid()
|
||||
val child = MockDataFactory.randomUuid()
|
||||
|
||||
val page = listOf(
|
||||
Block(
|
||||
id = root,
|
||||
fields = Block.Fields(emptyMap()),
|
||||
content = Block.Content.Page(
|
||||
style = Block.Content.Page.Style.SET
|
||||
),
|
||||
children = listOf(child)
|
||||
),
|
||||
Block(
|
||||
id = child,
|
||||
fields = Block.Fields(emptyMap()),
|
||||
content = Block.Content.Text(
|
||||
text = MockDataFactory.randomString(),
|
||||
marks = emptyList(),
|
||||
style = Block.Content.Text.Style.P
|
||||
),
|
||||
children = emptyList()
|
||||
)
|
||||
)
|
||||
|
||||
openPage.stub {
|
||||
onBlocking { invoke(any(), any(), any()) } doAnswer { answer ->
|
||||
answer.getArgument<(Either<Throwable, Unit>) -> Unit>(2)(Either.Right(Unit))
|
||||
}
|
||||
}
|
||||
val page = MockBlockFactory.makeOnePageWithOneTextBlock(root = root, child = child)
|
||||
|
||||
val style = Block.Content.Text.Style.H1
|
||||
|
||||
|
@ -1219,6 +1223,13 @@ class PageViewModelTest {
|
|||
)
|
||||
)
|
||||
delay(500)
|
||||
emit(
|
||||
Event.Command.UpdateStructure(
|
||||
context = root,
|
||||
id = root,
|
||||
children = listOf(child, new.id)
|
||||
)
|
||||
)
|
||||
emit(
|
||||
Event.Command.AddBlock(
|
||||
blocks = listOf(new)
|
||||
|
@ -1226,15 +1237,8 @@ class PageViewModelTest {
|
|||
)
|
||||
}
|
||||
|
||||
observeEvents.stub {
|
||||
onBlocking { build() } doReturn flow
|
||||
}
|
||||
|
||||
openPage.stub {
|
||||
onBlocking { invoke(any(), any(), any()) } doAnswer { answer ->
|
||||
answer.getArgument<(Either<Throwable, Unit>) -> Unit>(2)(Either.Right(Unit))
|
||||
}
|
||||
}
|
||||
stubObserveEvents(flow)
|
||||
stubOpenPage()
|
||||
|
||||
buildViewModel()
|
||||
|
||||
|
@ -1268,33 +1272,12 @@ class PageViewModelTest {
|
|||
val root = MockDataFactory.randomUuid()
|
||||
val child = MockDataFactory.randomUuid()
|
||||
|
||||
val page = listOf(
|
||||
Block(
|
||||
id = root,
|
||||
fields = Block.Fields(emptyMap()),
|
||||
content = Block.Content.Page(
|
||||
style = Block.Content.Page.Style.SET
|
||||
),
|
||||
children = listOf(child)
|
||||
),
|
||||
Block(
|
||||
id = child,
|
||||
fields = Block.Fields(emptyMap()),
|
||||
content = Block.Content.Text(
|
||||
text = MockDataFactory.randomString(),
|
||||
marks = emptyList(),
|
||||
style = Block.Content.Text.Style.CHECKBOX
|
||||
),
|
||||
children = emptyList()
|
||||
)
|
||||
val page = MockBlockFactory.makeOnePageWithOneTextBlock(
|
||||
root = root,
|
||||
child = child,
|
||||
style = Block.Content.Text.Style.CHECKBOX
|
||||
)
|
||||
|
||||
openPage.stub {
|
||||
onBlocking { invoke(any(), any(), any()) } doAnswer { answer ->
|
||||
answer.getArgument<(Either<Throwable, Unit>) -> Unit>(2)(Either.Right(Unit))
|
||||
}
|
||||
}
|
||||
|
||||
val flow: Flow<Event.Command> = flow {
|
||||
delay(1000)
|
||||
emit(
|
||||
|
@ -1305,16 +1288,8 @@ class PageViewModelTest {
|
|||
)
|
||||
}
|
||||
|
||||
observeEvents.stub {
|
||||
onBlocking { build() } doReturn flow
|
||||
}
|
||||
|
||||
openPage.stub {
|
||||
onBlocking { invoke(any(), any(), any()) } doAnswer { answer ->
|
||||
answer.getArgument<(Either<Throwable, Unit>) -> Unit>(2)(Either.Right(Unit))
|
||||
}
|
||||
}
|
||||
|
||||
stubObserveEvents(flow)
|
||||
stubOpenPage()
|
||||
buildViewModel()
|
||||
|
||||
vm.open(root)
|
||||
|
@ -1336,37 +1311,159 @@ class PageViewModelTest {
|
|||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should start duplicating focused block when requested`() {
|
||||
|
||||
val root = MockDataFactory.randomUuid()
|
||||
val child = MockDataFactory.randomUuid()
|
||||
val page = MockBlockFactory.makeOnePageWithOneTextBlock(root = root, child = child)
|
||||
|
||||
val events: Flow<Event.Command> = flow {
|
||||
delay(1000)
|
||||
emit(
|
||||
Event.Command.ShowBlock(
|
||||
rootId = root,
|
||||
blocks = page
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
stubOpenPage()
|
||||
stubObserveEvents(events)
|
||||
buildViewModel()
|
||||
|
||||
vm.open(root)
|
||||
|
||||
coroutineTestRule.advanceTime(1001)
|
||||
|
||||
vm.onBlockFocusChanged(id = child, hasFocus = true)
|
||||
vm.onActionDuplicateClicked()
|
||||
|
||||
verify(duplicateBlock, times(1)).invoke(
|
||||
scope = any(),
|
||||
params = eq(
|
||||
DuplicateBlock.Params(
|
||||
original = child,
|
||||
context = root
|
||||
)
|
||||
),
|
||||
onResult = any()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should start deleting focused block when requested`() {
|
||||
|
||||
val root = MockDataFactory.randomUuid()
|
||||
val child = MockDataFactory.randomUuid()
|
||||
val page = MockBlockFactory.makeOnePageWithOneTextBlock(root = root, child = child)
|
||||
|
||||
val events: Flow<Event.Command> = flow {
|
||||
delay(1000)
|
||||
emit(
|
||||
Event.Command.ShowBlock(
|
||||
rootId = root,
|
||||
blocks = page
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
stubOpenPage()
|
||||
stubObserveEvents(events)
|
||||
buildViewModel()
|
||||
|
||||
vm.open(root)
|
||||
|
||||
coroutineTestRule.advanceTime(1001)
|
||||
|
||||
vm.onBlockFocusChanged(id = child, hasFocus = true)
|
||||
vm.onActionDeleteClicked()
|
||||
|
||||
verify(unlinkBlocks, times(1)).invoke(
|
||||
scope = any(),
|
||||
params = eq(
|
||||
UnlinkBlocks.Params(
|
||||
context = root,
|
||||
targets = listOf(child)
|
||||
)
|
||||
),
|
||||
onResult = any()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should delete the first block when the delete-block event received for the first block, then rerender the page`() {
|
||||
|
||||
val pageOpenedDelay = 100L
|
||||
val blockDeletedEventDelay = 100L
|
||||
|
||||
val root = MockDataFactory.randomUuid()
|
||||
val firstChild = MockDataFactory.randomUuid()
|
||||
val secondChild = MockDataFactory.randomUuid()
|
||||
val page = MockBlockFactory.makeOnePageWithTwoTextBlocks(
|
||||
root = root,
|
||||
firstChild = firstChild,
|
||||
secondChild = secondChild
|
||||
)
|
||||
|
||||
val events: Flow<Event.Command> = flow {
|
||||
delay(pageOpenedDelay)
|
||||
emit(
|
||||
Event.Command.ShowBlock(
|
||||
rootId = root,
|
||||
blocks = page
|
||||
)
|
||||
)
|
||||
delay(blockDeletedEventDelay)
|
||||
emit(
|
||||
Event.Command.DeleteBlock(
|
||||
target = firstChild
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
stubOpenPage()
|
||||
stubObserveEvents(events)
|
||||
buildViewModel()
|
||||
|
||||
vm.open(root)
|
||||
|
||||
coroutineTestRule.advanceTime(pageOpenedDelay)
|
||||
|
||||
val testObserver = vm.state.test()
|
||||
|
||||
testObserver.assertValue(
|
||||
ViewState.Success(
|
||||
blocks = listOf(
|
||||
page[1].toView(focused = false),
|
||||
page.last().toView(focused = false)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
vm.onBlockFocusChanged(id = firstChild, hasFocus = true)
|
||||
vm.onActionDeleteClicked()
|
||||
|
||||
assertEquals(expected = 3, actual = vm.blocks.size)
|
||||
|
||||
coroutineTestRule.advanceTime(blockDeletedEventDelay)
|
||||
|
||||
assertEquals(expected = 2, actual = vm.blocks.size)
|
||||
|
||||
testObserver.assertValue(
|
||||
ViewState.Success(
|
||||
blocks = listOf(
|
||||
page.last().toView(focused = false)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun simulateNormalPageOpeningFlow() {
|
||||
|
||||
val root = MockDataFactory.randomUuid()
|
||||
val child = MockDataFactory.randomUuid()
|
||||
|
||||
val page = listOf(
|
||||
Block(
|
||||
id = root,
|
||||
fields = Block.Fields(emptyMap()),
|
||||
content = Block.Content.Page(
|
||||
style = Block.Content.Page.Style.SET
|
||||
),
|
||||
children = listOf(child)
|
||||
),
|
||||
Block(
|
||||
id = child,
|
||||
fields = Block.Fields(emptyMap()),
|
||||
content = Block.Content.Text(
|
||||
text = MockDataFactory.randomString(),
|
||||
marks = emptyList(),
|
||||
style = Block.Content.Text.Style.P
|
||||
),
|
||||
children = emptyList()
|
||||
)
|
||||
)
|
||||
|
||||
openPage.stub {
|
||||
onBlocking { invoke(any(), any(), any()) } doAnswer { answer ->
|
||||
answer.getArgument<(Either<Throwable, Unit>) -> Unit>(2)(Either.Right(Unit))
|
||||
}
|
||||
}
|
||||
val page = MockBlockFactory.makeOnePageWithOneTextBlock(root = root, child = child)
|
||||
|
||||
val flow: Flow<Event.Command> = flow {
|
||||
delay(1000)
|
||||
|
@ -1378,16 +1475,8 @@ class PageViewModelTest {
|
|||
)
|
||||
}
|
||||
|
||||
observeEvents.stub {
|
||||
onBlocking { build() } doReturn flow
|
||||
}
|
||||
|
||||
openPage.stub {
|
||||
onBlocking { invoke(any(), any(), any()) } doAnswer { answer ->
|
||||
answer.getArgument<(Either<Throwable, Unit>) -> Unit>(2)(Either.Right(Unit))
|
||||
}
|
||||
}
|
||||
|
||||
stubObserveEvents(flow)
|
||||
stubOpenPage()
|
||||
buildViewModel()
|
||||
|
||||
vm.open(root)
|
||||
|
@ -1411,9 +1500,9 @@ class PageViewModelTest {
|
|||
}
|
||||
}
|
||||
|
||||
private fun stubObserveEvents() {
|
||||
private fun stubObserveEvents(flow: Flow<Event> = flowOf()) {
|
||||
observeEvents.stub {
|
||||
onBlocking { build() } doReturn flowOf()
|
||||
onBlocking { build() } doReturn flow
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1424,7 +1513,9 @@ class PageViewModelTest {
|
|||
updateBlock = updateBlock,
|
||||
observeEvents = observeEvents,
|
||||
createBlock = createBlock,
|
||||
updateCheckbox = updateCheckbox
|
||||
updateCheckbox = updateCheckbox,
|
||||
unlinkBlocks = unlinkBlocks,
|
||||
duplicateBlock = duplicateBlock
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue