1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 05:47:05 +09:00

DROID-1432 Template | Enhancement | Flow in Sets/Collections (#266)

This commit is contained in:
Konstantin Ivanov 2023-08-07 16:40:07 +02:00 committed by GitHub
parent 10a8cc89fe
commit d7c0fea015
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 1514 additions and 161 deletions

View file

@ -127,6 +127,9 @@ abstract class TestObjectSetSetup {
@Mock
lateinit var storeOfObjectTypes: StoreOfObjectTypes
@Mock
lateinit var getDefaultType: GetDefaultPageType
private lateinit var getTemplates: GetTemplates
private lateinit var getDefaultPageType: GetDefaultPageType
@ -242,7 +245,9 @@ abstract class TestObjectSetSetup {
setQueryToObjectSet = setQueryToObjectSet,
objectStore = objectStore,
addObjectToCollection = addObjectToCollection,
storeOfObjectTypes = storeOfObjectTypes
storeOfObjectTypes = storeOfObjectTypes,
getDefaultPageType = getDefaultPageType,
getTemplates = getTemplates
)
}

View file

@ -195,7 +195,9 @@ object ObjectSetModule {
@Named("object-set-store") objectStore: ObjectStore,
addObjectToCollection: AddObjectToCollection,
convertObjectToCollection: ConvertObjectToCollection,
storeOfObjectTypes: StoreOfObjectTypes
storeOfObjectTypes: StoreOfObjectTypes,
getDefaultPageType: GetDefaultPageType,
getTemplates: GetTemplates
): ObjectSetViewModelFactory = ObjectSetViewModelFactory(
openObjectSet = openObjectSet,
closeBlock = closeBlock,
@ -225,7 +227,9 @@ object ObjectSetModule {
objectStore = objectStore,
addObjectToCollection = addObjectToCollection,
objectToCollection = convertObjectToCollection,
storeOfObjectTypes = storeOfObjectTypes
storeOfObjectTypes = storeOfObjectTypes,
getDefaultPageType = getDefaultPageType,
getTemplates = getTemplates
)
@JvmStatic

View file

@ -20,6 +20,7 @@ import android.widget.LinearLayout
import android.widget.TextView
import androidx.activity.addCallback
import androidx.appcompat.widget.AppCompatEditText
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.os.bundleOf
@ -30,6 +31,7 @@ import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.setFragmentResultListener
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.DividerItemDecoration
@ -46,7 +48,9 @@ import com.anytypeio.anytype.core_ui.reactive.clicks
import com.anytypeio.anytype.core_ui.reactive.editorActionEvents
import com.anytypeio.anytype.core_ui.reactive.touches
import com.anytypeio.anytype.core_ui.tools.DefaultTextWatcher
import com.anytypeio.anytype.core_ui.views.ButtonPrimarySmallIcon
import com.anytypeio.anytype.core_ui.widgets.FeaturedRelationGroupWidget
import com.anytypeio.anytype.core_ui.widgets.ObjectTypeTemplatesWidget
import com.anytypeio.anytype.core_ui.widgets.StatusBadgeWidget
import com.anytypeio.anytype.core_ui.widgets.text.TextInputWidget
import com.anytypeio.anytype.core_ui.widgets.toolbar.DataViewInfo
@ -142,6 +146,9 @@ open class ObjectSetFragment :
private val addNewButton: TextView
get() = binding.dataViewHeader.addNewButton
private val addNewIconButton: ButtonPrimarySmallIcon
get() = binding.dataViewHeader.addNewIconButton
private val customizeViewButton: ImageView
get() = binding.dataViewHeader.customizeViewButton
@ -213,12 +220,14 @@ open class ObjectSetFragment :
binding.root.setTransitionListener(transitionListener)
with(lifecycleScope) {
subscribe(addNewButton.clicks().throttleFirst()) { vm.onCreateNewDataViewObject() }
subscribe(addNewButton.clicks().throttleFirst()) { vm.proceedWithCreatingNewDataViewObject() }
subscribe(addNewIconButton.buttonClicks()) { vm.proceedWithCreatingNewDataViewObject() }
subscribe(addNewIconButton.iconClicks()) { vm.onNewButtonIconClicked() }
subscribe(dataViewInfo.clicks().throttleFirst()) { type ->
when (type) {
DataViewInfo.TYPE.COLLECTION_NO_ITEMS -> vm.onCreateObjectInCollectionClicked()
DataViewInfo.TYPE.SET_NO_QUERY -> vm.onSelectQueryButtonClicked()
DataViewInfo.TYPE.SET_NO_ITEMS -> vm.onCreateNewDataViewObject()
DataViewInfo.TYPE.SET_NO_ITEMS -> vm.proceedWithCreatingNewDataViewObject()
DataViewInfo.TYPE.INIT -> {}
}
}
@ -311,6 +320,18 @@ open class ObjectSetFragment :
toast("Error while setting the Set query. The query is empty")
}
}
binding.templatesWidget.apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
ObjectTypeTemplatesWidget(
state = vm.templatesWidgetState.collectAsStateWithLifecycle().value,
onDismiss = vm::onDismissTemplatesWidget,
itemClick = vm::onTemplateItemClicked,
scope = lifecycleScope
)
}
}
}
private fun setupWindowInsetAnimation() {
@ -432,7 +453,7 @@ open class ObjectSetFragment :
header.visible()
dataViewHeader.visible()
viewerTitle.isEnabled = true
addNewButton.isEnabled = true
setupNewButtons(state.hasTemplates)
customizeViewButton.isEnabled = true
setCurrentViewerName(state.title)
dataViewInfo.show(DataViewInfo.TYPE.COLLECTION_NO_ITEMS)
@ -445,7 +466,7 @@ open class ObjectSetFragment :
initView.gone()
dataViewHeader.visible()
viewerTitle.isEnabled = true
addNewButton.isEnabled = true
setupNewButtons(state.hasTemplates)
customizeViewButton.isEnabled = true
setCurrentViewerName(state.viewer?.title)
dataViewInfo.hide()
@ -471,12 +492,11 @@ open class ObjectSetFragment :
header.visible()
dataViewHeader.visible()
viewerTitle.isEnabled = true
addNewButton.isEnabled = true
setupNewButtons(state.hasTemplates)
customizeViewButton.isEnabled = true
setCurrentViewerName(state.title)
dataViewInfo.show(type = DataViewInfo.TYPE.SET_NO_ITEMS)
setViewer(viewer = null)
}
is DataViewViewState.Set.Default -> {
topToolbarThreeDotsButton.visible()
@ -485,7 +505,7 @@ open class ObjectSetFragment :
header.visible()
dataViewHeader.visible()
viewerTitle.isEnabled = true
addNewButton.isEnabled = true
setupNewButtons(state.hasTemplates)
customizeViewButton.isEnabled = true
setCurrentViewerName(state.viewer?.title)
setViewer(viewer = state.viewer)
@ -521,6 +541,17 @@ open class ObjectSetFragment :
}
}
private fun setupNewButtons(isTemplatesAllowed: Boolean) {
if (isTemplatesAllowed) {
addNewButton.gone()
addNewIconButton.visible()
} else {
addNewButton.visible()
addNewButton.isEnabled = true
addNewIconButton.gone()
}
}
private fun setViewer(viewer: Viewer?) {
when (viewer) {
is Viewer.GridView -> {
@ -1063,10 +1094,16 @@ open class ObjectSetFragment :
if (childFragmentManager.backStackEntryCount > 0) {
childFragmentManager.popBackStack()
} else {
if (vm.isCustomizeViewPanelVisible.value) {
vm.onHideViewerCustomizeSwiped()
} else {
vm.onSystemBackPressed()
when {
vm.isCustomizeViewPanelVisible.value -> {
vm.onHideViewerCustomizeSwiped()
}
vm.templatesWidgetState.value.showWidget -> {
vm.onDismissTemplatesWidget()
}
else -> {
vm.onSystemBackPressed()
}
}
}
}

View file

@ -2,14 +2,14 @@ package com.anytypeio.anytype.ui.templates
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.anytypeio.anytype.presentation.templates.TemplateSelectViewModel
import com.anytypeio.anytype.presentation.templates.TemplateView
class TemplateSelectAdapter(
private var items: List<TemplateSelectViewModel.TemplateView>,
private var items: List<TemplateView>,
fragment: Fragment
) : FragmentStateAdapter(fragment) {
fun update(newItems: List<TemplateSelectViewModel.TemplateView>) {
fun update(newItems: List<TemplateView>) {
items = newItems
notifyDataSetChanged()
}
@ -18,12 +18,12 @@ class TemplateSelectAdapter(
override fun createFragment(position: Int): Fragment {
return when (val templateView = items[position]) {
is TemplateSelectViewModel.TemplateView.Blank -> TemplateBlankFragment.new(
is TemplateView.Blank -> TemplateBlankFragment.new(
typeId = templateView.typeId,
typeName = templateView.typeName,
layout = templateView.layout
)
is TemplateSelectViewModel.TemplateView.Template -> TemplateFragment.new(
is TemplateView.Template -> TemplateFragment.new(
templateView.id
)
}

View file

@ -148,4 +148,12 @@
android:visibility="gone"
tools:visibility="visible" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/templatesWidget"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.motion.widget.MotionLayout>

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:motion="http://schemas.android.com/apk/res-auto">
xmlns:motion="http://schemas.android.com/apk/res-auto"
xmlns:app="http://schemas.android.com/tools">
<Transition
motion:constraintSetEnd="@+id/end"
@ -90,6 +91,12 @@
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toTopOf="parent"
motion:visibilityMode="ignore" />
<Constraint
android:id="@id/templatesWidget">
<PropertySet
app:applyMotionScene="false"
app:visibilityMode="ignore"/>
</Constraint>
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
@ -171,6 +178,13 @@
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toTopOf="parent"
motion:visibilityMode="ignore" />
<Constraint
android:id="@id/templatesWidget">
<PropertySet
app:applyMotionScene="false"
app:visibilityMode="ignore"/>
</Constraint>
</ConstraintSet>
</MotionScene>

View file

@ -25,6 +25,25 @@ object ObjectTypeIds {
const val SPACE = "ot-space"
const val DEFAULT_OBJECT_TYPE_PREFIX = "ot-"
fun getTypesWithoutTemplates(): List<String> =
listOf(BOOKMARK, NOTE).plus(getFileTypes()).plus(getSetTypes())
.plus(getSystemTypes())
fun getFileTypes(): List<String> = listOf(FILE, IMAGE, AUDIO, VIDEO)
fun getSystemTypes(): List<String> = listOf(
OBJECT_TYPE,
TEMPLATE,
RELATION,
RELATION_OPTION,
DASHBOARD,
DATE,
MarketplaceObjectTypeIds.OBJECT_TYPE,
MarketplaceObjectTypeIds.RELATION
)
fun getSetTypes(): List<String> = listOf(SET, COLLECTION)
}
object MarketplaceObjectTypeIds {

View file

@ -2,6 +2,10 @@ package com.anytypeio.anytype.core_ui.views
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.widget.AppCompatTextView
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.BorderStroke
@ -33,8 +37,10 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.reactive.clicks
import com.anytypeio.anytype.core_ui.views.animations.DotsLoadingIndicator
import com.anytypeio.anytype.core_ui.views.animations.FadeAnimationSpecs
import com.anytypeio.anytype.core_utils.ext.throttleFirst
class ButtonPrimaryXSmall @JvmOverloads constructor(
context: Context,
@ -563,4 +569,30 @@ fun MyWarningButton() {
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp)
)
}
class ButtonPrimarySmallIcon @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
) : LinearLayout(context, attrs) {
private lateinit var button: TextView
private lateinit var icon: ImageView
init {
setup(context)
}
private fun setup(context: Context) {
LayoutInflater.from(context).inflate(R.layout.ds_button_icon, this, true)
button = findViewById(R.id.button)
icon = findViewById(R.id.icon)
}
fun setButtonText(text: String) {
button.text = text
}
fun buttonClicks() = button.clicks().throttleFirst()
fun iconClicks() = icon.clicks().throttleFirst()
}

View file

@ -267,6 +267,14 @@ val Caption2Regular = TextStyle(
letterSpacing = (-0.006).em
)
val Caption2Semibold = TextStyle(
fontFamily = fontInterSemibold,
fontWeight = FontWeight.W600,
fontSize = 11.sp,
lineHeight = 14.sp,
letterSpacing = (-0.006).em
)
//UX/Button/Medium
val ButtonMedium = TextStyle(
fontFamily = fontInterMedium,

View file

@ -0,0 +1,332 @@
package com.anytypeio.anytype.core_ui.widgets
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.FractionalThreshold
import androidx.compose.material.Text
import androidx.compose.material.rememberSwipeableState
import androidx.compose.material.swipeable
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.foundation.noRippleClickable
import com.anytypeio.anytype.core_ui.views.BodyCalloutRegular
import com.anytypeio.anytype.core_ui.views.Caption2Semibold
import com.anytypeio.anytype.core_ui.views.Title1
import com.anytypeio.anytype.presentation.templates.TemplateView
import com.anytypeio.anytype.presentation.widgets.TemplatesWidgetUiState
import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun ObjectTypeTemplatesWidget(
state: TemplatesWidgetUiState,
onDismiss: () -> Unit,
itemClick: (TemplateView) -> Unit,
scope: CoroutineScope
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.BottomCenter,
) {
val currentState by rememberUpdatedState(state)
val swipeableState = rememberSwipeableState(DragStates.VISIBLE)
AnimatedVisibility(
visible = currentState.showWidget,
enter = fadeIn(),
exit = fadeOut(tween(200)
)
) {
Box(
Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.4f))
.noRippleClickable { onDismiss() }
)
}
if (swipeableState.isAnimationRunning) {
DisposableEffect(Unit) {
onDispose {
onDismiss()
}
}
}
if (!currentState.showWidget) {
DisposableEffect(Unit) {
onDispose {
scope.launch { swipeableState.snapTo(DragStates.VISIBLE) }
}
}
}
val sizePx = with(LocalDensity.current) { 312.dp.toPx() }
AnimatedVisibility(
visible = currentState.showWidget,
enter = slideInVertically { it },
exit = slideOutVertically(tween(200)) { it },
modifier = Modifier
.swipeable(
state = swipeableState,
orientation = Orientation.Vertical,
anchors = mapOf(
0f to DragStates.VISIBLE,
sizePx to DragStates.DISMISSED
),
thresholds = { _, _ -> FractionalThreshold(0.3f) }
)
.offset { IntOffset(0, swipeableState.offset.value.roundToInt()) }
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(312.dp)
.padding(start = 8.dp, end = 8.dp, bottom = 31.dp)
.background(
color = colorResource(id = R.color.background_primary),
shape = RoundedCornerShape(size = 16.dp)
)
,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(bottom = 24.dp)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(48.dp)
) {
// Box(
// modifier = Modifier
// .align(Alignment.CenterStart),
// ) {
// Text(
// modifier = Modifier.padding(
// start = 16.dp,
// top = 12.dp,
// bottom = 12.dp,
// end = 16.dp
// ),
// text = stringResource(id = R.string.edit),
// style = BodyCalloutRegular
// )
// }
Box(modifier = Modifier.align(Alignment.Center)) {
Text(
text = stringResource(R.string.type_templates_widget_title),
style = Title1,
color = colorResource(R.color.text_primary)
)
}
// Box(modifier = Modifier.align(Alignment.CenterEnd)) {
// Image(
// modifier = Modifier.padding(
// start = 16.dp,
// top = 12.dp,
// bottom = 12.dp,
// end = 16.dp
// ),
// painter = painterResource(id = R.drawable.ic_default_plus),
// contentDescription = null
// )
// }
}
TemplatesList(currentState.items) {
scope.launch {
onDismiss()
delay(200L)
itemClick(it)
}
}
}
}
}
}
}
@Composable
private fun TemplatesList(
items: List<TemplateView>,
itemClick: (TemplateView) -> Unit
) {
LazyRow(
modifier = Modifier
.padding(top = 8.dp)
.height(224.dp)
.fillMaxWidth(),
contentPadding = PaddingValues(start = 16.dp, end = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
)
{
itemsIndexed(
items = items,
itemContent = { index, item ->
Box(
modifier = Modifier
.border(
width = 1.dp,
color = colorResource(id = R.color.shape_primary),
shape = RoundedCornerShape(size = 16.dp)
)
.height(224.dp)
.width(120.dp)
.clickable {
itemClick(item)
}
) {
Column {
TemplateItemContent(item)
}
}
}
)
}
}
@Composable
private fun TemplateItemContent(item: TemplateView) {
when (item) {
is TemplateView.Blank -> {
Spacer(modifier = Modifier.height(28.dp))
TemplateItemTitle(text = stringResource(id = R.string.blank))
}
is TemplateView.Template -> {
Spacer(modifier = Modifier.height(28.dp))
TemplateItemTitle(text = item.name)
Spacer(modifier = Modifier.height(12.dp))
TemplateItemRectangles()
}
}
}
@Composable
private fun TemplateItemTitle(text: String) {
Text(
modifier = Modifier.padding(
start = 16.dp,
end = 16.dp
),
text = text,
style = Caption2Semibold.copy(
color = colorResource(id = R.color.text_primary)
),
)
}
@Composable
private fun TemplateItemRectangles() {
Column {
Box(
modifier = Modifier
.fillMaxWidth()
.height(6.dp)
.padding(start = 16.dp, end = 16.dp)
.background(
color = colorResource(id = R.color.shape_secondary),
shape = RoundedCornerShape(size = 1.dp)
)
)
Spacer(modifier = Modifier.height(6.dp))
Box(
modifier = Modifier
.fillMaxWidth()
.height(6.dp)
.padding(start = 16.dp, end = 16.dp)
.background(
color = colorResource(id = R.color.shape_secondary),
shape = RoundedCornerShape(size = 1.dp)
)
)
Spacer(modifier = Modifier.height(6.dp))
Box(
modifier = Modifier
.fillMaxWidth()
.height(6.dp)
.padding(start = 16.dp, end = 40.dp)
.background(
color = colorResource(id = R.color.shape_secondary),
shape = RoundedCornerShape(size = 1.dp)
)
)
}
}
private enum class DragStates {
VISIBLE,
DISMISSED
}
@Preview
@Composable
fun ComposablePreview() {
val items = listOf(
TemplateView.Blank(
typeId = "page",
typeName = "Page",
layout = ObjectType.Layout.BASIC.code
),
TemplateView.Template(
id = "1",
name = "Template 1",
typeId = "page",
layout = ObjectType.Layout.BASIC,
image = null,
emoji = null
),
)
val state = TemplatesWidgetUiState(items = items, showWidget = true)
ObjectTypeTemplatesWidget(state = state, onDismiss = {}, itemClick = {}, scope = CoroutineScope(
Dispatchers.Main
)
)
}

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="18dp"
android:height="18dp"
android:viewportWidth="18"
android:viewportHeight="18">
<path
android:pathData="M4,7L9,12L14,7"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="@color/text_color_button_primary_selector"
android:strokeLineCap="round"/>
</vector>

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="28dp"
android:background="@drawable/button_primary_xsmall_selector"
android:orientation="horizontal">
<TextView
android:id="@+id/button"
style="@style/TextView.UXStyle.Captions.1.Medium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:text="New"
android:textColor="@color/text_color_button_primary_selector" />
<View
android:id="@+id/divider"
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="@color/ds_button_icon_divider" />
<ImageView
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:padding="5dp"
android:src="@drawable/ic_arrow_down_18" />
</LinearLayout>

View file

@ -25,29 +25,15 @@
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:layout_marginEnd="@dimen/dp_16"
android:contentDescription="@string/content_description_customize_view_button"
android:paddingStart="@dimen/dp_20"
android:paddingEnd="@dimen/dp_8"
android:src="@drawable/bg_viewer_settings_icon_bg"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/addNewButton"
app:layout_constraintEnd_toStartOf="@+id/barrier"
app:layout_constraintTop_toTopOf="parent"
tools:enabled="true" />
<com.anytypeio.anytype.core_ui.views.ButtonPrimaryXSmall
android:id="@+id/addNewButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/dp_20"
android:paddingStart="10dp"
android:paddingTop="5dp"
android:paddingEnd="10dp"
android:paddingBottom="5dp"
android:text="@string/button_new"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
@ -56,4 +42,38 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="left"
app:barrierMargin="-8dp"
app:constraint_referenced_ids="addNewButton,addNewIconButton" />
<com.anytypeio.anytype.core_ui.views.ButtonPrimaryXSmall
android:id="@+id/addNewButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/dp_20"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:text="@string/button_new"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.anytypeio.anytype.core_ui.views.ButtonPrimarySmallIcon
android:id="@+id/addNewIconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:paddingEnd="@dimen/dp_20"
android:paddingBottom="10dp"
android:text="@string/button_new"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--typography, buttons 05.04-->
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"

View file

@ -95,5 +95,6 @@
<color name="snackbar_background">#FFFFFF</color>
<color name="widget_divider">#24DAD7CA</color>
<color name="ds_button_icon_divider">#1A000000</color>
</resources>

View file

@ -210,5 +210,6 @@
<color name="snackbar_background">#000000</color>
<color name="widget_divider">#E3E3E3</color>
<color name="ds_button_icon_divider">#1AFFFFFF</color>
</resources>

View file

@ -643,5 +643,7 @@
<string name="menu_type_open_set">Open set of %1$s</string>
<string name="menu_type_create_set">Create set of %1$s</string>
<string name="toast_active_view_delete">Current view cannot be deleted</string>
<string name="type_templates_widget_title">Select template</string>
<string name="blank">Blank</string>
</resources>

View file

@ -33,12 +33,12 @@ class CreateDataViewObject(
return when (params) {
is Params.SetByType -> {
val command = Command.CreateObject(
template = resolveTemplateForNewObject(type = params.type),
template = params.template,
prefilled = resolveSetByTypePrefilledObjectData(
filters = params.filters,
type = params.type
),
internalFlags = listOf(InternalFlags.ShouldSelectTemplate)
internalFlags = listOf()
)
val result = repo.createObject(command)
Result(
@ -49,16 +49,13 @@ class CreateDataViewObject(
is Params.SetByRelation -> {
val type = resolveDefaultObjectType()
val command = Command.CreateObject(
template = resolveTemplateForNewObject(type = type),
template = params.template,
prefilled = resolveSetByRelationPrefilledObjectData(
filters = params.filters,
relations = params.relations,
type = type
),
internalFlags = listOf(
InternalFlags.ShouldSelectType,
InternalFlags.ShouldSelectTemplate
)
internalFlags = listOf()
)
val result = repo.createObject(command)
Result(
@ -66,19 +63,16 @@ class CreateDataViewObject(
objectType = type
)
}
Params.Collection -> {
is Params.Collection -> {
val type = resolveDefaultObjectType()
val command = Command.CreateObject(
template = resolveTemplateForNewObject(type = type),
template = params.templateId,
prefilled = resolveSetByRelationPrefilledObjectData(
filters = emptyList(),
relations = emptyList(),
type = type
),
internalFlags = listOf(
InternalFlags.ShouldSelectType,
InternalFlags.ShouldSelectTemplate
)
internalFlags = listOf()
)
val result = repo.createObject(command)
Result(
@ -180,15 +174,19 @@ class CreateDataViewObject(
sealed class Params {
data class SetByType(
val type: Id,
val filters: List<DVFilter>
val filters: List<DVFilter>,
val template: Id?
) : Params()
data class SetByRelation(
val filters: List<DVFilter>,
val relations: List<Id>
val relations: List<Id>,
val template: Id?
) : Params()
object Collection : Params()
data class Collection(
val templateId: Id?
) : Params()
}
data class Result(

View file

@ -1,7 +1,9 @@
package com.anytypeio.anytype.domain.templates
import com.anytypeio.anytype.core_models.Block
import com.anytypeio.anytype.core_models.DVFilter
import com.anytypeio.anytype.core_models.DVFilterCondition
import com.anytypeio.anytype.core_models.DVSort
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.ObjectTypeIds
@ -10,7 +12,6 @@ import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.base.ResultInteractor
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
/**
@ -28,13 +29,13 @@ class GetTemplates(
filters = listOf(
DVFilter(
relation = Relations.IS_ARCHIVED,
condition = DVFilterCondition.EQUAL,
value = false
condition = DVFilterCondition.NOT_EQUAL,
value = true
),
DVFilter(
relation = Relations.IS_DELETED,
condition = DVFilterCondition.EQUAL,
value = false
condition = DVFilterCondition.NOT_EQUAL,
value = true
),
DVFilter(
relation = Relations.TYPE,
@ -47,11 +48,25 @@ class GetTemplates(
value = params.type
)
),
keys = listOf(Relations.ID, Relations.NAME),
sorts = emptyList(),
keys = listOf(
Relations.ID,
Relations.NAME,
Relations.LAYOUT,
Relations.ICON_EMOJI,
Relations.ICON_IMAGE,
Relations.ICON_OPTION,
Relations.COVER_ID,
Relations.COVER_TYPE
),
sorts = listOf(
DVSort(
relationKey = Relations.CREATED_DATE,
type = Block.Content.DataView.Sort.Type.DESC
)
),
fulltext = "",
offset = 0,
limit = 0
limit = 100
).map { obj ->
ObjectWrapper.Basic(obj)
}

View file

@ -4,7 +4,7 @@ kotlinVersion = '1.7.10'
androidxCoreVersion = "1.10.1"
androidxComposeVersion = '1.3.1'
androidxComposeVersion = '1.4.3'
composeKotlinCompilerVersion = '1.3.1'
composeMaterial3Version = '1.1.1'
composeMaterialVersion = '1.3.1'

View file

@ -1,10 +1,12 @@
package com.anytypeio.anytype.presentation.objects
import com.anytypeio.anytype.core_models.ObjectTypeIds
import com.anytypeio.anytype.core_models.ObjectTypeIds.BOOKMARK
import com.anytypeio.anytype.core_models.ObjectTypeIds.COLLECTION
import com.anytypeio.anytype.core_models.ObjectTypeIds.SET
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.presentation.mapper.toObjectTypeView
import com.anytypeio.anytype.presentation.objects.SupportedLayouts.editorLayouts
/**
* The method allows you to get object type views for using in the editor and set
@ -49,4 +51,16 @@ fun List<ObjectWrapper.Type>.getObjectTypeViewsForSBPage(
return@forEach
}
return result.sortedWith(ObjectTypeViewComparator())
}
/**
*
* This method is used to understand if objects of this type can use templates.
*
* @return `true` if templates are allowed for this type of object, `false` otherwise.
*/
fun ObjectWrapper.Type.isTemplatesAllowed(): Boolean {
val showTemplates = !ObjectTypeIds.getTypesWithoutTemplates().contains(this.id)
val allowedObject = editorLayouts.contains(recommendedLayout)
return showTemplates && allowedObject
}

View file

@ -7,15 +7,15 @@ sealed class DataViewViewState {
sealed class Collection : DataViewViewState() {
object NoView : Collection()
data class NoItems(val title: String) : Collection()
data class Default(val viewer: Viewer?) : Collection()
data class NoItems(val title: String, val hasTemplates: Boolean = false) : Collection()
data class Default(val viewer: Viewer?, val hasTemplates: Boolean = false) : Collection()
}
sealed class Set : DataViewViewState() {
object NoQuery : Set()
object NoView : Set()
data class NoItems(val title: String) : Set()
data class Default(val viewer: Viewer?) : Set()
data class NoItems(val title: String, val hasTemplates: Boolean) : Set()
data class Default(val viewer: Viewer?, val hasTemplates: Boolean) : Set()
}
object Init: DataViewViewState()

View file

@ -11,6 +11,7 @@ import com.anytypeio.anytype.core_models.Event.Command.DataView.UpdateView.DVSor
import com.anytypeio.anytype.core_models.Event.Command.DataView.UpdateView.DVViewerFields
import com.anytypeio.anytype.core_models.Event.Command.DataView.UpdateView.DVViewerRelationUpdate
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.ObjectTypeIds
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.core_models.RelationFormat
@ -20,22 +21,26 @@ import com.anytypeio.anytype.core_utils.ext.addAfterIndexInLine
import com.anytypeio.anytype.core_utils.ext.mapInPlace
import com.anytypeio.anytype.core_utils.ext.moveAfterIndexInLine
import com.anytypeio.anytype.core_utils.ext.moveOnTop
import com.anytypeio.anytype.domain.launch.GetDefaultPageType
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
import com.anytypeio.anytype.domain.objects.StoreOfRelations
import com.anytypeio.anytype.presentation.editor.cover.CoverImageHashProvider
import com.anytypeio.anytype.presentation.editor.editor.model.BlockView
import com.anytypeio.anytype.presentation.objects.ObjectIcon
import com.anytypeio.anytype.presentation.objects.getProperName
import com.anytypeio.anytype.presentation.objects.isTemplatesAllowed
import com.anytypeio.anytype.presentation.relations.ObjectRelationView
import com.anytypeio.anytype.presentation.relations.ObjectSetConfig.ID_KEY
import com.anytypeio.anytype.presentation.relations.isSystemKey
import com.anytypeio.anytype.presentation.relations.title
import com.anytypeio.anytype.presentation.relations.type
import com.anytypeio.anytype.presentation.relations.view
import com.anytypeio.anytype.presentation.sets.model.ObjectView
import com.anytypeio.anytype.presentation.sets.model.SimpleRelationView
import com.anytypeio.anytype.presentation.sets.model.Viewer
import com.anytypeio.anytype.presentation.sets.state.ObjectState
import timber.log.Timber
import com.anytypeio.anytype.presentation.templates.TemplateView
fun ObjectState.DataView.featuredRelations(
ctx: Id,
@ -370,6 +375,35 @@ fun ObjectState.DataView.filterOutDeletedAndMissingObjects(query: List<Id>): Lis
return query.filter(::isValidObject)
}
suspend fun ObjectState.DataView.Set.isTemplatesAllowed(
setOfValue: List<Id>,
storeOfObjectTypes: StoreOfObjectTypes,
getDefaultPageType: GetDefaultPageType
): Boolean {
val objectDetails = details[setOfValue.first()]?.map.orEmpty()
return when (objectDetails.type){
ObjectTypeIds.OBJECT_TYPE -> {
val objectWrapper = ObjectWrapper.Type(objectDetails)
objectWrapper.isTemplatesAllowed()
}
ObjectTypeIds.RELATION -> {
//We have set of relations, need to check default object type
storeOfObjectTypes.isTemplatesAllowedForDefaultType(getDefaultPageType)
}
else -> false
}
}
suspend fun StoreOfObjectTypes.isTemplatesAllowedForDefaultType(getDefaultPageType: GetDefaultPageType): Boolean {
try {
val defaultObjectType = getDefaultPageType.run(Unit).type ?: return false
val defaultObjType = get(defaultObjectType) ?: return false
return defaultObjType.isTemplatesAllowed()
} catch (e: Exception){
return false
}
}
private fun ObjectState.DataView.isValidObject(objectId: Id): Boolean {
val objectDetails = details[objectId] ?: return false
val objectWrapper = ObjectWrapper.Basic(objectDetails.map)
@ -394,4 +428,22 @@ fun Viewer.isEmpty(): Boolean =
is Viewer.GridView -> this.rows.isEmpty()
is Viewer.ListView -> this.items.isEmpty()
is Viewer.Unsupported -> false
}
}
fun ObjectWrapper.Basic.toTemplateView(typeId: Id): TemplateView.Template {
return TemplateView.Template(
id = id,
name = name.orEmpty(),
typeId = typeId,
emoji = iconEmoji,
image = iconImage,
layout = layout ?: ObjectType.Layout.BASIC
)
}
fun ObjectWrapper.Basic.toTemplateViewBlank(typeId: Id): TemplateView.Blank {
return TemplateView.Blank(
typeId = typeId,
layout = layout?.code ?: ObjectType.Layout.BASIC.code
)
}

View file

@ -26,6 +26,7 @@ import com.anytypeio.anytype.domain.cover.SetDocCoverImage
import com.anytypeio.anytype.domain.dataview.interactor.CreateDataViewObject
import com.anytypeio.anytype.domain.error.Error
import com.anytypeio.anytype.domain.event.interactor.InterceptEvents
import com.anytypeio.anytype.domain.launch.GetDefaultPageType
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.`object`.ConvertObjectToCollection
import com.anytypeio.anytype.domain.`object`.UpdateDetail
@ -40,6 +41,7 @@ import com.anytypeio.anytype.domain.search.DataViewSubscriptionContainer
import com.anytypeio.anytype.domain.sets.OpenObjectSet
import com.anytypeio.anytype.domain.sets.SetQueryToObjectSet
import com.anytypeio.anytype.domain.status.InterceptThreadStatus
import com.anytypeio.anytype.domain.templates.GetTemplates
import com.anytypeio.anytype.domain.unsplash.DownloadUnsplashImage
import com.anytypeio.anytype.domain.workspace.WorkspaceManager
import com.anytypeio.anytype.presentation.common.Action
@ -54,6 +56,7 @@ import com.anytypeio.anytype.presentation.extension.sendAnalyticsObjectCreateEve
import com.anytypeio.anytype.presentation.extension.sendAnalyticsRelationValueEvent
import com.anytypeio.anytype.presentation.navigation.AppNavigation
import com.anytypeio.anytype.presentation.navigation.SupportNavigation
import com.anytypeio.anytype.presentation.objects.isTemplatesAllowed
import com.anytypeio.anytype.presentation.relations.ObjectRelationView
import com.anytypeio.anytype.presentation.relations.ObjectSetConfig.DEFAULT_LIMIT
import com.anytypeio.anytype.presentation.relations.RelationListViewModel
@ -63,7 +66,9 @@ import com.anytypeio.anytype.presentation.sets.model.Viewer
import com.anytypeio.anytype.presentation.sets.state.ObjectState
import com.anytypeio.anytype.presentation.sets.state.ObjectStateReducer
import com.anytypeio.anytype.presentation.sets.subscription.DataViewSubscription
import com.anytypeio.anytype.presentation.templates.TemplateView
import com.anytypeio.anytype.presentation.util.Dispatcher
import com.anytypeio.anytype.presentation.widgets.TemplatesWidgetUiState
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
@ -116,7 +121,9 @@ class ObjectSetViewModel(
private val objectStore: ObjectStore,
private val addObjectToCollection: AddObjectToCollection,
private val objectToCollection: ConvertObjectToCollection,
private val storeOfObjectTypes: StoreOfObjectTypes
private val storeOfObjectTypes: StoreOfObjectTypes,
private val getDefaultPageType: GetDefaultPageType,
private val getTemplates: GetTemplates
) : ViewModel(), SupportNavigation<EventWrapper<AppNavigation.Command>> {
val status = MutableStateFlow(SyncStatus.UNKNOWN)
@ -144,12 +151,15 @@ class ObjectSetViewModel(
MutableStateFlow(DataViewViewState.Init)
val currentViewer = _currentViewer
private val _templateViews = MutableStateFlow<List<TemplateView>>(emptyList())
private val _header = MutableStateFlow<SetOrCollectionHeaderState>(
SetOrCollectionHeaderState.None
)
val header: StateFlow<SetOrCollectionHeaderState> = _header
val isCustomizeViewPanelVisible = MutableStateFlow(false)
val templatesWidgetState = MutableStateFlow(TemplatesWidgetUiState.empty())
@Deprecated("could be deleted")
val isLoading = MutableStateFlow(false)
@ -173,6 +183,7 @@ class ObjectSetViewModel(
urlBuilder = urlBuilder,
coverImageHashProvider = coverImageHashProvider
)
gettingTemplatesForDataViewState(state)
}
}
@ -405,9 +416,10 @@ class ObjectSetViewModel(
combine(
database.index,
stateReducer.state,
session.currentViewerId
) { dataViewState, objectState, currentViewId ->
processViewState(dataViewState, objectState, currentViewId)
session.currentViewerId,
_templateViews
) { dataViewState, objectState, currentViewId, templates ->
processViewState(dataViewState, objectState, currentViewId, templates)
}.distinctUntilChanged().collect { viewState ->
Timber.d("subscribeToDataViewViewer, newViewerState:[$viewState]")
_currentViewer.value = viewState
@ -418,18 +430,21 @@ class ObjectSetViewModel(
private suspend fun processViewState(
dataViewState: DataViewState,
objectState: ObjectState,
currentViewId: String?
currentViewId: String?,
templates: List<TemplateView>
): DataViewViewState {
return when (objectState) {
is ObjectState.DataView.Collection -> processCollectionState(
dataViewState,
objectState,
currentViewId
dataViewState = dataViewState,
objectState = objectState,
currentViewId = currentViewId,
templates = templates
)
is ObjectState.DataView.Set -> processSetState(
dataViewState,
objectState,
currentViewId
dataViewState = dataViewState,
objectState = objectState,
currentViewId = currentViewId,
templates = templates
)
ObjectState.Init -> DataViewViewState.Init
ObjectState.ErrorLayout -> DataViewViewState.Error(msg = "Wrong layout, couldn't open object")
@ -439,7 +454,8 @@ class ObjectSetViewModel(
private suspend fun processCollectionState(
dataViewState: DataViewState,
objectState: ObjectState.DataView.Collection,
currentViewId: String?
currentViewId: String?,
templates: List<TemplateView>
): DataViewViewState {
if (!objectState.isInitialized) return DataViewViewState.Init
@ -461,8 +477,26 @@ class ObjectSetViewModel(
when {
viewer == null -> DataViewViewState.Collection.NoView
viewer.isEmpty() -> DataViewViewState.Collection.NoItems(title = viewer.title)
else -> DataViewViewState.Collection.Default(viewer = viewer)
viewer.isEmpty() -> {
val isTemplatesPresent = templates.isNotEmpty() &&
storeOfObjectTypes.isTemplatesAllowedForDefaultType(
getDefaultPageType = getDefaultPageType
)
DataViewViewState.Collection.NoItems(
title = viewer.title,
hasTemplates = isTemplatesPresent
)
}
else -> {
val isTemplatesPresent = templates.isNotEmpty() &&
storeOfObjectTypes.isTemplatesAllowedForDefaultType(
getDefaultPageType = getDefaultPageType
)
DataViewViewState.Collection.Default(
viewer = viewer,
hasTemplates = isTemplatesPresent
)
}
}
}
}
@ -471,7 +505,8 @@ class ObjectSetViewModel(
private suspend fun processSetState(
dataViewState: DataViewState,
objectState: ObjectState.DataView.Set,
currentViewId: String?
currentViewId: String?,
templates: List<TemplateView>
): DataViewViewState {
if (!objectState.isInitialized) return DataViewViewState.Init
@ -503,8 +538,30 @@ class ObjectSetViewModel(
when {
query.isEmpty() || setOfValue.isEmpty() -> DataViewViewState.Set.NoQuery
render == null -> DataViewViewState.Set.NoView
render.isEmpty() -> DataViewViewState.Set.NoItems(title = render.title)
else -> DataViewViewState.Set.Default(viewer = render)
render.isEmpty() -> {
val isTemplatesAllowed = templates.isNotEmpty() &&
objectState.isTemplatesAllowed(
setOfValue,
storeOfObjectTypes,
getDefaultPageType
)
DataViewViewState.Set.NoItems(
title = render.title,
hasTemplates = isTemplatesAllowed
)
}
else -> {
val isTemplatesAllowed = templates.isNotEmpty() &&
objectState.isTemplatesAllowed(
setOfValue,
storeOfObjectTypes,
getDefaultPageType
)
DataViewViewState.Set.Default(
viewer = render,
hasTemplates = isTemplatesAllowed
)
}
}
}
}
@ -810,16 +867,29 @@ class ObjectSetViewModel(
}
}
fun onCreateNewDataViewObject() {
Timber.d("onCreateNewRecord, ")
fun proceedWithCreatingNewDataViewObject(templatesId: Id? = null) {
Timber.d("proceedWithCreatingNewDataViewObject, templatesId:[$templatesId]")
val state = stateReducer.state.value.dataViewState() ?: return
when (state) {
is ObjectState.DataView.Collection -> proceedWithAddingObjectToCollection()
is ObjectState.DataView.Set -> proceedWithCreatingSetObject(state)
is ObjectState.DataView.Collection -> proceedWithAddingObjectToCollection(templatesId)
is ObjectState.DataView.Set -> proceedWithCreatingSetObject(state, templatesId)
}
}
private fun proceedWithCreatingSetObject(currentState: ObjectState.DataView.Set) {
fun onNewButtonIconClicked() {
Timber.d("onNewButtonIconClicked, ")
templatesWidgetState.value = TemplatesWidgetUiState(
items = _templateViews.value,
showWidget = true
)
}
fun onDismissTemplatesWidget() {
Timber.d("onDismissTemplatesWidget, ")
templatesWidgetState.value = templatesWidgetState.value.copy(showWidget = false)
}
private fun proceedWithCreatingSetObject(currentState: ObjectState.DataView.Set, templateId: Id?) {
if (isRestrictionPresent(DataViewRestriction.CREATE_OBJECT)) {
toast(NOT_ALLOWED)
} else {
@ -849,7 +919,8 @@ class ObjectSetViewModel(
proceedWithCreatingDataViewObject(
CreateDataViewObject.Params.SetByType(
type = sourceId,
filters = viewer.filters
filters = viewer.filters,
template = templateId
)
)
}
@ -858,7 +929,8 @@ class ObjectSetViewModel(
proceedWithCreatingDataViewObject(
CreateDataViewObject.Params.SetByRelation(
filters = viewer.filters,
relations = setObject.setOf
relations = setObject.setOf,
template = templateId
)
)
}
@ -879,8 +951,11 @@ class ObjectSetViewModel(
proceedWithAddingObjectToCollection()
}
private fun proceedWithAddingObjectToCollection() {
proceedWithCreatingDataViewObject(CreateDataViewObject.Params.Collection) { result ->
private fun proceedWithAddingObjectToCollection(templateId: Id? = null) {
val createObjectParams = CreateDataViewObject.Params.Collection(
templateId = templateId
)
proceedWithCreatingDataViewObject(createObjectParams) { result ->
val params = AddObjectToCollection.Params(
ctx = context,
after = "",
@ -917,7 +992,7 @@ class ObjectSetViewModel(
private suspend fun proceedWithNewDataViewObject(params: CreateDataViewObject.Params, newObject: Id) {
when (params) {
CreateDataViewObject.Params.Collection -> {
is CreateDataViewObject.Params.Collection -> {
proceedWithOpeningObject(newObject)
}
is CreateDataViewObject.Params.SetByRelation -> {
@ -1391,6 +1466,84 @@ class ObjectSetViewModel(
}
}
// region TEMPLATES
private suspend fun gettingTemplatesForDataViewState(state: ObjectState.DataView) {
when (state) {
is ObjectState.DataView.Collection -> {
proceedWithGettingTemplates(typeId = null)
}
is ObjectState.DataView.Set -> {
val sourceId = proceedWithGettingSetSourceId(state)
val sourceMap = state.details[sourceId] ?: return
when (sourceMap.type.firstOrNull()) {
ObjectTypeIds.RELATION -> proceedWithGettingTemplates(typeId = null)
ObjectTypeIds.OBJECT_TYPE -> proceedWithGettingTemplates(typeId = sourceId)
else -> { Timber.d("Ignoring type of source") }
}
}
}
}
private suspend fun proceedWithGettingTemplates(typeId: Id?) {
val objectType = resolveObjectType(typeId)
if (objectType?.isTemplatesAllowed() == true) {
viewModelScope.launch {
getTemplates.async(GetTemplates.Params(objectType.id)).fold(
onSuccess = { templates ->
if (templates.isNotEmpty()) {
_templateViews.value =
listOf(templates.first().toTemplateViewBlank(objectType.id)) +
templates.map { it.toTemplateView(typeId = objectType.id) }
}
},
onFailure = { e ->
Timber.e(e, "Error getting templates for type ${objectType.id}")
}
)
}
} else {
Timber.d("Templates are not allowed for type:[${objectType?.id}]")
}
}
private suspend fun resolveObjectType(type: Id?): ObjectWrapper.Type? {
return if (type == null) {
val defaultObjectType = getDefaultPageType.run(Unit).type ?: return null
storeOfObjectTypes.get(defaultObjectType)
} else {
storeOfObjectTypes.get(type)
}
}
fun onTemplateItemClicked(item: TemplateView) {
when(item) {
is TemplateView.Blank -> {
templatesWidgetState.value = TemplatesWidgetUiState.empty()
proceedWithCreatingNewDataViewObject()
}
is TemplateView.Template -> {
templatesWidgetState.value = TemplatesWidgetUiState.empty()
proceedWithCreatingNewDataViewObject(templatesId = item.id)
}
}
}
private fun proceedWithGettingSetSourceId(currentState: ObjectState.DataView.Set): Id? {
if (isRestrictionPresent(DataViewRestriction.CREATE_OBJECT) || !currentState.isInitialized) {
toast(NOT_ALLOWED)
return null
}
val setObject = ObjectWrapper.Basic(currentState.details[context]?.map ?: emptyMap())
val sourceId = setObject.setOf.singleOrNull()
if (sourceId == null) {
Timber.e("Unable to define a source for a new object.")
toast("Unable to define a source for a new object.")
}
return sourceId
}
//endregion
companion object {
const val NOT_ALLOWED = "Not allowed for this set"
const val NOT_ALLOWED_CELL = "Not allowed for this cell"

View file

@ -10,6 +10,7 @@ import com.anytypeio.anytype.domain.collections.AddObjectToCollection
import com.anytypeio.anytype.domain.cover.SetDocCoverImage
import com.anytypeio.anytype.domain.dataview.interactor.CreateDataViewObject
import com.anytypeio.anytype.domain.event.interactor.InterceptEvents
import com.anytypeio.anytype.domain.launch.GetDefaultPageType
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.`object`.ConvertObjectToCollection
import com.anytypeio.anytype.domain.objects.ObjectStore
@ -22,6 +23,7 @@ import com.anytypeio.anytype.domain.search.DataViewSubscriptionContainer
import com.anytypeio.anytype.domain.sets.OpenObjectSet
import com.anytypeio.anytype.domain.sets.SetQueryToObjectSet
import com.anytypeio.anytype.domain.status.InterceptThreadStatus
import com.anytypeio.anytype.domain.templates.GetTemplates
import com.anytypeio.anytype.domain.unsplash.DownloadUnsplashImage
import com.anytypeio.anytype.domain.workspace.WorkspaceManager
import com.anytypeio.anytype.presentation.common.Action
@ -60,7 +62,9 @@ class ObjectSetViewModelFactory(
private val objectStore: ObjectStore,
private val addObjectToCollection: AddObjectToCollection,
private val objectToCollection: ConvertObjectToCollection,
private val storeOfObjectTypes: StoreOfObjectTypes
private val storeOfObjectTypes: StoreOfObjectTypes,
private val getDefaultPageType: GetDefaultPageType,
private val getTemplates: GetTemplates
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
@ -93,7 +97,9 @@ class ObjectSetViewModelFactory(
objectStore = objectStore,
addObjectToCollection = addObjectToCollection,
objectToCollection = objectToCollection,
storeOfObjectTypes = storeOfObjectTypes
storeOfObjectTypes = storeOfObjectTypes,
getDefaultPageType = getDefaultPageType,
getTemplates = getTemplates
) as T
}
}

View file

@ -32,7 +32,7 @@ sealed class ObjectState {
override val details: Map<Id, Block.Fields> = emptyMap(),
override val objectRestrictions: List<ObjectRestriction> = emptyList(),
override val dataViewRestrictions: List<DataViewRestrictions> = emptyList(),
override val objectRelationLinks: List<RelationLink> = emptyList()
override val objectRelationLinks: List<RelationLink> = emptyList(),
) : DataView() {
override val isInitialized get() = blocks.any { it.content is DV }

View file

@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.core_utils.common.EventWrapper
import com.anytypeio.anytype.domain.base.fold
@ -71,7 +72,16 @@ class TemplateSelectViewModel(
layout = objType.recommendedLayout?.code ?: 0
)
)
addAll(templates.map { TemplateView.Template(it.id) })
addAll(templates.map {
TemplateView.Template(
id = it.id,
name = it.name.orEmpty(),
layout = it.layout ?: ObjectType.Layout.BASIC,
emoji = it.iconEmoji.orEmpty(),
image = it.iconImage.orEmpty(),
typeId = objType.id,
)
})
}
_viewState.emit(
ViewState.Success(
@ -142,11 +152,4 @@ class TemplateSelectViewModel(
object ErrorGettingType : ViewState()
}
sealed class TemplateView {
data class Blank(
val typeId: Id, val typeName: String, val layout: Int
) : TemplateView()
data class Template(val id: Id) : TemplateView()
}
}

View file

@ -0,0 +1,20 @@
package com.anytypeio.anytype.presentation.templates
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ObjectType
sealed class TemplateView {
data class Blank(
val typeId: Id, val typeName: String = "", val layout: Int
) : TemplateView()
data class Template(
val id: Id,
val name: String,
val typeId: Id,
val layout: ObjectType.Layout,
val emoji: String?,
val image: String?
) : TemplateView()
}

View file

@ -0,0 +1,15 @@
package com.anytypeio.anytype.presentation.widgets
import com.anytypeio.anytype.presentation.templates.TemplateView
data class TemplatesWidgetUiState(
val items: List<TemplateView>,
val showWidget: Boolean
) {
companion object {
fun empty() = TemplatesWidgetUiState(
items = emptyList(),
showWidget = false
)
}
}

View file

@ -50,6 +50,7 @@ class CollectionAddRelationTest : ObjectSetViewModelTestSetup() {
MockitoAnnotations.openMocks(this)
viewModel = givenViewModel()
objectCollection = MockCollection(context = root)
stubGetDefaultPageType()
}
@After
@ -62,7 +63,6 @@ class CollectionAddRelationTest : ObjectSetViewModelTestSetup() {
@Test
fun `should add new relation to data view`() = runTest {
// SETUP
stubWorkspaceManager(objectCollection.workspaceId)
stubStoreOfRelations(objectCollection)
stubSubscriptionResults(

View file

@ -30,6 +30,7 @@ class CollectionDataViewUpdateTest : ObjectSetViewModelTestSetup() {
MockitoAnnotations.openMocks(this)
viewModel = givenViewModel()
objectCollection = MockCollection(context = root)
stubGetDefaultPageType()
}
@After

View file

@ -0,0 +1,155 @@
package com.anytypeio.anytype.presentation.collections
import com.anytypeio.anytype.core_models.Block
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.ObjectTypeIds
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.domain.templates.GetTemplates
import com.anytypeio.anytype.presentation.sets.ObjectSetViewModel
import com.anytypeio.anytype.presentation.sets.main.ObjectSetViewModelTestSetup
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoInteractions
@OptIn(ExperimentalCoroutinesApi::class)
class CollectionTemplatesDelegateTest: ObjectSetViewModelTestSetup() {
private lateinit var viewModel: ObjectSetViewModel
private lateinit var mockCollection: MockCollection
@Before
fun setup() {
MockitoAnnotations.openMocks(this)
viewModel = givenViewModel()
}
@After
fun after() {
rule.advanceTime()
}
@Test
fun `should start get templates when collection with default type allowed templates`() = runTest {
val defaultType = ObjectTypeIds.PAGE
val defaultTypeName = "Page"
val defaultTypeMap = mapOf(
Relations.ID to defaultType,
Relations.TYPE to ObjectTypeIds.OBJECT_TYPE,
Relations.RECOMMENDED_LAYOUT to ObjectType.Layout.BASIC.code.toDouble(),
Relations.NAME to defaultTypeName
)
mockCollection = MockCollection(context = root)
// SETUP
stubWorkspaceManager(mockCollection.workspaceId)
stubInterceptEvents()
stubInterceptThreadStatus()
stubStoreOfObjectTypes(defaultTypeMap)
stubGetDefaultPageType(type = defaultType, name = defaultTypeName)
stubGetTemplates(type = defaultType)
val details = Block.Details(
details = mapOf(
root to Block.Fields(
mapOf(
Relations.ID to root,
Relations.LAYOUT to ObjectType.Layout.COLLECTION.code.toDouble(),
Relations.TYPE to ObjectTypeIds.COLLECTION
)
)
)
)
stubOpenObject(
doc = listOf(mockCollection.header, mockCollection.title, mockCollection.dataView),
details = details
)
stubSubscriptionResults(
subscription = mockCollection.subscriptionId,
collection = root,
workspace = mockCollection.workspaceId,
storeOfRelations = storeOfRelations,
keys = mockCollection.dvKeys,
objects = listOf(mockCollection.obj1, mockCollection.obj2),
dvSorts = mockCollection.sorts
)
// TESTING
viewModel.onStart(ctx = root)
advanceUntilIdle()
verify(getTemplates, times(1)).async(
GetTemplates.Params(type = defaultType)
)
verifyNoInteractions(createDataViewObject)
}
@Test
fun `should not start get templates when collection with default type not allowed templates`() = runTest {
val defaultType = ObjectTypeIds.NOTE
val defaultTypeName = "Note"
val defaultTypeMap = mapOf(
Relations.ID to defaultType,
Relations.TYPE to ObjectTypeIds.OBJECT_TYPE,
Relations.RECOMMENDED_LAYOUT to ObjectType.Layout.NOTE.code.toDouble(),
Relations.NAME to defaultTypeName
)
mockCollection = MockCollection(context = root)
// SETUP
stubWorkspaceManager(mockCollection.workspaceId)
stubInterceptEvents()
stubInterceptThreadStatus()
stubStoreOfObjectTypes(defaultTypeMap)
stubGetDefaultPageType(type = defaultType, name = defaultTypeName)
val details = Block.Details(
details = mapOf(
root to Block.Fields(
mapOf(
Relations.ID to root,
Relations.LAYOUT to ObjectType.Layout.COLLECTION.code.toDouble(),
Relations.TYPE to ObjectTypeIds.COLLECTION
)
)
)
)
stubOpenObject(
doc = listOf(mockCollection.header, mockCollection.title, mockCollection.dataView),
details = details
)
stubSubscriptionResults(
subscription = mockCollection.subscriptionId,
collection = root,
workspace = mockCollection.workspaceId,
storeOfRelations = storeOfRelations,
keys = mockCollection.dvKeys,
objects = listOf(mockCollection.obj1, mockCollection.obj2),
dvSorts = mockCollection.sorts
)
// TESTING
viewModel.onStart(ctx = root)
advanceUntilIdle()
viewModel.onNewButtonIconClicked()
advanceUntilIdle()
verifyNoInteractions(getTemplates)
verifyNoInteractions(createDataViewObject)
}
}

View file

@ -20,7 +20,7 @@ import com.anytypeio.anytype.presentation.sets.subscription.DefaultDataViewSubsc
import com.anytypeio.anytype.test_utils.MockDataFactory
import net.bytebuddy.utility.RandomString
class MockSet(context: String, setOfValue: String = "setOf-${RandomString.make()}") {
class MockSet(context: String, val setOfValue: String = "setOf-${RandomString.make()}") {
val root = context
val title =
@ -34,7 +34,7 @@ class MockSet(context: String, setOfValue: String = "setOf-${RandomString.make()
)
val workspaceId = "workspace-${RandomString.make()}"
val subscriptionId = DefaultDataViewSubscription.getSubscriptionId(context)
val setOf = setOfValue
val setOf get() = setOfValue
val setOfNote = ObjectTypeIds.NOTE
// RELATION OBJECTS
@ -180,7 +180,8 @@ class MockSet(context: String, setOfValue: String = "setOf-${RandomString.make()
setOf to Block.Fields(
map = mapOf(
Relations.ID to setOf,
Relations.TYPE to ObjectTypeIds.OBJECT_TYPE
Relations.TYPE to ObjectTypeIds.OBJECT_TYPE,
Relations.RECOMMENDED_LAYOUT to ObjectType.Layout.BASIC.code.toDouble(),
)
)
)

View file

@ -31,6 +31,7 @@ class ObjectCreateTest : ObjectSetViewModelTestSetup() {
fun setup() {
MockitoAnnotations.openMocks(this)
viewModel = givenViewModel()
stubGetDefaultPageType()
}
@After
@ -69,7 +70,8 @@ class ObjectCreateTest : ObjectSetViewModelTestSetup() {
doReturn(Resultat.success(result)).`when`(createDataViewObject).async(
CreateDataViewObject.Params.SetByType(
type = ObjectTypeIds.NOTE,
filters = mockObjectSet.filters
filters = mockObjectSet.filters,
template = null
)
)
doReturn(Resultat.success(Unit)).`when`(closeBlock).async(mockObjectSet.root)
@ -79,7 +81,7 @@ class ObjectCreateTest : ObjectSetViewModelTestSetup() {
advanceUntilIdle()
viewModel.onCreateNewDataViewObject()
viewModel.proceedWithCreatingNewDataViewObject()
advanceUntilIdle()
@ -87,7 +89,8 @@ class ObjectCreateTest : ObjectSetViewModelTestSetup() {
async(
CreateDataViewObject.Params.SetByType(
type = ObjectTypeIds.NOTE,
filters = mockObjectSet.filters
filters = mockObjectSet.filters,
template = null
)
)
}
@ -125,7 +128,8 @@ class ObjectCreateTest : ObjectSetViewModelTestSetup() {
doReturn(Resultat.success(result)).`when`(createDataViewObject).async(
CreateDataViewObject.Params.SetByType(
type = ObjectTypeIds.PAGE,
filters = mockObjectSet.filters
filters = mockObjectSet.filters,
template = null
)
)
doReturn(Resultat.success(Unit)).`when`(closeBlock).async(mockObjectSet.root)
@ -136,7 +140,7 @@ class ObjectCreateTest : ObjectSetViewModelTestSetup() {
advanceUntilIdle()
viewModel.onCreateNewDataViewObject()
viewModel.proceedWithCreatingNewDataViewObject()
assertIs<ObjectSetCommand.Modal.SetNameForCreatedObject>(commandFlow.awaitItem())
@ -146,7 +150,8 @@ class ObjectCreateTest : ObjectSetViewModelTestSetup() {
async(
CreateDataViewObject.Params.SetByType(
type = ObjectTypeIds.PAGE,
filters = mockObjectSet.filters
filters = mockObjectSet.filters,
template = null
)
)
}
@ -184,7 +189,8 @@ class ObjectCreateTest : ObjectSetViewModelTestSetup() {
doReturn(Resultat.success(result)).`when`(createDataViewObject).async(
CreateDataViewObject.Params.SetByRelation(
relations = listOf(mockObjectSet.relationObject3.id),
filters = mockObjectSet.filters
filters = mockObjectSet.filters,
template = null
)
)
doReturn(Resultat.success(Unit)).`when`(closeBlock).async(mockObjectSet.root)
@ -194,7 +200,7 @@ class ObjectCreateTest : ObjectSetViewModelTestSetup() {
advanceUntilIdle()
viewModel.onCreateNewDataViewObject()
viewModel.proceedWithCreatingNewDataViewObject()
advanceUntilIdle()
@ -202,7 +208,8 @@ class ObjectCreateTest : ObjectSetViewModelTestSetup() {
async(
CreateDataViewObject.Params.SetByRelation(
relations = listOf(mockObjectSet.relationObject3.id),
filters = mockObjectSet.filters
filters = mockObjectSet.filters,
template = null
)
)
}
@ -244,7 +251,9 @@ class ObjectCreateTest : ObjectSetViewModelTestSetup() {
objectType = ObjectTypeIds.NOTE
)
doReturn(Resultat.success(result)).`when`(createDataViewObject).async(
CreateDataViewObject.Params.Collection
CreateDataViewObject.Params.Collection(
templateId = null
)
)
doReturn(Resultat.success(Unit)).`when`(closeBlock).async(objectCollection.root)
@ -253,12 +262,12 @@ class ObjectCreateTest : ObjectSetViewModelTestSetup() {
advanceUntilIdle()
viewModel.onCreateNewDataViewObject()
viewModel.proceedWithCreatingNewDataViewObject()
advanceUntilIdle()
verifyBlocking(createDataViewObject, times(1)) {
async(CreateDataViewObject.Params.Collection)
async(CreateDataViewObject.Params.Collection(null))
}
verifyBlocking(closeBlock, times(1)) { async(objectCollection.root)}

View file

@ -1,14 +1,21 @@
package com.anytypeio.anytype.presentation.collections
import app.cash.turbine.testIn
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.ObjectTypeIds
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.core_models.StubObject
import com.anytypeio.anytype.presentation.sets.DataViewViewState
import com.anytypeio.anytype.presentation.sets.ObjectSetViewModel
import com.anytypeio.anytype.presentation.sets.SetOrCollectionHeaderState
import com.anytypeio.anytype.presentation.sets.main.ObjectSetViewModelTestSetup
import com.anytypeio.anytype.presentation.sets.model.Viewer
import com.anytypeio.anytype.presentation.sets.state.ObjectState
import com.anytypeio.anytype.test_utils.MockDataFactory
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertIs
import kotlin.test.assertTrue
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
@ -29,6 +36,7 @@ class ObjectStateCollectionViewTest : ObjectSetViewModelTestSetup() {
MockitoAnnotations.openMocks(this)
viewModel = givenViewModel()
mockObjectCollection = MockCollection(context = root)
stubGetDefaultPageType()
}
@After
@ -487,6 +495,7 @@ class ObjectStateCollectionViewTest : ObjectSetViewModelTestSetup() {
),
dvSorts = listOf(mockObjectCollection.sortGallery)
)
stubGetTemplates()
// TESTING
viewModel.onStart(ctx = root)
@ -510,4 +519,167 @@ class ObjectStateCollectionViewTest : ObjectSetViewModelTestSetup() {
assertEquals(mockObjectCollection.obj5.id, rows[4].objectId)
}
}
@Test
fun `should be collection with templates present when default type is custom with proper recommended layout`() = runTest {
// SETUP
val defaultObjectType = MockDataFactory.randomString()
val defaultObjectTypeName = "CustomName"
stubWorkspaceManager(mockObjectCollection.workspaceId)
stubInterceptEvents()
stubInterceptThreadStatus()
stubGetDefaultPageType(defaultObjectType, defaultObjectTypeName)
stubGetTemplates(
type = defaultObjectType,
templates = listOf(StubObject(objectType = defaultObjectType))
)
stubOpenObject(
doc = listOf(
mockObjectCollection.header,
mockObjectCollection.title,
mockObjectCollection.dataView
),
details = mockObjectCollection.details
)
stubStoreOfObjectTypes(
mapOf(
Relations.ID to defaultObjectType,
Relations.RECOMMENDED_LAYOUT to ObjectType.Layout.BASIC.code.toDouble(),
Relations.NAME to defaultObjectTypeName
)
)
stubStoreOfRelations(mockObjectCollection)
stubSubscriptionResults(
subscription = mockObjectCollection.subscriptionId,
workspace = mockObjectCollection.workspaceId,
collection = root,
storeOfRelations = storeOfRelations,
keys = mockObjectCollection.dvKeys,
dvSorts = mockObjectCollection.sorts
)
// TESTING
viewModel.onStart(ctx = root)
val viewerFlow = viewModel.currentViewer.testIn(backgroundScope)
val stateFlow = stateReducer.state.testIn(backgroundScope)
// ASSERT STATES
assertIs<ObjectState.Init>(stateFlow.awaitItem())
assertIs<DataViewViewState.Init>(viewerFlow.awaitItem())
assertIs<ObjectState.DataView.Collection>(stateFlow.awaitItem())
val item = viewerFlow.awaitItem()
assertIs<DataViewViewState.Collection.NoItems>(item)
assertTrue(item.hasTemplates)
}
@Test
fun `should be collection without templates allowed when default type is custom with not proper recommended layout`() = runTest {
// SETUP
val defaultObjectType = MockDataFactory.randomString()
val defaultObjectTypeName = "CustomName"
stubWorkspaceManager(mockObjectCollection.workspaceId)
stubInterceptEvents()
stubInterceptThreadStatus()
stubGetDefaultPageType(defaultObjectType, defaultObjectTypeName)
stubOpenObject(
doc = listOf(
mockObjectCollection.header,
mockObjectCollection.title,
mockObjectCollection.dataView
),
details = mockObjectCollection.details
)
stubStoreOfObjectTypes(
mapOf(
Relations.ID to defaultObjectType,
Relations.RECOMMENDED_LAYOUT to ObjectType.Layout.SET.code.toDouble(),
Relations.NAME to defaultObjectTypeName
)
)
stubStoreOfRelations(mockObjectCollection)
stubSubscriptionResults(
subscription = mockObjectCollection.subscriptionId,
workspace = mockObjectCollection.workspaceId,
collection = root,
storeOfRelations = storeOfRelations,
keys = mockObjectCollection.dvKeys,
dvSorts = mockObjectCollection.sorts
)
// TESTING
viewModel.onStart(ctx = root)
val viewerFlow = viewModel.currentViewer.testIn(backgroundScope)
val stateFlow = stateReducer.state.testIn(backgroundScope)
// ASSERT STATES
assertIs<ObjectState.Init>(stateFlow.awaitItem())
assertIs<DataViewViewState.Init>(viewerFlow.awaitItem())
assertIs<ObjectState.DataView.Collection>(stateFlow.awaitItem())
val item = viewerFlow.awaitItem()
assertIs<DataViewViewState.Collection.NoItems>(item)
assertFalse(item.hasTemplates)
}
@Test
fun `should be collection without templates allowed when default type is NOTE`() = runTest {
// SETUP
val defaultObjectType = ObjectTypeIds.NOTE
val defaultObjectTypeName = "Note"
stubWorkspaceManager(mockObjectCollection.workspaceId)
stubInterceptEvents()
stubInterceptThreadStatus()
stubGetDefaultPageType(defaultObjectType, defaultObjectTypeName)
stubOpenObject(
doc = listOf(
mockObjectCollection.header,
mockObjectCollection.title,
mockObjectCollection.dataView
),
details = mockObjectCollection.details
)
stubStoreOfObjectTypes(
mapOf(
Relations.ID to defaultObjectType,
Relations.RECOMMENDED_LAYOUT to ObjectType.Layout.NOTE.code.toDouble(),
Relations.NAME to defaultObjectTypeName
)
)
stubStoreOfRelations(mockObjectCollection)
stubSubscriptionResults(
subscription = mockObjectCollection.subscriptionId,
workspace = mockObjectCollection.workspaceId,
collection = root,
storeOfRelations = storeOfRelations,
keys = mockObjectCollection.dvKeys,
dvSorts = mockObjectCollection.sorts
)
// TESTING
viewModel.onStart(ctx = root)
val viewerFlow = viewModel.currentViewer.testIn(backgroundScope)
val stateFlow = stateReducer.state.testIn(backgroundScope)
// ASSERT STATES
assertIs<ObjectState.Init>(stateFlow.awaitItem())
assertIs<DataViewViewState.Init>(viewerFlow.awaitItem())
assertIs<ObjectState.DataView.Collection>(stateFlow.awaitItem())
val item = viewerFlow.awaitItem()
assertIs<DataViewViewState.Collection.NoItems>(item)
assertFalse(item.hasTemplates)
}
}

View file

@ -7,14 +7,18 @@ import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.ObjectTypeIds
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.core_models.StubObject
import com.anytypeio.anytype.presentation.relations.ObjectSetConfig
import com.anytypeio.anytype.presentation.search.ObjectSearchConstants
import com.anytypeio.anytype.presentation.sets.DataViewViewState
import com.anytypeio.anytype.presentation.sets.ObjectSetViewModel
import com.anytypeio.anytype.presentation.sets.main.ObjectSetViewModelTestSetup
import com.anytypeio.anytype.presentation.sets.state.ObjectState
import com.anytypeio.anytype.test_utils.MockDataFactory
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertIs
import kotlin.test.assertTrue
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
@ -408,4 +412,92 @@ class ObjectStateSetViewTest : ObjectSetViewModelTestSetup() {
stateFlow.ensureAllEventsConsumed()
viewerFlow.ensureAllEventsConsumed()
}
@Test
fun `displaying set with templates present when opening object set of pages with templates`() = runTest {
// SETUP
mockObjectSet = MockSet(context = root, setOfValue = ObjectTypeIds.PAGE)
val pageTypeMap = mapOf(
Relations.ID to ObjectTypeIds.PAGE,
Relations.TYPE to ObjectTypeIds.OBJECT_TYPE,
Relations.RECOMMENDED_LAYOUT to ObjectType.Layout.BASIC.code.toDouble(),
Relations.NAME to MockDataFactory.randomString()
)
stubWorkspaceManager(mockObjectSet.workspaceId)
stubInterceptEvents()
stubInterceptThreadStatus()
stubOpenObject(
doc = listOf(mockObjectSet.header, mockObjectSet.title, mockObjectSet.dataView),
details = mockObjectSet.details
)
stubSubscriptionResults(
subscription = mockObjectSet.subscriptionId,
workspace = mockObjectSet.workspaceId,
storeOfRelations = storeOfRelations,
keys = mockObjectSet.dvKeys,
sources = listOf(ObjectTypeIds.PAGE),
dvFilters = mockObjectSet.filters
)
stubStoreOfObjectTypes(pageTypeMap)
stubGetTemplates(
type = ObjectTypeIds.PAGE,
templates = listOf(StubObject(objectType = ObjectTypeIds.PAGE)
)
)
// TESTING
viewModel.onStart(ctx = root)
// ASSERT STATES
val viewerFlow = viewModel.currentViewer.testIn(backgroundScope)
val stateFlow = stateReducer.state.testIn(backgroundScope)
// ASSERT STATES
assertIs<ObjectState.Init>(stateFlow.awaitItem())
assertIs<ObjectState.DataView.Set>(stateFlow.awaitItem())
assertIs<DataViewViewState.Init>(viewerFlow.awaitItem())
val item = viewerFlow.awaitItem()
assertIs<DataViewViewState.Set.NoItems>(item)
assertTrue(item.hasTemplates)
}
@Test
fun `displaying set without templates allowed when opening object set of notes`() = runTest {
// SETUP
mockObjectSet = MockSet(context = root, setOfValue = ObjectTypeIds.NOTE)
stubWorkspaceManager(mockObjectSet.workspaceId)
stubInterceptEvents()
stubInterceptThreadStatus()
stubOpenObject(
doc = listOf(mockObjectSet.header, mockObjectSet.title, mockObjectSet.dataView),
details = mockObjectSet.details
)
stubSubscriptionResults(
subscription = mockObjectSet.subscriptionId,
workspace = mockObjectSet.workspaceId,
storeOfRelations = storeOfRelations,
keys = mockObjectSet.dvKeys,
sources = listOf(ObjectTypeIds.NOTE),
dvFilters = mockObjectSet.filters
)
// TESTING
viewModel.onStart(ctx = root)
// ASSERT STATES
val viewerFlow = viewModel.currentViewer.testIn(backgroundScope)
val stateFlow = stateReducer.state.testIn(backgroundScope)
// ASSERT STATES
assertIs<ObjectState.Init>(stateFlow.awaitItem())
assertIs<ObjectState.DataView.Set>(stateFlow.awaitItem())
assertIs<DataViewViewState.Init>(viewerFlow.awaitItem())
val item = viewerFlow.awaitItem()
assertIs<DataViewViewState.Set.NoItems>(item)
assertFalse(item.hasTemplates)
}
}

View file

@ -0,0 +1,81 @@
package com.anytypeio.anytype.presentation.objects
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.ObjectTypeIds
import com.anytypeio.anytype.core_models.StubObjectType
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import org.junit.Test
class ObjectTypeExtensionsTest {
@Test
fun `isTemplateAllowed returns true when type is not in getNoTemplates and recommendedLayout is in editorLayouts`() {
val objectType = StubObjectType(
id = ObjectTypeIds.PAGE,
recommendedLayout = ObjectType.Layout.BASIC.code.toDouble()
)
val result = objectType.isTemplatesAllowed()
assertTrue(result)
}
@Test
fun `isTemplateAllowed returns false when type is not in getNoTemplates and recommendedLayout is not in editorLayouts`() {
val objectType = StubObjectType(
id = ObjectTypeIds.PAGE,
recommendedLayout = ObjectType.Layout.DASHBOARD.code.toDouble()
)
val result = objectType.isTemplatesAllowed()
assertFalse(result)
}
@Test
fun `isTemplateAllowed returns false when type is BOOKMARK`() {
val objectType = StubObjectType(
id = ObjectTypeIds.BOOKMARK,
recommendedLayout = ObjectType.Layout.BASIC.code.toDouble()
)
val result = objectType.isTemplatesAllowed()
assertFalse(result)
}
@Test
fun `isTemplateAllowed returns false when type is FILE`() {
val objectType = StubObjectType(
id = ObjectTypeIds.FILE,
recommendedLayout = ObjectType.Layout.BASIC.code.toDouble()
)
val result = objectType.isTemplatesAllowed()
assertFalse(result)
}
@Test
fun `isTemplateAllowed returns false when type is NOTE`() {
val objectType = StubObjectType(
id = ObjectTypeIds.NOTE,
recommendedLayout = ObjectType.Layout.NOTE.code.toDouble()
)
val result = objectType.isTemplatesAllowed()
assertFalse(result)
}
@Test
fun `isTemplateAllowed returns false when type is SET`() {
val objectType = StubObjectType(
id = ObjectTypeIds.SET,
recommendedLayout = ObjectType.Layout.SET.code.toDouble()
)
val result = objectType.isTemplatesAllowed()
assertFalse(result)
}
@Test
fun `isTemplateAllowed returns false when type is COLLECTION`() {
val objectType = StubObjectType(
id = ObjectTypeIds.COLLECTION,
recommendedLayout = ObjectType.Layout.COLLECTION.code.toDouble()
)
val result = objectType.isTemplatesAllowed()
assertFalse(result)
}
}

View file

@ -12,6 +12,7 @@ import com.anytypeio.anytype.core_models.StubFilter
import com.anytypeio.anytype.core_models.StubRelationLink
import com.anytypeio.anytype.core_models.StubSort
import com.anytypeio.anytype.core_models.StubTitle
import com.anytypeio.anytype.domain.launch.GetDefaultPageType
import com.anytypeio.anytype.presentation.sets.state.DefaultObjectStateReducer
import com.anytypeio.anytype.presentation.sets.state.ObjectState
import com.anytypeio.anytype.presentation.sets.state.ObjectStateReducer

View file

@ -32,6 +32,7 @@ class ObjectSetConvertToCollectionTest : ObjectSetViewModelTestSetup() {
MockitoAnnotations.openMocks(this)
viewModel = givenViewModel()
mockObjectSet = MockSet(context = root)
stubGetDefaultPageType()
}
@After

View file

@ -57,7 +57,8 @@ class ObjectSetDataViewObjectCreateTest : ObjectSetViewModelTestSetup() {
doReturn(Unit).`when`(createDataViewObject).async(
CreateDataViewObject.Params.SetByType(
type = mockObjectSet.setOf,
filters = mockObjectSet.filters
filters = mockObjectSet.filters,
template = null
)
)
@ -72,14 +73,15 @@ class ObjectSetDataViewObjectCreateTest : ObjectSetViewModelTestSetup() {
val second = awaitItem()
assertIs<DataViewViewState.Set.Default>(second)
viewModel.onCreateNewDataViewObject()
viewModel.proceedWithCreatingNewDataViewObject()
advanceUntilIdle()
verifyBlocking(createDataViewObject, times(1)) {
async(
CreateDataViewObject.Params.SetByType(
type = mockObjectSet.setOf,
filters = mockObjectSet.filters
filters = mockObjectSet.filters,
template = null
)
)
}

View file

@ -42,7 +42,7 @@ class ObjectSetInitializationTest : ObjectSetViewModelTestSetup() {
// TESTING
viewModel.onStart(ctx = root)
viewModel.onCreateNewDataViewObject()
viewModel.proceedWithCreatingNewDataViewObject()
// ASSERT
verifyNoInteractions(createObject)

View file

@ -118,7 +118,7 @@ class ObjectSetRestrictionsTest : ObjectSetViewModelTestSetup() {
// ASSERT ERROR TOAST
viewModel.toasts.test {
viewModel.onCreateNewDataViewObject()
viewModel.proceedWithCreatingNewDataViewObject()
assertEquals(ObjectSetViewModel.NOT_ALLOWED, awaitItem())
cancelAndIgnoreRemainingEvents()
}

View file

@ -16,6 +16,7 @@ import com.anytypeio.anytype.core_models.restrictions.DataViewRestrictions
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.base.Either
import com.anytypeio.anytype.domain.base.Result
import com.anytypeio.anytype.domain.base.Resultat
import com.anytypeio.anytype.domain.block.interactor.UpdateText
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import com.anytypeio.anytype.domain.collections.AddObjectToCollection
@ -23,6 +24,7 @@ import com.anytypeio.anytype.domain.config.Gateway
import com.anytypeio.anytype.domain.cover.SetDocCoverImage
import com.anytypeio.anytype.domain.dataview.interactor.CreateDataViewObject
import com.anytypeio.anytype.domain.event.interactor.InterceptEvents
import com.anytypeio.anytype.domain.launch.GetDefaultPageType
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.`object`.ConvertObjectToCollection
import com.anytypeio.anytype.domain.`object`.UpdateDetail
@ -54,6 +56,7 @@ import com.anytypeio.anytype.presentation.sets.ObjectSetPaginator
import com.anytypeio.anytype.presentation.sets.ObjectSetSession
import com.anytypeio.anytype.presentation.sets.ObjectSetViewModel
import com.anytypeio.anytype.presentation.sets.state.DefaultObjectStateReducer
import com.anytypeio.anytype.presentation.sets.state.ObjectStateReducer
import com.anytypeio.anytype.presentation.sets.subscription.DataViewSubscription
import com.anytypeio.anytype.presentation.sets.subscription.DefaultDataViewSubscription
import com.anytypeio.anytype.presentation.sets.updateFormatForSubscription
@ -147,6 +150,9 @@ open class ObjectSetViewModelTestSetup {
@Mock
lateinit var storeOfObjectTypes: StoreOfObjectTypes
@Mock
lateinit var getDefaultPageType: GetDefaultPageType
var stateReducer = DefaultObjectStateReducer()
lateinit var dataViewSubscriptionContainer: DataViewSubscriptionContainer
@ -165,6 +171,9 @@ open class ObjectSetViewModelTestSetup {
val urlBuilder: UrlBuilder get() = UrlBuilder(gateway)
val defaultObjectPageType = MockDataFactory.randomString()
val defaultObjectPageTypeName = MockDataFactory.randomString()
lateinit var dispatchers: AppCoroutineDispatchers
fun givenViewModel(): ObjectSetViewModel {
@ -210,7 +219,9 @@ open class ObjectSetViewModelTestSetup {
addObjectToCollection = addObjectToCollection,
objectToCollection = objectToCollection,
setQueryToObjectSet = setQueryToObjectSet,
storeOfObjectTypes = storeOfObjectTypes
storeOfObjectTypes = storeOfObjectTypes,
getDefaultPageType = getDefaultPageType,
getTemplates = getTemplates
)
}
@ -262,25 +273,6 @@ open class ObjectSetViewModelTestSetup {
}
}
fun stubGetTemplates(
type: String,
templates: List<Id> = emptyList()
) {
getTemplates.stub {
onBlocking {
run(
GetTemplates.Params(type)
)
} doReturn templates.map {
ObjectWrapper.Basic(
map = mapOf(
Relations.ID to it
)
)
}
}
}
suspend fun stubWorkspaceManager(workspace: Id) {
workspaceManager.setCurrentWorkspace(workspace)
}
@ -353,9 +345,27 @@ open class ObjectSetViewModelTestSetup {
)
}
fun stubStoreOfObjectTypes() {
fun stubStoreOfObjectTypes(map: Map<String, Any?> = emptyMap()) {
storeOfObjectTypes.stub {
onBlocking { get(any()) } doReturn ObjectWrapper.Type(map = emptyMap())
onBlocking { get(any()) } doReturn ObjectWrapper.Type(map = map)
}
}
fun stubGetDefaultPageType(type: String = defaultObjectPageType, name: String = defaultObjectPageTypeName) {
getDefaultPageType.stub {
onBlocking { run(Unit) } doReturn GetDefaultPageType.Response(type = type, name = name)
}
}
fun stubGetTemplates(
type: String = MockDataFactory.randomString(),
templates: List<ObjectWrapper.Basic> = emptyList()
) {
val params = GetTemplates.Params(
type = type
)
getTemplates.stub {
onBlocking { async(params) }.thenReturn(Resultat.success(templates))
}
}
}

View file

@ -48,7 +48,6 @@ class SetByRelationTest : ObjectSetViewModelTestSetup() {
doc = listOf(mockObjectSet.header, mockObjectSet.title, mockObjectSet.dataView),
details = mockObjectSet.details
)
stubGetTemplates(type = mockObjectSet.setOf)
stubSubscriptionResults(
subscription = mockObjectSet.subscriptionId,
workspace = mockObjectSet.workspaceId,
@ -61,7 +60,8 @@ class SetByRelationTest : ObjectSetViewModelTestSetup() {
doReturn(Unit).`when`(createDataViewObject).async(
CreateDataViewObject.Params.SetByType(
type = mockObjectSet.setOf,
filters = mockObjectSet.filters
filters = mockObjectSet.filters,
template = null
)
)
@ -76,14 +76,15 @@ class SetByRelationTest : ObjectSetViewModelTestSetup() {
val second = awaitItem()
assertIs<DataViewViewState.Set.Default>(second)
viewModel.onCreateNewDataViewObject()
viewModel.proceedWithCreatingNewDataViewObject()
advanceUntilIdle()
verifyBlocking(createDataViewObject, times(1)) {
async(
CreateDataViewObject.Params.SetByType(
type = mockObjectSet.setOf,
filters = mockObjectSet.filters
filters = mockObjectSet.filters,
template = null
)
)
}
@ -101,7 +102,6 @@ class SetByRelationTest : ObjectSetViewModelTestSetup() {
doc = listOf(mockObjectSet.header, mockObjectSet.title, mockObjectSet.dataView),
details = mockObjectSet.detailsSetByRelation
)
stubGetTemplates(type = mockObjectSet.setOf)
stubSubscriptionResults(
subscription = mockObjectSet.subscriptionId,
workspace = mockObjectSet.workspaceId,
@ -114,7 +114,8 @@ class SetByRelationTest : ObjectSetViewModelTestSetup() {
doReturn(Unit).`when`(createDataViewObject).async(
CreateDataViewObject.Params.SetByRelation(
relations = listOf(mockObjectSet.relationObject3.id),
filters = mockObjectSet.filters
filters = mockObjectSet.filters,
template = null
)
)
@ -129,14 +130,15 @@ class SetByRelationTest : ObjectSetViewModelTestSetup() {
val second = awaitItem()
assertIs<DataViewViewState.Set.Default>(second)
viewModel.onCreateNewDataViewObject()
viewModel.proceedWithCreatingNewDataViewObject()
advanceUntilIdle()
verifyBlocking(createDataViewObject, times(1)) {
async(
CreateDataViewObject.Params.SetByRelation(
relations = listOf(mockObjectSet.relationObject3.id),
filters = mockObjectSet.filters
filters = mockObjectSet.filters,
template = null
)
)
}

View file

@ -37,6 +37,27 @@
app:layout_constraintStart_toStartOf="@+id/textView21"
app:layout_constraintTop_toBottomOf="@+id/button8" />
<TextView
android:id="@+id/textView26"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="XSmall button with arrow"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button13" />
<com.anytypeio.anytype.core_ui.views.ButtonPrimarySmallIcon
android:id="@+id/btnSmallArrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:clickable="true"
app:buttonTitle="New"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView26"/>
<TextView
android:id="@+id/textView22"
android:layout_width="wrap_content"
@ -45,7 +66,7 @@
android:text="Small Button Primary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button13" />
app:layout_constraintTop_toBottomOf="@+id/btnSmallArrow" />
<com.anytypeio.anytype.core_ui.views.ButtonPrimarySmall
android:id="@+id/button9"

View file

@ -94,6 +94,6 @@
android:text="Dialogs Compose"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button20" />
app:layout_constraintTop_toBottomOf="@+id/btnCompose" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -62,7 +62,8 @@ fun StubObjectType(
iconEmoji: String? = null,
isReadOnly: Boolean? = null,
isHidden: Boolean? = null,
sourceObject: Id? = null
sourceObject: Id? = null,
recommendedLayout: Double? = null
): ObjectWrapper.Type = ObjectWrapper.Type(
map = mapOf(
Relations.ID to id,
@ -76,6 +77,7 @@ fun StubObjectType(
Relations.ICON_EMOJI to iconEmoji,
Relations.IS_READ_ONLY to isReadOnly,
Relations.IS_HIDDEN to isHidden,
Relations.SOURCE_OBJECT to sourceObject
Relations.SOURCE_OBJECT to sourceObject,
Relations.RECOMMENDED_LAYOUT to recommendedLayout
)
)