mirror of
https://github.com/anyproto/anytype-kotlin.git
synced 2025-06-08 05:47:05 +09:00
Editor | Feature | Slash widget, actions and alignment (#1454)
* slash actions + alignment * turn off ci * alignment click * fix delete action * on take target id * ci off * fixes * tests * fixes * copy action + tests * action paste + tests * fixes * fix move to + tests * add block extension * past and copy fixes * pr fixes * ci on * ci
This commit is contained in:
parent
16babf5264
commit
ea15716610
18 changed files with 1330 additions and 100 deletions
2
.github/workflows/check.yml
vendored
2
.github/workflows/check.yml
vendored
|
@ -1,7 +1,7 @@
|
|||
on:
|
||||
pull_request:
|
||||
# add "synchronize" in "types", in order to trigger workflow for pull request commit(s) pushes.
|
||||
types: [open,synchronize]
|
||||
types: [synchronize]
|
||||
branches: [develop]
|
||||
name: Run debug unit tests
|
||||
jobs:
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
package com.anytypeio.anytype.core_ui.features.page.slash
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.anytypeio.anytype.core_ui.R
|
||||
import com.anytypeio.anytype.core_ui.features.page.slash.holders.ActionMenuHolder
|
||||
import com.anytypeio.anytype.core_ui.features.page.slash.holders.SubheaderMenuHolder
|
||||
import com.anytypeio.anytype.presentation.page.editor.slash.SlashItem
|
||||
import kotlinx.android.synthetic.main.item_slash_widget_subheader.view.*
|
||||
|
||||
class SlashActionsAdapter(
|
||||
private var items: List<SlashItem>,
|
||||
private val clicks: (SlashItem) -> Unit,
|
||||
private val clickBack: () -> Unit
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
fun update(items: List<SlashItem>) {
|
||||
this.items = items
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
val size = items.size
|
||||
if (size > 0) {
|
||||
items = listOf()
|
||||
notifyItemRangeRemoved(0, size)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
return when (viewType) {
|
||||
R.layout.item_slash_widget_style -> {
|
||||
ActionMenuHolder(
|
||||
view = inflater.inflate(viewType, parent, false)
|
||||
).apply {
|
||||
itemView.setOnClickListener {
|
||||
clicks(items[bindingAdapterPosition])
|
||||
}
|
||||
}
|
||||
}
|
||||
R.layout.item_slash_widget_subheader -> {
|
||||
SubheaderMenuHolder(
|
||||
view = inflater.inflate(viewType, parent, false)
|
||||
).apply {
|
||||
itemView.flBack.setOnClickListener {
|
||||
clickBack.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> throw IllegalArgumentException("Wrong viewtype:$viewType")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (holder) {
|
||||
is ActionMenuHolder -> {
|
||||
val item = items[position] as SlashItem.Actions
|
||||
holder.bind(item)
|
||||
}
|
||||
is SubheaderMenuHolder -> {
|
||||
val item = items[position] as SlashItem.Subheader
|
||||
holder.bind(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int = when (val item = items[position]) {
|
||||
is SlashItem.Actions -> R.layout.item_slash_widget_style
|
||||
is SlashItem.Subheader -> R.layout.item_slash_widget_subheader
|
||||
else -> throw IllegalArgumentException("Wrong item type:$item")
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = items.size
|
||||
}
|
|
@ -7,6 +7,7 @@ import androidx.constraintlayout.widget.ConstraintLayout
|
|||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.anytypeio.anytype.core_ui.R
|
||||
import com.anytypeio.anytype.core_ui.features.page.slash.holders.SlashAlignmentAdapter
|
||||
import com.anytypeio.anytype.core_ui.tools.SlashHelper
|
||||
import com.anytypeio.anytype.presentation.page.editor.slash.SlashCommand
|
||||
import com.anytypeio.anytype.presentation.page.editor.slash.SlashItem
|
||||
|
@ -73,13 +74,31 @@ class SlashWidget @JvmOverloads constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private val actionsAdapter by lazy {
|
||||
SlashActionsAdapter(
|
||||
items = listOf(),
|
||||
clicks = { _clickEvents.offer(it) },
|
||||
clickBack = { _backEvent.offer(true) }
|
||||
)
|
||||
}
|
||||
|
||||
private val alignAdapter by lazy {
|
||||
SlashAlignmentAdapter(
|
||||
items = listOf(),
|
||||
clicks = { _clickEvents.offer(it) },
|
||||
clickBack = { _backEvent.offer(true) }
|
||||
)
|
||||
}
|
||||
|
||||
private val concatAdapter = ConcatAdapter(
|
||||
mainAdapter,
|
||||
styleAdapter,
|
||||
mediaAdapter,
|
||||
objectTypesAdapter,
|
||||
relationsAdapter,
|
||||
otherAdapter
|
||||
otherAdapter,
|
||||
actionsAdapter,
|
||||
alignAdapter
|
||||
)
|
||||
|
||||
init {
|
||||
|
@ -106,6 +125,8 @@ class SlashWidget @JvmOverloads constructor(
|
|||
objectTypesAdapter.clear()
|
||||
relationsAdapter.clear()
|
||||
otherAdapter.clear()
|
||||
actionsAdapter.clear()
|
||||
alignAdapter.clear()
|
||||
}
|
||||
is SlashCommand.ShowStyleItems -> {
|
||||
styleAdapter.update(command.items)
|
||||
|
@ -116,6 +137,8 @@ class SlashWidget @JvmOverloads constructor(
|
|||
objectTypesAdapter.clear()
|
||||
relationsAdapter.clear()
|
||||
otherAdapter.clear()
|
||||
actionsAdapter.clear()
|
||||
alignAdapter.clear()
|
||||
}
|
||||
is SlashCommand.ShowMediaItems -> {
|
||||
mediaAdapter.update(command.items)
|
||||
|
@ -126,6 +149,8 @@ class SlashWidget @JvmOverloads constructor(
|
|||
objectTypesAdapter.clear()
|
||||
relationsAdapter.clear()
|
||||
otherAdapter.clear()
|
||||
actionsAdapter.clear()
|
||||
alignAdapter.clear()
|
||||
}
|
||||
is SlashCommand.ShowRelations -> {
|
||||
relationsAdapter.update(command.relations)
|
||||
|
@ -136,6 +161,8 @@ class SlashWidget @JvmOverloads constructor(
|
|||
mediaAdapter.clear()
|
||||
objectTypesAdapter.clear()
|
||||
otherAdapter.clear()
|
||||
actionsAdapter.clear()
|
||||
alignAdapter.clear()
|
||||
}
|
||||
is SlashCommand.ShowObjectTypes -> {
|
||||
objectTypesAdapter.update(command.items)
|
||||
|
@ -146,15 +173,44 @@ class SlashWidget @JvmOverloads constructor(
|
|||
mediaAdapter.clear()
|
||||
relationsAdapter.clear()
|
||||
otherAdapter.clear()
|
||||
actionsAdapter.clear()
|
||||
alignAdapter.clear()
|
||||
}
|
||||
is SlashCommand.ShowOtherItems -> {
|
||||
otherAdapter.update(command.items)
|
||||
rvSlash.smoothScrollToPosition(0)
|
||||
|
||||
mainAdapter.clear()
|
||||
styleAdapter.clear()
|
||||
mediaAdapter.clear()
|
||||
objectTypesAdapter.clear()
|
||||
relationsAdapter.clear()
|
||||
actionsAdapter.clear()
|
||||
alignAdapter.clear()
|
||||
}
|
||||
is SlashCommand.ShowActionItems -> {
|
||||
actionsAdapter.update(command.items)
|
||||
rvSlash.smoothScrollToPosition(0)
|
||||
|
||||
mainAdapter.clear()
|
||||
styleAdapter.clear()
|
||||
mediaAdapter.clear()
|
||||
objectTypesAdapter.clear()
|
||||
relationsAdapter.clear()
|
||||
otherAdapter.clear()
|
||||
alignAdapter.clear()
|
||||
}
|
||||
is SlashCommand.ShowAlignmentItems -> {
|
||||
alignAdapter.update(command.items)
|
||||
rvSlash.smoothScrollToPosition(0)
|
||||
|
||||
mainAdapter.clear()
|
||||
styleAdapter.clear()
|
||||
mediaAdapter.clear()
|
||||
objectTypesAdapter.clear()
|
||||
relationsAdapter.clear()
|
||||
otherAdapter.clear()
|
||||
actionsAdapter.clear()
|
||||
}
|
||||
is SlashCommand.FilterItems -> {
|
||||
val filter = command.filter.removePrefix(SLASH_PREFIX)
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
package com.anytypeio.anytype.core_ui.features.page.slash.holders
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.anytypeio.anytype.core_ui.R
|
||||
import com.anytypeio.anytype.core_utils.ext.gone
|
||||
import com.anytypeio.anytype.presentation.page.editor.slash.SlashItem
|
||||
import kotlinx.android.synthetic.main.item_slash_widget_style.view.*
|
||||
|
||||
class ActionMenuHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
|
||||
fun bind(item: SlashItem.Actions) = with(itemView) {
|
||||
when (item) {
|
||||
SlashItem.Actions.CleanStyle -> {
|
||||
tvTitle.setText(R.string.slash_widget_actions_clean_style)
|
||||
ivIcon.setImageResource(R.drawable.ic_slash_actions_clean_style)
|
||||
tvSubtitle.gone()
|
||||
}
|
||||
SlashItem.Actions.Copy -> {
|
||||
tvTitle.setText(R.string.slash_widget_actions_copy)
|
||||
ivIcon.setImageResource(R.drawable.ic_slash_actions_copy)
|
||||
tvSubtitle.gone()
|
||||
}
|
||||
SlashItem.Actions.Delete -> {
|
||||
tvTitle.setText(R.string.slash_widget_actions_delete)
|
||||
ivIcon.setImageResource(R.drawable.ic_slash_actions_delete)
|
||||
tvSubtitle.gone()
|
||||
}
|
||||
SlashItem.Actions.Duplicate -> {
|
||||
tvTitle.setText(R.string.slash_widget_actions_duplicate)
|
||||
ivIcon.setImageResource(R.drawable.ic_slash_actions_duplicate)
|
||||
tvSubtitle.gone()
|
||||
}
|
||||
SlashItem.Actions.Move -> {
|
||||
tvTitle.setText(R.string.slash_widget_actions_move)
|
||||
ivIcon.setImageResource(R.drawable.ic_slash_actions_move)
|
||||
tvSubtitle.gone()
|
||||
}
|
||||
SlashItem.Actions.MoveTo -> {
|
||||
tvTitle.setText(R.string.slash_widget_actions_moveto)
|
||||
ivIcon.setImageResource(R.drawable.ic_slash_actions_move_to)
|
||||
tvSubtitle.gone()
|
||||
}
|
||||
SlashItem.Actions.Paste -> {
|
||||
tvTitle.setText(R.string.slash_widget_actions_paste)
|
||||
ivIcon.setImageResource(R.drawable.ic_slash_actions_clean_style)
|
||||
tvSubtitle.gone()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package com.anytypeio.anytype.core_ui.features.page.slash.holders
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.anytypeio.anytype.core_ui.R
|
||||
import com.anytypeio.anytype.core_utils.ext.gone
|
||||
import com.anytypeio.anytype.presentation.page.editor.slash.SlashItem
|
||||
import kotlinx.android.synthetic.main.item_slash_widget_style.view.*
|
||||
|
||||
class AlignMenuHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
|
||||
fun bind(item: SlashItem.Alignment) = with(itemView) {
|
||||
when (item) {
|
||||
SlashItem.Alignment.Left -> {
|
||||
tvTitle.setText(R.string.slash_widget_align_left)
|
||||
ivIcon.setImageResource(R.drawable.ic_slash_align_left)
|
||||
tvSubtitle.gone()
|
||||
}
|
||||
SlashItem.Alignment.Center -> {
|
||||
tvTitle.setText(R.string.slash_widget_align_center)
|
||||
ivIcon.setImageResource(R.drawable.ic_slash_align_center)
|
||||
tvSubtitle.gone()
|
||||
}
|
||||
SlashItem.Alignment.Right -> {
|
||||
tvTitle.setText(R.string.slash_widget_align_right)
|
||||
ivIcon.setImageResource(R.drawable.ic_slash_align_right)
|
||||
tvSubtitle.gone()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package com.anytypeio.anytype.core_ui.features.page.slash.holders
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.anytypeio.anytype.core_ui.R
|
||||
import com.anytypeio.anytype.presentation.page.editor.slash.SlashItem
|
||||
import kotlinx.android.synthetic.main.item_slash_widget_subheader.view.*
|
||||
|
||||
class SlashAlignmentAdapter(
|
||||
private var items: List<SlashItem>,
|
||||
private val clicks: (SlashItem) -> Unit,
|
||||
private val clickBack: () -> Unit
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
fun update(items: List<SlashItem>) {
|
||||
this.items = items
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
val size = items.size
|
||||
if (size > 0) {
|
||||
items = listOf()
|
||||
notifyItemRangeRemoved(0, size)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
return when (viewType) {
|
||||
R.layout.item_slash_widget_style -> {
|
||||
AlignMenuHolder(
|
||||
view = inflater.inflate(viewType, parent, false)
|
||||
).apply {
|
||||
itemView.setOnClickListener {
|
||||
clicks(items[bindingAdapterPosition])
|
||||
}
|
||||
}
|
||||
}
|
||||
R.layout.item_slash_widget_subheader -> {
|
||||
SubheaderMenuHolder(
|
||||
view = inflater.inflate(viewType, parent, false)
|
||||
).apply {
|
||||
itemView.flBack.setOnClickListener {
|
||||
clickBack.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> throw IllegalArgumentException("Wrong viewtype:$viewType")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (holder) {
|
||||
is AlignMenuHolder -> {
|
||||
val item = items[position] as SlashItem.Alignment
|
||||
holder.bind(item)
|
||||
}
|
||||
is SubheaderMenuHolder -> {
|
||||
val item = items[position] as SlashItem.Subheader
|
||||
holder.bind(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int = when (val item = items[position]) {
|
||||
is SlashItem.Alignment -> R.layout.item_slash_widget_style
|
||||
is SlashItem.Subheader -> R.layout.item_slash_widget_subheader
|
||||
else -> throw IllegalArgumentException("Wrong item type:$item")
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = items.size
|
||||
}
|
|
@ -39,6 +39,38 @@ class SubheaderMenuHolder(view: View) : RecyclerView.ViewHolder(view) {
|
|||
flBack.visible()
|
||||
R.string.slash_widget_main_other
|
||||
}
|
||||
SlashItem.Subheader.Actions -> {
|
||||
flBack.invisible()
|
||||
R.string.slash_widget_main_actions
|
||||
}
|
||||
SlashItem.Subheader.ActionsWithBack -> {
|
||||
flBack.visible()
|
||||
R.string.slash_widget_main_actions
|
||||
}
|
||||
SlashItem.Subheader.Alignment -> {
|
||||
flBack.invisible()
|
||||
R.string.slash_widget_main_alignment
|
||||
}
|
||||
SlashItem.Subheader.AlignmentWithBack -> {
|
||||
flBack.visible()
|
||||
R.string.slash_widget_main_alignment
|
||||
}
|
||||
SlashItem.Subheader.Color -> {
|
||||
flBack.invisible()
|
||||
R.string.slash_widget_main_color
|
||||
}
|
||||
SlashItem.Subheader.ColorWithBack -> {
|
||||
flBack.visible()
|
||||
R.string.slash_widget_main_color
|
||||
}
|
||||
SlashItem.Subheader.Background -> {
|
||||
flBack.invisible()
|
||||
R.string.slash_widget_main_background
|
||||
}
|
||||
SlashItem.Subheader.BackgroundWithBack -> {
|
||||
flBack.visible()
|
||||
R.string.slash_widget_main_background
|
||||
}
|
||||
}
|
||||
subheader.setText(text)
|
||||
}
|
||||
|
|
|
@ -54,6 +54,7 @@ ext {
|
|||
robolectric_latest_version = '4.5.1'
|
||||
junit_version = '4.12'
|
||||
kluent_version = '1.14'
|
||||
timber_junit = '1.0.1'
|
||||
coroutine_testing_version = '1.4.3'
|
||||
live_data_testing_version = '1.1.0'
|
||||
mockito_kotlin_version = '3.2.0'
|
||||
|
@ -147,7 +148,8 @@ ext {
|
|||
liveDataTesting: "com.jraska.livedata:testing-ktx:$live_data_testing_version",
|
||||
coroutineTesting: "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutine_testing_version",
|
||||
assertj: "com.squareup.assertj:assertj-android:1.0.0",
|
||||
androidXTestCore: "androidx.test:core:$androidx_test_core_version"
|
||||
androidXTestCore: "androidx.test:core:$androidx_test_core_version",
|
||||
timberJUnit: "net.lachlanmckee:timber-junit-rule:$timber_junit"
|
||||
]
|
||||
|
||||
acceptanceTesting = [
|
||||
|
|
|
@ -52,4 +52,8 @@ class Paste(
|
|||
val focus: Id,
|
||||
val range: IntRange
|
||||
)
|
||||
|
||||
companion object {
|
||||
val DEFAULT_RANGE = IntRange(0, 0)
|
||||
}
|
||||
}
|
|
@ -81,4 +81,5 @@ dependencies {
|
|||
testImplementation unitTestDependencies.liveDataTesting
|
||||
testImplementation unitTestDependencies.archCoreTesting
|
||||
testImplementation unitTestDependencies.androidXTestCore
|
||||
testImplementation unitTestDependencies.timberJUnit
|
||||
}
|
||||
|
|
|
@ -615,6 +615,12 @@ sealed class ControlPanelMachine {
|
|||
cursorCoordinate = event.cursorCoordinate,
|
||||
updateList = false,
|
||||
items = emptyList()
|
||||
),
|
||||
mainToolbar = state.mainToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
navigationToolbar = state.navigationToolbar.copy(
|
||||
isVisible = false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -752,7 +758,8 @@ sealed class ControlPanelMachine {
|
|||
),
|
||||
navigationToolbar = state.navigationToolbar.copy(
|
||||
isVisible = false
|
||||
)
|
||||
),
|
||||
slashWidget = Toolbar.SlashWidget.reset()
|
||||
)
|
||||
is Event.SAM.OnApply -> {
|
||||
if (state.multiSelect.isQuickScrollAndMoveMode) {
|
||||
|
|
|
@ -37,6 +37,8 @@ import com.anytypeio.anytype.domain.block.interactor.RemoveLinkMark
|
|||
import com.anytypeio.anytype.domain.block.interactor.UpdateLinkMarks
|
||||
import com.anytypeio.anytype.domain.block.interactor.UpdateText
|
||||
import com.anytypeio.anytype.domain.block.interactor.sets.GetObjectTypes
|
||||
import com.anytypeio.anytype.domain.clipboard.Copy
|
||||
import com.anytypeio.anytype.domain.clipboard.Paste
|
||||
import com.anytypeio.anytype.domain.cover.RemoveDocCover
|
||||
import com.anytypeio.anytype.domain.cover.SetDocCoverImage
|
||||
import com.anytypeio.anytype.domain.editor.Editor
|
||||
|
@ -1237,19 +1239,26 @@ class PageViewModel(
|
|||
|
||||
private fun onBlockAlignmentActionClicked(alignment: Alignment) {
|
||||
controlPanelViewState.value?.stylingToolbar?.target?.id?.let { id ->
|
||||
viewModelScope.launch {
|
||||
orchestrator.proxies.intents.send(
|
||||
Intent.Text.Align(
|
||||
context = context,
|
||||
target = id,
|
||||
alignment = when (alignment) {
|
||||
Alignment.START -> Block.Align.AlignLeft
|
||||
Alignment.CENTER -> Block.Align.AlignCenter
|
||||
Alignment.END -> Block.Align.AlignRight
|
||||
}
|
||||
)
|
||||
proceedWithAlignmentUpdate(
|
||||
id = id,
|
||||
alignment = when (alignment) {
|
||||
Alignment.START -> Block.Align.AlignLeft
|
||||
Alignment.CENTER -> Block.Align.AlignCenter
|
||||
Alignment.END -> Block.Align.AlignRight
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun proceedWithAlignmentUpdate(id: String, alignment: Block.Align) {
|
||||
viewModelScope.launch {
|
||||
orchestrator.proxies.intents.send(
|
||||
Intent.Text.Align(
|
||||
context = context,
|
||||
target = id,
|
||||
alignment = alignment
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1405,30 +1414,7 @@ class PageViewModel(
|
|||
ActionItemType.MoveTo -> {
|
||||
onExitActionMode()
|
||||
dispatch(Command.PopBackStack)
|
||||
viewModelScope.sendEvent(
|
||||
analytics = analytics,
|
||||
eventName = EventsDictionary.SCREEN_MOVE_TO
|
||||
)
|
||||
|
||||
val excluded = mutableListOf<Id>()
|
||||
|
||||
val target = blocks.find { it.id == id }
|
||||
|
||||
if (target != null) {
|
||||
(target.content as? Content.Link)?.let { content ->
|
||||
excluded.add(content.target)
|
||||
}
|
||||
}
|
||||
|
||||
navigate(
|
||||
EventWrapper(
|
||||
AppNavigation.Command.OpenMoveToScreen(
|
||||
context = context,
|
||||
targets = listOf(id),
|
||||
excluded = excluded
|
||||
)
|
||||
)
|
||||
)
|
||||
proceedWithMoveTo(id)
|
||||
}
|
||||
ActionItemType.Style -> {
|
||||
val textSelection = orchestrator.stores.textSelection.current()
|
||||
|
@ -1475,6 +1461,33 @@ class PageViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
private fun proceedWithMoveTo(id: String) {
|
||||
viewModelScope.sendEvent(
|
||||
analytics = analytics,
|
||||
eventName = EventsDictionary.SCREEN_MOVE_TO
|
||||
)
|
||||
|
||||
val excluded = mutableListOf<Id>()
|
||||
|
||||
val target = blocks.find { it.id == id }
|
||||
|
||||
if (target != null) {
|
||||
(target.content as? Content.Link)?.let { content ->
|
||||
excluded.add(content.target)
|
||||
}
|
||||
}
|
||||
|
||||
navigate(
|
||||
EventWrapper(
|
||||
AppNavigation.Command.OpenMoveToScreen(
|
||||
context = context,
|
||||
targets = listOf(id),
|
||||
excluded = excluded
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun proceedWithUnlinking(target: String) {
|
||||
|
||||
val position = views.indexOfFirst { it.id == target }
|
||||
|
@ -2963,7 +2976,7 @@ class PageViewModel(
|
|||
}
|
||||
|
||||
fun onCopy(
|
||||
range: IntRange
|
||||
range: IntRange?
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
orchestrator.proxies.intents.send(
|
||||
|
@ -3599,9 +3612,18 @@ class PageViewModel(
|
|||
|
||||
//region SLASH WIDGET
|
||||
fun onSlashItemClicked(item: SlashItem) {
|
||||
val target = orchestrator.stores.focus.current()
|
||||
if (!target.isEmpty) {
|
||||
proceedWithSlashItem(item, target.id)
|
||||
} else {
|
||||
Timber.e("Slash Widget Error, target is empty")
|
||||
}
|
||||
}
|
||||
|
||||
private fun proceedWithSlashItem(item: SlashItem, targetId: Id) {
|
||||
when (item) {
|
||||
is SlashItem.Main.Style -> {
|
||||
val items = listOf(SlashItem.Subheader.StyleWithBack) + SlashExtensions.getSlashItems()
|
||||
val items = listOf(SlashItem.Subheader.StyleWithBack) + SlashExtensions.getStyleItems()
|
||||
onSlashCommand(SlashCommand.ShowStyleItems(items))
|
||||
}
|
||||
is SlashItem.Main.Media -> {
|
||||
|
@ -3643,8 +3665,16 @@ class PageViewModel(
|
|||
val items = listOf(SlashItem.Subheader.OtherWithBack) + SlashExtensions.getOtherItems()
|
||||
onSlashCommand(SlashCommand.ShowOtherItems(items))
|
||||
}
|
||||
is SlashItem.Main.Actions -> {
|
||||
val items = listOf(SlashItem.Subheader.ActionsWithBack) + SlashExtensions.getActionItems()
|
||||
onSlashCommand(SlashCommand.ShowActionItems(items))
|
||||
}
|
||||
is SlashItem.Main.Alignment -> {
|
||||
val items = listOf(SlashItem.Subheader.AlignmentWithBack) + SlashExtensions.getAlignmentItems()
|
||||
onSlashCommand(SlashCommand.ShowAlignmentItems(items))
|
||||
}
|
||||
is SlashItem.Style.Type -> {
|
||||
onSlashStyleTypeItemClicked(item)
|
||||
onSlashStyleTypeItemClicked(item, targetId)
|
||||
}
|
||||
is SlashItem.Media -> {
|
||||
onSlashMediaItemClicked(item)
|
||||
|
@ -3661,8 +3691,14 @@ class PageViewModel(
|
|||
controlPanelInteractor.onEvent(ControlPanelMachine.Event.Slash.OnStop)
|
||||
addDividerBlock(style = Content.Divider.Style.DOTS)
|
||||
}
|
||||
is SlashItem.Actions -> {
|
||||
onSlashActionItemClicked(item, targetId)
|
||||
}
|
||||
is SlashItem.Alignment -> {
|
||||
onSlashAlignmentItemClicked(item, targetId)
|
||||
}
|
||||
else -> {
|
||||
Timber.d("PRESSED ON SLAH ITEM : $item")
|
||||
Timber.d("PRESSED ON SLASH ITEM : $item")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3692,26 +3728,82 @@ class PageViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
private fun onSlashStyleTypeItemClicked(item: SlashItem.Style.Type) {
|
||||
val target = _focus.value
|
||||
if (target != null) {
|
||||
viewModelScope.launch {
|
||||
orchestrator.stores.focus.update(
|
||||
Editor.Focus(
|
||||
id = target,
|
||||
cursor = Editor.Cursor.End
|
||||
)
|
||||
private fun onSlashStyleTypeItemClicked(item: SlashItem.Style.Type, targetId: Id) {
|
||||
viewModelScope.launch {
|
||||
orchestrator.stores.focus.update(
|
||||
Editor.Focus(
|
||||
id = targetId,
|
||||
cursor = Editor.Cursor.End
|
||||
)
|
||||
}
|
||||
val uiBlock = item.convertToUiBlock()
|
||||
controlPanelInteractor.onEvent(ControlPanelMachine.Event.Slash.OnStop)
|
||||
onTurnIntoBlockClicked(
|
||||
target = target,
|
||||
uiBlock = uiBlock
|
||||
)
|
||||
} else {
|
||||
Timber.e("Error while style item clicked, target is null")
|
||||
}
|
||||
val uiBlock = item.convertToUiBlock()
|
||||
controlPanelInteractor.onEvent(ControlPanelMachine.Event.Slash.OnStop)
|
||||
onTurnIntoBlockClicked(
|
||||
target = targetId,
|
||||
uiBlock = uiBlock
|
||||
)
|
||||
}
|
||||
|
||||
private fun onSlashActionItemClicked(item: SlashItem.Actions, targetId: Id) {
|
||||
when (item) {
|
||||
SlashItem.Actions.CleanStyle -> {
|
||||
controlPanelInteractor.onEvent(ControlPanelMachine.Event.Slash.OnStop)
|
||||
proceedWithClearStyle(targetId)
|
||||
}
|
||||
SlashItem.Actions.Copy -> {
|
||||
controlPanelInteractor.onEvent(ControlPanelMachine.Event.Slash.OnStop)
|
||||
onCopy(range = null)
|
||||
}
|
||||
SlashItem.Actions.Paste -> {
|
||||
val range = orchestrator.stores.textSelection.current().selection ?: Paste.DEFAULT_RANGE
|
||||
controlPanelInteractor.onEvent(ControlPanelMachine.Event.Slash.OnStop)
|
||||
onPaste(range = range)
|
||||
}
|
||||
SlashItem.Actions.Delete -> {
|
||||
controlPanelInteractor.onEvent(ControlPanelMachine.Event.Slash.OnStop)
|
||||
proceedWithUnlinking(targetId)
|
||||
}
|
||||
SlashItem.Actions.Duplicate -> {
|
||||
controlPanelInteractor.onEvent(ControlPanelMachine.Event.Slash.OnStop)
|
||||
duplicateBlock(targetId)
|
||||
}
|
||||
SlashItem.Actions.Move -> {
|
||||
mode = EditorMode.SAM
|
||||
viewModelScope.launch { orchestrator.stores.focus.update(Editor.Focus.empty()) }
|
||||
viewModelScope.launch { refresh() }
|
||||
proceedWithSAMQuickStartSelection(targetId)
|
||||
controlPanelInteractor.onEvent(ControlPanelMachine.Event.SAM.OnQuickStart(1))
|
||||
}
|
||||
SlashItem.Actions.MoveTo -> {
|
||||
onHideKeyboardClicked()
|
||||
proceedWithMoveTo(targetId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun proceedWithClearStyle(targetId: Id) {
|
||||
val targetBlock = blocks.first { it.id == targetId }
|
||||
proceedWithUpdatingText(
|
||||
Intent.Text.UpdateText(
|
||||
context = context,
|
||||
target = targetId,
|
||||
text = targetBlock.content.asText().text,
|
||||
marks = emptyList()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun onSlashAlignmentItemClicked(item: SlashItem.Alignment, targetId: Id) {
|
||||
val alignment = when (item) {
|
||||
SlashItem.Alignment.Center -> Block.Align.AlignCenter
|
||||
SlashItem.Alignment.Left -> Block.Align.AlignLeft
|
||||
SlashItem.Alignment.Right -> Block.Align.AlignRight
|
||||
}
|
||||
proceedWithAlignmentUpdate(
|
||||
id = targetId,
|
||||
alignment = alignment
|
||||
)
|
||||
}
|
||||
|
||||
private fun onSlashCommand(command: SlashCommand) {
|
||||
|
|
|
@ -54,10 +54,10 @@ data class ControlPanelState(
|
|||
* @property isVisible defines whether the toolbar is visible or not
|
||||
*/
|
||||
data class Styling(
|
||||
override val isVisible: Boolean,
|
||||
val target: Target? = null,
|
||||
val config: StyleConfig? = null,
|
||||
val props: Props? = null,
|
||||
override val isVisible: Boolean,
|
||||
val mode: StylingMode? = null
|
||||
) : Toolbar() {
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ object SlashExtensions {
|
|||
SlashItem.Main.Background,
|
||||
)
|
||||
|
||||
fun getSlashItems() = listOf(
|
||||
fun getStyleItems() = listOf(
|
||||
SlashItem.Style.Type.Text,
|
||||
SlashItem.Style.Type.Title,
|
||||
SlashItem.Style.Type.Heading,
|
||||
|
@ -69,4 +69,20 @@ object SlashExtensions {
|
|||
SlashItem.Other.Line,
|
||||
SlashItem.Other.Dots
|
||||
)
|
||||
|
||||
fun getActionItems() = listOf(
|
||||
SlashItem.Actions.Delete,
|
||||
SlashItem.Actions.Duplicate,
|
||||
SlashItem.Actions.Copy,
|
||||
SlashItem.Actions.Paste,
|
||||
SlashItem.Actions.Move,
|
||||
SlashItem.Actions.MoveTo,
|
||||
SlashItem.Actions.CleanStyle
|
||||
)
|
||||
|
||||
fun getAlignmentItems() = listOf(
|
||||
SlashItem.Alignment.Left,
|
||||
SlashItem.Alignment.Center,
|
||||
SlashItem.Alignment.Right
|
||||
)
|
||||
}
|
|
@ -12,6 +12,10 @@ sealed class SlashCommand {
|
|||
data class ShowOtherItems(val items: List<SlashItem>) : SlashCommand()
|
||||
data class ShowRelations(val relations: List<RelationListViewModel.Model>): SlashCommand()
|
||||
data class ShowObjectTypes(val items: List<SlashItem>): SlashCommand()
|
||||
data class ShowActionItems(val items: List<SlashItem>) : SlashCommand()
|
||||
data class ShowAlignmentItems(val items: List<SlashItem>) : SlashCommand()
|
||||
data class ShowColorItems(val items: List<SlashItem>) : SlashCommand()
|
||||
data class ShowBackgroundItems(val items: List<SlashItem>) : SlashCommand()
|
||||
}
|
||||
|
||||
sealed class SlashItem {
|
||||
|
@ -25,6 +29,14 @@ sealed class SlashItem {
|
|||
object ObjectType: Subheader()
|
||||
object Other: Subheader()
|
||||
object OtherWithBack: Subheader()
|
||||
object Actions: Subheader()
|
||||
object ActionsWithBack: Subheader()
|
||||
object Alignment: Subheader()
|
||||
object AlignmentWithBack: Subheader()
|
||||
object Color: Subheader()
|
||||
object ColorWithBack: Subheader()
|
||||
object Background: Subheader()
|
||||
object BackgroundWithBack: Subheader()
|
||||
}
|
||||
//endregion
|
||||
|
||||
|
|
|
@ -184,6 +184,8 @@ open class EditorPresentationTestSetup {
|
|||
|
||||
private lateinit var updateDetail: UpdateDetail
|
||||
|
||||
open lateinit var orchestrator: Orchestrator
|
||||
|
||||
open fun buildViewModel(urlBuilder: UrlBuilder = builder): PageViewModel {
|
||||
|
||||
val storage = Editor.Storage()
|
||||
|
@ -193,6 +195,44 @@ open class EditorPresentationTestSetup {
|
|||
)
|
||||
updateDetail = UpdateDetail(repo)
|
||||
|
||||
orchestrator = Orchestrator(
|
||||
createBlock = createBlock,
|
||||
replaceBlock = replaceBlock,
|
||||
updateTextColor = updateTextColor,
|
||||
duplicateBlock = duplicateBlock,
|
||||
downloadFile = downloadFile,
|
||||
undo = undo,
|
||||
redo = redo,
|
||||
updateTitle = updateTitle,
|
||||
updateText = updateText,
|
||||
updateCheckbox = updateCheckbox,
|
||||
updateTextStyle = updateTextStyle,
|
||||
updateBackgroundColor = updateBackgroundColor,
|
||||
mergeBlocks = mergeBlocks,
|
||||
uploadBlock = uploadBlock,
|
||||
splitBlock = splitBlock,
|
||||
unlinkBlocks = unlinkBlocks,
|
||||
updateDivider = updateDivider,
|
||||
memory = memory,
|
||||
stores = storage,
|
||||
proxies = proxies,
|
||||
textInteractor = Interactor.TextInteractor(
|
||||
proxies = proxies,
|
||||
stores = storage,
|
||||
matcher = DefaultPatternMatcher()
|
||||
),
|
||||
updateAlignment = updateAlignment,
|
||||
setupBookmark = setupBookmark,
|
||||
paste = paste,
|
||||
copy = copy,
|
||||
move = move,
|
||||
turnIntoDocument = turnIntoDocument,
|
||||
analytics = analytics,
|
||||
updateFields = updateFields,
|
||||
setRelationKey = setRelationKey,
|
||||
turnIntoStyle = turnIntoStyle
|
||||
)
|
||||
|
||||
return PageViewModel(
|
||||
getListPages = getListPages,
|
||||
openPage = openPage,
|
||||
|
@ -215,43 +255,7 @@ open class EditorPresentationTestSetup {
|
|||
createDocument = createDocument,
|
||||
createNewDocument = createNewDocument,
|
||||
analytics = analytics,
|
||||
orchestrator = Orchestrator(
|
||||
createBlock = createBlock,
|
||||
replaceBlock = replaceBlock,
|
||||
updateTextColor = updateTextColor,
|
||||
duplicateBlock = duplicateBlock,
|
||||
downloadFile = downloadFile,
|
||||
undo = undo,
|
||||
redo = redo,
|
||||
updateTitle = updateTitle,
|
||||
updateText = updateText,
|
||||
updateCheckbox = updateCheckbox,
|
||||
updateTextStyle = updateTextStyle,
|
||||
updateBackgroundColor = updateBackgroundColor,
|
||||
mergeBlocks = mergeBlocks,
|
||||
uploadBlock = uploadBlock,
|
||||
splitBlock = splitBlock,
|
||||
unlinkBlocks = unlinkBlocks,
|
||||
updateDivider = updateDivider,
|
||||
memory = memory,
|
||||
stores = storage,
|
||||
proxies = proxies,
|
||||
textInteractor = Interactor.TextInteractor(
|
||||
proxies = proxies,
|
||||
stores = storage,
|
||||
matcher = DefaultPatternMatcher()
|
||||
),
|
||||
updateAlignment = updateAlignment,
|
||||
setupBookmark = setupBookmark,
|
||||
paste = paste,
|
||||
copy = copy,
|
||||
move = move,
|
||||
turnIntoDocument = turnIntoDocument,
|
||||
analytics = analytics,
|
||||
updateFields = updateFields,
|
||||
setRelationKey = setRelationKey,
|
||||
turnIntoStyle = turnIntoStyle
|
||||
),
|
||||
orchestrator = orchestrator,
|
||||
dispatcher = Dispatcher.Default(),
|
||||
removeDocCover = removeDocCover,
|
||||
setDocCoverImage = setDocCoverImage,
|
||||
|
|
|
@ -0,0 +1,665 @@
|
|||
package com.anytypeio.anytype.presentation.page.editor
|
||||
|
||||
import MockDataFactory
|
||||
import android.util.Log
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.anytypeio.anytype.core_models.Block
|
||||
import com.anytypeio.anytype.domain.block.interactor.DuplicateBlock
|
||||
import com.anytypeio.anytype.domain.block.interactor.UnlinkBlocks
|
||||
import com.anytypeio.anytype.domain.block.interactor.UpdateText
|
||||
import com.anytypeio.anytype.domain.clipboard.Copy
|
||||
import com.anytypeio.anytype.domain.clipboard.Paste
|
||||
import com.anytypeio.anytype.presentation.MockTypicalDocumentFactory
|
||||
import com.anytypeio.anytype.presentation.navigation.AppNavigation
|
||||
import com.anytypeio.anytype.presentation.page.editor.control.ControlPanelState
|
||||
import com.anytypeio.anytype.presentation.page.editor.slash.SlashEvent
|
||||
import com.anytypeio.anytype.presentation.page.editor.slash.SlashItem
|
||||
import com.anytypeio.anytype.presentation.util.CoroutinesTestRule
|
||||
import com.jraska.livedata.test
|
||||
import net.lachlanmckee.timberjunit.TimberTestRule
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.mockito.MockitoAnnotations
|
||||
import org.mockito.kotlin.times
|
||||
import org.mockito.kotlin.verifyBlocking
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
class EditorSlashWidgetActionsTest : EditorPresentationTestSetup() {
|
||||
|
||||
@get:Rule
|
||||
val rule = InstantTaskExecutorRule()
|
||||
|
||||
@get:Rule
|
||||
val coroutineTestRule = CoroutinesTestRule()
|
||||
|
||||
@get:Rule
|
||||
val timberTestRule: TimberTestRule = TimberTestRule.builder()
|
||||
.minPriority(Log.DEBUG)
|
||||
.showThread(true)
|
||||
.showTimestamp(false)
|
||||
.onlyLogWhenTestFails(true)
|
||||
.build()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
}
|
||||
|
||||
//region {Action DELETE}
|
||||
@Test
|
||||
fun `should not hide slash widget when action delete happened`() {
|
||||
|
||||
val doc = MockTypicalDocumentFactory.page(root)
|
||||
val block = MockTypicalDocumentFactory.a
|
||||
|
||||
stubInterceptEvents()
|
||||
stubOpenDocument(document = doc)
|
||||
val vm = buildViewModel()
|
||||
|
||||
vm.onStart(root)
|
||||
|
||||
vm.apply {
|
||||
onBlockFocusChanged(
|
||||
id = block.id,
|
||||
hasFocus = true
|
||||
)
|
||||
onSlashEvent(
|
||||
SlashEvent.Start(
|
||||
cursorCoordinate = 100,
|
||||
slashStart = 0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// TESTING
|
||||
|
||||
vm.onSlashItemClicked(SlashItem.Actions.Delete)
|
||||
|
||||
val state = vm.controlPanelViewState.value
|
||||
|
||||
assertNotNull(state)
|
||||
assertFalse(state.slashWidget.isVisible)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should send unlinkBlocks UseCase when action Delete happened`() {
|
||||
|
||||
val doc = MockTypicalDocumentFactory.page(root)
|
||||
val block = MockTypicalDocumentFactory.a
|
||||
|
||||
stubInterceptEvents()
|
||||
stubOpenDocument(document = doc)
|
||||
val vm = buildViewModel()
|
||||
|
||||
vm.onStart(root)
|
||||
|
||||
vm.apply {
|
||||
onBlockFocusChanged(
|
||||
id = block.id,
|
||||
hasFocus = true
|
||||
)
|
||||
onSlashEvent(
|
||||
SlashEvent.Start(
|
||||
cursorCoordinate = 100,
|
||||
slashStart = 0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// TESTING
|
||||
|
||||
vm.onSlashItemClicked(SlashItem.Actions.Delete)
|
||||
|
||||
val params = UnlinkBlocks.Params(
|
||||
context = root,
|
||||
targets = listOf(block.id)
|
||||
)
|
||||
verifyBlocking(unlinkBlocks, times(1)) { invoke(params) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should not triggered unlinkBlocks UseCase when no blocks in focus`() {
|
||||
|
||||
val doc = MockTypicalDocumentFactory.page(root)
|
||||
val block = MockTypicalDocumentFactory.a
|
||||
|
||||
stubInterceptEvents()
|
||||
stubOpenDocument(document = doc)
|
||||
val vm = buildViewModel()
|
||||
|
||||
vm.onStart(root)
|
||||
|
||||
vm.apply {
|
||||
onSlashEvent(
|
||||
SlashEvent.Start(
|
||||
cursorCoordinate = 100,
|
||||
slashStart = 0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// TESTING
|
||||
|
||||
vm.onSlashItemClicked(SlashItem.Actions.Delete)
|
||||
|
||||
val params = UnlinkBlocks.Params(
|
||||
context = root,
|
||||
targets = listOf(block.id)
|
||||
)
|
||||
verifyBlocking(unlinkBlocks, times(0)) { invoke(params) }
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region {Action DUPLICATE}
|
||||
@Test
|
||||
fun `should hide slash widget after action Duplicate`() {
|
||||
val doc = MockTypicalDocumentFactory.page(root)
|
||||
val block = MockTypicalDocumentFactory.a
|
||||
|
||||
stubInterceptEvents()
|
||||
stubOpenDocument(document = doc)
|
||||
val vm = buildViewModel()
|
||||
|
||||
vm.onStart(root)
|
||||
|
||||
vm.apply {
|
||||
onBlockFocusChanged(
|
||||
id = block.id,
|
||||
hasFocus = true
|
||||
)
|
||||
onSlashEvent(
|
||||
SlashEvent.Start(
|
||||
cursorCoordinate = 100,
|
||||
slashStart = 0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// TESTING
|
||||
|
||||
vm.onSlashItemClicked(SlashItem.Actions.Duplicate)
|
||||
|
||||
val stateAfter = vm.controlPanelViewState.value
|
||||
|
||||
assertNotNull(stateAfter)
|
||||
assertFalse(stateAfter.slashWidget.isVisible)
|
||||
assertFalse(stateAfter.navigationToolbar.isVisible)
|
||||
assertFalse(stateAfter.mainToolbar.isVisible)
|
||||
|
||||
val params = DuplicateBlock.Params(
|
||||
context = root,
|
||||
original = block.id
|
||||
)
|
||||
verifyBlocking(duplicateBlock, times(1)) { invoke(params) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should invoke duplicateBlock UseCase after action Duplicate`() {
|
||||
val doc = MockTypicalDocumentFactory.page(root)
|
||||
val block = MockTypicalDocumentFactory.a
|
||||
|
||||
stubInterceptEvents()
|
||||
stubOpenDocument(document = doc)
|
||||
val vm = buildViewModel()
|
||||
|
||||
vm.onStart(root)
|
||||
|
||||
vm.apply {
|
||||
onBlockFocusChanged(
|
||||
id = block.id,
|
||||
hasFocus = true
|
||||
)
|
||||
onSlashEvent(
|
||||
SlashEvent.Start(
|
||||
cursorCoordinate = 100,
|
||||
slashStart = 0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// TESTING
|
||||
|
||||
vm.onSlashItemClicked(SlashItem.Actions.Duplicate)
|
||||
|
||||
val params = DuplicateBlock.Params(
|
||||
context = root,
|
||||
original = block.id
|
||||
)
|
||||
verifyBlocking(duplicateBlock, times(1)) { invoke(params) }
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region {Action COPY}
|
||||
@Test
|
||||
fun `should hide slash widget after action Copy`() {
|
||||
val doc = MockTypicalDocumentFactory.page(root)
|
||||
val block = MockTypicalDocumentFactory.a
|
||||
|
||||
stubInterceptEvents()
|
||||
stubOpenDocument(document = doc)
|
||||
val vm = buildViewModel()
|
||||
|
||||
vm.onStart(root)
|
||||
|
||||
vm.apply {
|
||||
onBlockFocusChanged(
|
||||
id = block.id,
|
||||
hasFocus = true
|
||||
)
|
||||
onSlashEvent(
|
||||
SlashEvent.Start(
|
||||
cursorCoordinate = 100,
|
||||
slashStart = 0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// TESTING
|
||||
|
||||
vm.onSlashItemClicked(SlashItem.Actions.Copy)
|
||||
|
||||
val state = vm.controlPanelViewState.value
|
||||
|
||||
assertNotNull(state)
|
||||
assertFalse(state.slashWidget.isVisible)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should send Copy UseCase with null range after action Copy`() {
|
||||
val doc = MockTypicalDocumentFactory.page(root)
|
||||
val block = MockTypicalDocumentFactory.a
|
||||
|
||||
stubInterceptEvents()
|
||||
stubOpenDocument(document = doc)
|
||||
val vm = buildViewModel()
|
||||
|
||||
vm.onStart(root)
|
||||
|
||||
vm.apply {
|
||||
onBlockFocusChanged(
|
||||
id = block.id,
|
||||
hasFocus = true
|
||||
)
|
||||
onSlashEvent(
|
||||
SlashEvent.Start(
|
||||
cursorCoordinate = 100,
|
||||
slashStart = 0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// TESTING
|
||||
|
||||
vm.onSlashItemClicked(SlashItem.Actions.Copy)
|
||||
|
||||
val params = Copy.Params(
|
||||
context = root,
|
||||
range = null,
|
||||
blocks = listOf(block)
|
||||
)
|
||||
|
||||
verifyBlocking(copy, times(1)) { invoke(params) }
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region {Action PASTE}
|
||||
@Test
|
||||
fun `should hide slash widget after action Paste`() {
|
||||
val doc = MockTypicalDocumentFactory.page(root)
|
||||
val block = MockTypicalDocumentFactory.a
|
||||
|
||||
stubInterceptEvents()
|
||||
stubOpenDocument(document = doc)
|
||||
val vm = buildViewModel()
|
||||
|
||||
vm.onStart(root)
|
||||
|
||||
vm.apply {
|
||||
onBlockFocusChanged(
|
||||
id = block.id,
|
||||
hasFocus = true
|
||||
)
|
||||
onSlashEvent(
|
||||
SlashEvent.Start(
|
||||
cursorCoordinate = 100,
|
||||
slashStart = 0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// TESTING
|
||||
|
||||
vm.onSlashItemClicked(SlashItem.Actions.Paste)
|
||||
|
||||
val state = vm.controlPanelViewState.value
|
||||
|
||||
assertNotNull(state)
|
||||
assertFalse(state.slashWidget.isVisible)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should send Paste UseCase with selection range after action Paste`() {
|
||||
val doc = MockTypicalDocumentFactory.page(root)
|
||||
val block = MockTypicalDocumentFactory.a
|
||||
|
||||
stubInterceptEvents()
|
||||
stubOpenDocument(document = doc)
|
||||
val vm = buildViewModel()
|
||||
|
||||
vm.onStart(root)
|
||||
|
||||
vm.apply {
|
||||
onBlockFocusChanged(
|
||||
id = block.id,
|
||||
hasFocus = true
|
||||
)
|
||||
onSelectionChanged(
|
||||
id = block.id,
|
||||
selection = IntRange(3, 3)
|
||||
)
|
||||
onSlashEvent(
|
||||
SlashEvent.Start(
|
||||
cursorCoordinate = 100,
|
||||
slashStart = 0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// TESTING
|
||||
|
||||
vm.onSlashItemClicked(SlashItem.Actions.Paste)
|
||||
val focus = vm.focus.value
|
||||
assertNotNull(focus)
|
||||
|
||||
val params = Paste.Params(
|
||||
context = root,
|
||||
range = IntRange(3, 3),
|
||||
focus = focus
|
||||
)
|
||||
|
||||
verifyBlocking(paste, times(1)) { invoke(params) }
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region {Action MOVE}
|
||||
@Test
|
||||
fun `should hide slash widget after action Move`() {
|
||||
val doc = MockTypicalDocumentFactory.page(root)
|
||||
val block = MockTypicalDocumentFactory.a
|
||||
|
||||
stubInterceptEvents()
|
||||
stubOpenDocument(document = doc)
|
||||
val vm = buildViewModel()
|
||||
|
||||
vm.onStart(root)
|
||||
|
||||
vm.apply {
|
||||
onBlockFocusChanged(
|
||||
id = block.id,
|
||||
hasFocus = true
|
||||
)
|
||||
onSlashEvent(
|
||||
SlashEvent.Start(
|
||||
cursorCoordinate = 100,
|
||||
slashStart = 0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// TESTING
|
||||
|
||||
vm.onSlashItemClicked(SlashItem.Actions.Move)
|
||||
|
||||
val state = vm.controlPanelViewState.value
|
||||
|
||||
assertNotNull(state)
|
||||
assertFalse(state.slashWidget.isVisible)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should enter mode SAM after action Move`() {
|
||||
val doc = MockTypicalDocumentFactory.page(root)
|
||||
val block = MockTypicalDocumentFactory.a
|
||||
|
||||
stubInterceptEvents()
|
||||
stubOpenDocument(document = doc)
|
||||
val vm = buildViewModel()
|
||||
|
||||
vm.onStart(root)
|
||||
|
||||
vm.apply {
|
||||
onBlockFocusChanged(
|
||||
id = block.id,
|
||||
hasFocus = true
|
||||
)
|
||||
onSlashEvent(
|
||||
SlashEvent.Start(
|
||||
cursorCoordinate = 100,
|
||||
slashStart = 0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// TESTING
|
||||
|
||||
vm.onSlashItemClicked(SlashItem.Actions.Move)
|
||||
|
||||
val state = vm.controlPanelViewState.value
|
||||
|
||||
assertNotNull(state)
|
||||
|
||||
val expected = ControlPanelState.Toolbar.MultiSelect(
|
||||
isVisible = true,
|
||||
isScrollAndMoveEnabled = true,
|
||||
isQuickScrollAndMoveMode = true,
|
||||
count = 1
|
||||
)
|
||||
|
||||
assertEquals(expected, state.multiSelect)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should clear focus after action Move`() {
|
||||
val doc = MockTypicalDocumentFactory.page(root)
|
||||
val block = MockTypicalDocumentFactory.a
|
||||
|
||||
stubInterceptEvents()
|
||||
stubOpenDocument(document = doc)
|
||||
val vm = buildViewModel()
|
||||
|
||||
vm.onStart(root)
|
||||
|
||||
vm.apply {
|
||||
onBlockFocusChanged(
|
||||
id = block.id,
|
||||
hasFocus = true
|
||||
)
|
||||
onSlashEvent(
|
||||
SlashEvent.Start(
|
||||
cursorCoordinate = 100,
|
||||
slashStart = 0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// TESTING
|
||||
|
||||
val focusBefore = orchestrator.stores.focus.current()
|
||||
|
||||
assertEquals(block.id, focusBefore.id)
|
||||
|
||||
vm.onSlashItemClicked(SlashItem.Actions.Move)
|
||||
|
||||
val focusAfter = orchestrator.stores.focus.current()
|
||||
|
||||
assertEquals("", focusAfter.id)
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region {Action MOVE TO}
|
||||
@Test
|
||||
fun `should hide slash widget and navigate to move to screen after move to action`() {
|
||||
val doc = MockTypicalDocumentFactory.page(root)
|
||||
val block = MockTypicalDocumentFactory.a
|
||||
|
||||
stubInterceptEvents()
|
||||
stubOpenDocument(document = doc)
|
||||
val vm = buildViewModel()
|
||||
|
||||
vm.onStart(root)
|
||||
|
||||
vm.apply {
|
||||
onBlockFocusChanged(
|
||||
id = block.id,
|
||||
hasFocus = true
|
||||
)
|
||||
onSlashEvent(
|
||||
SlashEvent.Start(
|
||||
cursorCoordinate = 100,
|
||||
slashStart = 0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// TESTING
|
||||
|
||||
vm.onSlashItemClicked(SlashItem.Actions.MoveTo)
|
||||
|
||||
val state = vm.controlPanelViewState.value
|
||||
|
||||
assertNotNull(state)
|
||||
assertFalse(state.slashWidget.isVisible)
|
||||
|
||||
val expected = AppNavigation.Command.OpenMoveToScreen(
|
||||
context = root,
|
||||
targets = listOf(block.id),
|
||||
excluded = listOf()
|
||||
)
|
||||
|
||||
vm.navigation
|
||||
.test()
|
||||
.assertHasValue()
|
||||
.assertValue { event ->
|
||||
(event.peekContent() as AppNavigation.Command.OpenMoveToScreen).let { result ->
|
||||
result == expected
|
||||
}
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region {Action CLEAN STYLE}
|
||||
@Test
|
||||
fun `should hide slash widget after action Clean Style`() {
|
||||
val doc = MockTypicalDocumentFactory.page(root)
|
||||
val block = MockTypicalDocumentFactory.a
|
||||
|
||||
stubInterceptEvents()
|
||||
stubOpenDocument(document = doc)
|
||||
val vm = buildViewModel()
|
||||
|
||||
vm.onStart(root)
|
||||
|
||||
vm.apply {
|
||||
onBlockFocusChanged(
|
||||
id = block.id,
|
||||
hasFocus = true
|
||||
)
|
||||
onSlashEvent(
|
||||
SlashEvent.Start(
|
||||
cursorCoordinate = 100,
|
||||
slashStart = 0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// TESTING
|
||||
|
||||
vm.onSlashItemClicked(SlashItem.Actions.CleanStyle)
|
||||
|
||||
val state = vm.controlPanelViewState.value
|
||||
|
||||
assertNotNull(state)
|
||||
assertFalse(state.slashWidget.isVisible)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should send UpdateText UseCase after action Clean Style`() {
|
||||
|
||||
val header = MockTypicalDocumentFactory.header
|
||||
val title = MockTypicalDocumentFactory.title
|
||||
|
||||
val block = Block(
|
||||
id = MockDataFactory.randomUuid(),
|
||||
fields = Block.Fields.empty(),
|
||||
children = emptyList(),
|
||||
content = Block.Content.Text(
|
||||
text = MockDataFactory.randomString(),
|
||||
marks = listOf(
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(0, 5),
|
||||
type = Block.Content.Text.Mark.Type.BOLD,
|
||||
param = null
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(3, 10),
|
||||
type = Block.Content.Text.Mark.Type.ITALIC,
|
||||
param = null
|
||||
)
|
||||
),
|
||||
style = Block.Content.Text.Style.NUMBERED
|
||||
)
|
||||
)
|
||||
|
||||
val page = Block(
|
||||
id = root,
|
||||
fields = Block.Fields(emptyMap()),
|
||||
content = Block.Content.Smart(
|
||||
type = Block.Content.Smart.Type.PAGE
|
||||
),
|
||||
children = listOf(header.id, block.id)
|
||||
)
|
||||
|
||||
val doc = listOf(
|
||||
page,
|
||||
header,
|
||||
title,
|
||||
block
|
||||
)
|
||||
|
||||
stubInterceptEvents()
|
||||
stubOpenDocument(document = doc)
|
||||
val vm = buildViewModel()
|
||||
|
||||
vm.onStart(root)
|
||||
|
||||
vm.apply {
|
||||
onBlockFocusChanged(
|
||||
id = block.id,
|
||||
hasFocus = true
|
||||
)
|
||||
onSlashEvent(
|
||||
SlashEvent.Start(
|
||||
cursorCoordinate = 100,
|
||||
slashStart = 0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// TESTING
|
||||
|
||||
vm.onSlashItemClicked(SlashItem.Actions.CleanStyle)
|
||||
|
||||
val params = UpdateText.Params(
|
||||
context = root,
|
||||
target = block.id,
|
||||
text = block.content.asText().text,
|
||||
marks = emptyList()
|
||||
)
|
||||
|
||||
verifyBlocking(updateText, times(1)) { invoke(params) }
|
||||
}
|
||||
//endregion
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
package com.anytypeio.anytype.presentation.page.editor
|
||||
|
||||
import android.util.Log
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.anytypeio.anytype.presentation.MockTypicalDocumentFactory
|
||||
import com.anytypeio.anytype.presentation.page.editor.slash.SlashEvent
|
||||
import com.anytypeio.anytype.presentation.util.CoroutinesTestRule
|
||||
import net.lachlanmckee.timberjunit.TimberTestRule
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.mockito.MockitoAnnotations
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class EditorSlashWidgetShowHideTest : EditorPresentationTestSetup() {
|
||||
|
||||
@get:Rule
|
||||
val rule = InstantTaskExecutorRule()
|
||||
|
||||
@get:Rule
|
||||
val coroutineTestRule = CoroutinesTestRule()
|
||||
|
||||
@get:Rule
|
||||
val timberTestRule: TimberTestRule = TimberTestRule.builder()
|
||||
.minPriority(Log.DEBUG)
|
||||
.showThread(true)
|
||||
.showTimestamp(false)
|
||||
.onlyLogWhenTestFails(true)
|
||||
.build()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should show slash widget when slash event start happened`() {
|
||||
|
||||
val doc = MockTypicalDocumentFactory.page(root)
|
||||
val block = MockTypicalDocumentFactory.a
|
||||
|
||||
stubInterceptEvents()
|
||||
stubOpenDocument(document = doc)
|
||||
val vm = buildViewModel()
|
||||
|
||||
vm.onStart(root)
|
||||
|
||||
vm.apply {
|
||||
onBlockFocusChanged(
|
||||
id = block.id,
|
||||
hasFocus = true
|
||||
)
|
||||
onSlashEvent(
|
||||
SlashEvent.Start(
|
||||
cursorCoordinate = 100,
|
||||
slashStart = 0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// TESTING
|
||||
|
||||
val state = vm.controlPanelViewState.value
|
||||
|
||||
assertNotNull(state)
|
||||
assertTrue(state.slashWidget.isVisible)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should hide other toolbars when slash event start happened`() {
|
||||
val doc = MockTypicalDocumentFactory.page(root)
|
||||
val block = MockTypicalDocumentFactory.a
|
||||
|
||||
stubInterceptEvents()
|
||||
stubOpenDocument(document = doc)
|
||||
val vm = buildViewModel()
|
||||
|
||||
vm.onStart(root)
|
||||
|
||||
vm.apply {
|
||||
onBlockFocusChanged(
|
||||
id = block.id,
|
||||
hasFocus = true
|
||||
)
|
||||
onSlashEvent(
|
||||
SlashEvent.Start(
|
||||
cursorCoordinate = 100,
|
||||
slashStart = 0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// TESTING
|
||||
|
||||
val state = vm.controlPanelViewState.value
|
||||
|
||||
assertNotNull(state)
|
||||
assertFalse(state.mainToolbar.isVisible)
|
||||
assertFalse(state.navigationToolbar.isVisible)
|
||||
assertFalse(state.mentionToolbar.isVisible)
|
||||
assertFalse(state.multiSelect.isVisible)
|
||||
assertFalse(state.searchToolbar.isVisible)
|
||||
assertFalse(state.stylingToolbar.isVisible)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue