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

DROID-2905 Primitives | Epic | Foundation for primitives (#2098)

Co-authored-by: Evgenii Kozlov <enklave.mare.balticum@protonmail.com>
This commit is contained in:
Konstantin Ivanov 2025-02-28 20:47:43 +01:00 committed by GitHub
parent 88aa30d64b
commit 4bc1e060f3
Signed by: github
GPG key ID: B5690EEEBB952194
153 changed files with 10877 additions and 1616 deletions

View file

@ -0,0 +1,61 @@
plugins {
id "com.android.library"
id "kotlin-android"
alias(libs.plugins.compose.compiler)
}
android {
defaultConfig {
buildConfigField "boolean", "USE_NEW_WINDOW_INSET_API", "true"
buildConfigField "boolean", "USE_EDGE_TO_EDGE", "true"
}
buildFeatures {
compose true
}
namespace 'com.anytypeio.anytype.feature_object_type'
}
dependencies {
implementation project(':domain')
implementation project(':core-ui')
implementation project(':analytics')
implementation project(':core-models')
implementation project(':core-utils')
implementation project(':localization')
implementation project(':presentation')
implementation project(':library-emojifier')
compileOnly libs.javaxInject
implementation libs.lifecycleViewModel
implementation libs.lifecycleRuntime
implementation libs.appcompat
implementation libs.compose
implementation libs.composeFoundation
implementation libs.composeToolingPreview
implementation libs.composeMaterial3
implementation libs.composeMaterial
implementation libs.navigationCompose
implementation libs.composeReorderable
debugImplementation libs.composeTooling
implementation libs.timber
testImplementation project(':test:android-utils')
testImplementation project(':test:utils')
testImplementation project(":test:core-models-stub")
testImplementation libs.junit
testImplementation libs.kotlinTest
testImplementation libs.robolectric
testImplementation libs.androidXTestCore
testImplementation libs.mockitoKotlin
testImplementation libs.coroutineTesting
testImplementation libs.timberJUnit
testImplementation libs.turbine
}

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest/>

View file

@ -0,0 +1,52 @@
package com.anytypeio.anytype.feature_object_type.fields
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.RelationFormat
sealed class FieldEvent {
data object OnFieldEditScreenDismiss : FieldEvent()
data object OnAddFieldScreenDismiss : FieldEvent()
data class OnFieldItemClick(val item: UiFieldsListItem) : FieldEvent()
data class OnAddToHeaderFieldClick(
val item: UiAddFieldItem
) : FieldEvent()
data class OnAddToSidebarFieldClick(
val item: UiAddFieldItem
) : FieldEvent()
data class OnSaveButtonClicked(
val name: String,
val format: RelationFormat,
val limitObjectTypes: List<Id>
) : FieldEvent()
data object OnChangeTypeClick : FieldEvent()
data object OnLimitTypesClick : FieldEvent()
sealed class FieldItemMenu : FieldEvent() {
data class OnDeleteFromTypeClick(val item: UiFieldsListItem) : FieldItemMenu()
data class OnRemoveLocalClick(val item: UiFieldsListItem) : FieldItemMenu()
data class OnAddLocalToTypeClick(val item: UiFieldsListItem) : FieldItemMenu()
}
sealed class FieldLocalInfo : FieldEvent() {
data object OnDismiss : FieldLocalInfo()
}
sealed class Section : FieldEvent() {
data object OnAddToHeaderIconClick : Section()
data object OnAddToSidebarIconClick : Section()
data object OnLocalInfoClick : Section()
}
sealed class DragEvent : FieldEvent() {
data class OnMove(val fromKey: String, val toKey: String) : DragEvent()
data object OnDragEnd : DragEvent()
}
data class OnAddFieldSearchQueryChanged(val query: String) : FieldEvent()
}

View file

@ -0,0 +1,203 @@
package com.anytypeio.anytype.feature_object_type.fields
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Key
import com.anytypeio.anytype.core_models.RelationFormat
import com.anytypeio.anytype.presentation.objects.ObjectIcon
//region Top bar
sealed class UiFieldsTitleState {
data object Hidden : UiFieldsTitleState()
data class Visible(val title: String) : UiFieldsTitleState()
}
sealed class UiFieldsCancelButtonState {
data object Hidden : UiFieldsCancelButtonState()
data object Visible : UiFieldsCancelButtonState()
}
sealed class UiFieldsSaveButtonState {
data object Hidden : UiFieldsSaveButtonState()
data object Visible : UiFieldsSaveButtonState()
}
sealed class UiFieldsEditingPanelState {
data object Hidden : UiFieldsEditingPanelState()
data object Visible : UiFieldsEditingPanelState()
}
//endregion
//region Fields List
data class UiFieldsListState(val items: List<UiFieldsListItem>) {
companion object {
val EMPTY = UiFieldsListState(emptyList())
}
}
sealed class UiFieldsListItem {
abstract val id: Id
sealed class Item : UiFieldsListItem() {
abstract val fieldKey: Key
abstract val fieldTitle: String
abstract val format: RelationFormat
abstract val limitObjectTypes: List<UiFieldObjectItem>
abstract val canDelete: Boolean
abstract val isEditableField: Boolean
data class Draggable(
override val id: Id,
override val fieldKey: Key,
override val fieldTitle: String,
override val format: RelationFormat,
override val limitObjectTypes: List<UiFieldObjectItem> = emptyList(),
override val canDelete: Boolean,
override val isEditableField: Boolean
) : Item()
data class Local(
override val id: Id,
override val fieldKey: Key,
override val fieldTitle: String,
override val format: RelationFormat,
override val limitObjectTypes: List<UiFieldObjectItem> = emptyList(),
override val canDelete: Boolean = false,
override val isEditableField: Boolean
) : Item()
}
sealed class Section : UiFieldsListItem() {
abstract val canAdd: Boolean
data class Header(
override val id: Id = ID,
override val canAdd: Boolean = false
) : Section() {
companion object {
const val ID = "section_header"
}
}
data class SideBar(
override val id: Id = ID,
override val canAdd: Boolean = false
) : Section() {
companion object {
const val ID = "section_sidebar"
}
}
data class Hidden(
override val id: Id = ID,
override val canAdd: Boolean = false
) : Section() {
companion object {
const val ID = "section_hidden"
}
}
data class Local(
override val id: Id = ID,
override val canAdd: Boolean = false
) : Section() {
companion object {
const val ID = "section_local"
}
}
data class File(
override val id: Id = ID,
override val canAdd: Boolean = false
) : Section() {
companion object {
const val ID = "section_file_recommended"
}
}
data class SpaceFields(
override val id: Id = ID,
override val canAdd: Boolean = false
) : Section() {
companion object {
const val ID = "section_space_fields"
}
}
data class LibraryFields(
override val id: Id = ID,
override val canAdd: Boolean = false
) : Section() {
companion object {
const val ID = "section_library_fields"
}
}
}
}
//endregion
//region Edit or New Field
data class UiFieldObjectItem(
val id: Id, val key: Key, val title: String, val icon: ObjectIcon
)
sealed class UiFieldEditOrNewState {
data object Hidden : UiFieldEditOrNewState()
sealed class Visible : UiFieldEditOrNewState() {
abstract val item: UiFieldsListItem.Item
data class Edit(
override val item: UiFieldsListItem.Item
) : Visible()
data class New(
override val item: UiFieldsListItem.Item
) : Visible()
data class ViewOnly(
override val item: UiFieldsListItem.Item
) : Visible()
}
}
//endregion
//region ERRORS
sealed class UiFieldsErrorState {
data object Hidden : UiFieldsErrorState()
data class Show(val reason: Reason) : UiFieldsErrorState()
sealed class Reason {
data class ErrorGettingObjects(val msg: String) : Reason()
data class Other(val msg: String) : Reason()
}
}
//endregion
//region COMMANDS
sealed class TypeFieldsCommand {
data class OpenEmojiPicker(val emoji: String) : TypeFieldsCommand()
}
//endregion
//region Section Local Fields Info
sealed class UiLocalsFieldsInfoState {
data object Hidden : UiLocalsFieldsInfoState()
data object Visible : UiLocalsFieldsInfoState()
}
//endregion
//region Add Fields screen
sealed class UiAddFieldsScreenState {
data object Hidden : UiAddFieldsScreenState()
data class Visible(val items: List<UiAddFieldItem>, val addToHeader: Boolean) : UiAddFieldsScreenState()
}
data class UiAddFieldItem(
val id: Id,
val fieldKey: Key,
val fieldTitle: String,
val format: RelationFormat
)
//endregion

View file

@ -0,0 +1,170 @@
package com.anytypeio.anytype.feature_object_type.fields.ui
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_models.RelationFormat
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
import com.anytypeio.anytype.core_ui.extensions.simpleIcon
import com.anytypeio.anytype.core_ui.foundation.DefaultSearchBar
import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable
import com.anytypeio.anytype.core_ui.views.BodyRegular
import com.anytypeio.anytype.core_ui.widgets.dv.DragHandle
import com.anytypeio.anytype.feature_object_type.fields.FieldEvent
import com.anytypeio.anytype.feature_object_type.fields.UiAddFieldItem
import com.anytypeio.anytype.feature_object_type.fields.UiAddFieldsScreenState
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AddFieldScreen(
state: UiAddFieldsScreenState,
fieldEvent: (FieldEvent) -> Unit
) {
val bottomSheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
)
var isSearchEmpty by remember { mutableStateOf(true) }
val lazyListState = rememberLazyListState()
if (state is UiAddFieldsScreenState.Visible) {
ModalBottomSheet(
modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.safeDrawing)
.nestedScroll(rememberNestedScrollInteropConnection()),
dragHandle = { DragHandle() },
scrimColor = colorResource(id = R.color.modal_screen_outside_background),
containerColor = colorResource(id = R.color.background_primary),
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
sheetState = bottomSheetState,
onDismissRequest = {
fieldEvent(FieldEvent.OnAddFieldScreenDismiss)
},
) {
LazyColumn(
modifier = Modifier
.fillMaxSize(),
state = lazyListState
) {
item {
DefaultSearchBar(
modifier = Modifier.fillMaxWidth()
.padding(horizontal = 20.dp)
) {
isSearchEmpty = it.isEmpty()
fieldEvent(FieldEvent.OnAddFieldSearchQueryChanged(it))
}
}
items(
count = state.items.size,
key = { index -> state.items[index].id },
itemContent = { index ->
val item = state.items[index]
FieldItem(
modifier = commonItemModifier()
.noRippleThrottledClickable {
if (state.addToHeader) {
fieldEvent(
FieldEvent.OnAddToHeaderFieldClick(
item = item
)
)
} else {
fieldEvent(
FieldEvent.OnAddToSidebarFieldClick(
item = item
)
)
}
},
item = item
)
}
)
}
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun FieldItem(
modifier: Modifier,
item: UiAddFieldItem
) {
Row(
modifier = modifier,
verticalAlignment = CenterVertically
) {
val formatIcon = item.format.simpleIcon()
if (formatIcon != null) {
Image(
modifier = Modifier
.padding(end = 10.dp)
.size(24.dp),
painter = painterResource(id = formatIcon),
contentDescription = "Relation format icon",
)
}
Text(
modifier = Modifier
.fillMaxWidth()
.weight(1.0f)
.padding(end = 16.dp),
text = item.fieldTitle,
style = BodyRegular,
color = colorResource(id = R.color.text_primary),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
@DefaultPreviews
@Composable
fun PreviewAddFieldScreen() {
AddFieldScreen(
state = UiAddFieldsScreenState.Visible(
items = listOf(
UiAddFieldItem(
id = "1",
fieldKey = "key",
fieldTitle = "Title",
format = RelationFormat.LONG_TEXT
)
),
addToHeader = true
),
fieldEvent = {}
)
}

View file

@ -0,0 +1,386 @@
package com.anytypeio.anytype.feature_object_type.fields.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.SoftwareKeyboardController
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_models.RelationFormat
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
import com.anytypeio.anytype.core_ui.extensions.getPrettyName
import com.anytypeio.anytype.core_ui.extensions.simpleIcon
import com.anytypeio.anytype.core_ui.foundation.Divider
import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable
import com.anytypeio.anytype.core_ui.views.BodyRegular
import com.anytypeio.anytype.core_ui.views.ButtonPrimary
import com.anytypeio.anytype.core_ui.views.ButtonSize
import com.anytypeio.anytype.core_ui.views.Caption1Regular
import com.anytypeio.anytype.core_ui.views.HeadlineHeading
import com.anytypeio.anytype.core_ui.views.Title2
import com.anytypeio.anytype.core_ui.widgets.ListWidgetObjectIcon
import com.anytypeio.anytype.core_ui.widgets.dv.DragHandle
import com.anytypeio.anytype.feature_object_type.fields.FieldEvent
import com.anytypeio.anytype.feature_object_type.fields.UiFieldEditOrNewState
import com.anytypeio.anytype.feature_object_type.fields.UiFieldObjectItem
import com.anytypeio.anytype.feature_object_type.ui.createDummyFieldDraggableItem
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EditFieldScreen(
modifier: Modifier,
uiFieldEditOrNewState: UiFieldEditOrNewState,
fieldEvent: (FieldEvent) -> Unit
) {
if (uiFieldEditOrNewState is UiFieldEditOrNewState.Visible) {
val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
ModalBottomSheet(
modifier = modifier,
dragHandle = { DragHandle() },
scrimColor = colorResource(id = R.color.modal_screen_outside_background),
containerColor = colorResource(id = R.color.background_primary),
shape = RoundedCornerShape(16.dp),
sheetState = bottomSheetState,
onDismissRequest = { fieldEvent(FieldEvent.OnFieldEditScreenDismiss) },
) {
EditFieldContent(
modifier = Modifier.fillMaxWidth(),
uiState = uiFieldEditOrNewState,
fieldEvent = fieldEvent
)
}
}
}
@Composable
private fun EditFieldContent(
modifier: Modifier,
uiState: UiFieldEditOrNewState.Visible,
fieldEvent: (FieldEvent) -> Unit
) {
val field = uiState.item
var innerValue by remember(field.fieldTitle) { mutableStateOf(field.fieldTitle) }
val focusRequester = remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current
val isEditable = field.isEditableField
val title = when (uiState) {
is UiFieldEditOrNewState.Visible.Edit -> stringResource(R.string.object_type_fields_edit_field)
is UiFieldEditOrNewState.Visible.New -> stringResource(R.string.object_type_fields_new_field)
is UiFieldEditOrNewState.Visible.ViewOnly -> stringResource(R.string.object_type_fields_preview_field)
}
Column(modifier = modifier) {
// Header title
Box(
modifier = Modifier
.fillMaxWidth()
.height(44.dp),
contentAlignment = Alignment.Center
) {
Text(
text = title,
style = Title2,
color = colorResource(id = R.color.text_primary)
)
}
// Name text field
NameTextField(
modifier = Modifier.fillMaxWidth(),
value = innerValue,
isEditable = isEditable,
focusRequester = focusRequester,
keyboardController = keyboardController,
onValueChange = { innerValue = it }
)
Spacer(modifier = Modifier.height(10.dp))
Divider()
// Field type section
FieldTypeSection(
format = field.format,
isEditable = isEditable,
onTypeClick = { fieldEvent(FieldEvent.OnChangeTypeClick) }
)
Divider()
// Limit object types (only for OBJECT format)
if (field.format == RelationFormat.OBJECT) {
LimitTypesSection(
objTypes = field.limitObjectTypes,
isEditable = isEditable,
onLimitTypesClick = { fieldEvent(FieldEvent.OnLimitTypesClick) }
)
Divider()
}
Spacer(modifier = Modifier.height(14.dp))
if (isEditable) {
ButtonPrimary(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp),
text = stringResource(R.string.object_type_fields_btn_save),
onClick = {
fieldEvent(
FieldEvent.OnSaveButtonClicked(
name = innerValue,
format = field.format,
limitObjectTypes = field.limitObjectTypes.map { it.id }
)
)
},
size = ButtonSize.Large
)
}
}
}
@Composable
fun NameTextField(
modifier: Modifier,
value: String,
isEditable: Boolean,
focusRequester: FocusRequester,
keyboardController: SoftwareKeyboardController?,
onValueChange: (String) -> Unit
) {
val focusManager = LocalFocusManager.current
Column(modifier = modifier) {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp),
text = stringResource(id = R.string.name),
style = Caption1Regular,
color = colorResource(id = R.color.text_secondary)
)
BasicTextField(
value = value,
onValueChange = onValueChange,
textStyle = HeadlineHeading.copy(color = colorResource(id = R.color.text_primary)),
singleLine = true,
enabled = isEditable,
cursorBrush = SolidColor(colorResource(id = R.color.text_primary)),
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(start = 20.dp, top = 6.dp, end = 20.dp)
.focusRequester(focusRequester)
.onFocusChanged { /* You can handle focus changes here if needed */ },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions {
keyboardController?.hide()
focusManager.clearFocus()
onValueChange(value)
},
decorationBox = { innerTextField ->
if (value.isEmpty()) {
Text(
text = stringResource(id = R.string.untitled),
style = HeadlineHeading,
color = colorResource(id = R.color.text_tertiary),
modifier = Modifier.fillMaxWidth()
)
}
innerTextField()
}
)
}
}
@Composable
fun FieldTypeSection(
format: RelationFormat,
isEditable: Boolean,
onTypeClick: () -> Unit
) {
val icon = format.simpleIcon()
SectionItem(
modifier = Modifier
.fillMaxWidth()
.height(52.dp),
text = stringResource(id = R.string.type)
)
Divider()
Box(
modifier = Modifier
.fillMaxWidth()
.height(52.dp)
.padding(horizontal = 20.dp)
.noRippleThrottledClickable { if (isEditable) onTypeClick() }
) {
if (icon != null) {
Image(
modifier = Modifier
.size(24.dp)
.align(Alignment.CenterStart),
painter = painterResource(id = icon),
contentDescription = "Relation format icon"
)
}
Text(
modifier = Modifier
.fillMaxWidth()
.padding(start = 34.dp)
.align(Alignment.CenterStart),
text = stringResource(format.getPrettyName()),
style = BodyRegular,
color = colorResource(id = R.color.text_primary),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
if (isEditable) {
Image(
modifier = Modifier.align(Alignment.CenterEnd),
painter = painterResource(id = R.drawable.ic_arrow_forward_24),
contentDescription = "Change field format icon"
)
}
}
}
@Composable
fun LimitTypesSection(
objTypes: List<UiFieldObjectItem>,
isEditable: Boolean,
onLimitTypesClick: () -> Unit
) {
val size = objTypes.size
SectionItem(
modifier = Modifier
.fillMaxWidth()
.height(52.dp),
text = stringResource(id = R.string.limit_object_types)
)
Divider()
Box(
modifier = Modifier
.fillMaxWidth()
.height(52.dp)
.padding(horizontal = 20.dp)
.noRippleThrottledClickable { if (isEditable) onLimitTypesClick() }
) {
if (objTypes.isNotEmpty()) {
Row(modifier = Modifier.align(Alignment.CenterStart)) {
ListWidgetObjectIcon(
modifier = Modifier.size(20.dp),
icon = objTypes.first().icon,
backgroundColor = R.color.transparent_black
)
Text(
modifier = Modifier.padding(start = 6.dp),
text = objTypes.first().title.take(20),
style = BodyRegular,
color = colorResource(id = R.color.text_primary),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
if (size > 1) {
Text(
text = " +${size - 1}",
style = BodyRegular,
color = colorResource(id = R.color.text_primary)
)
}
}
} else {
Text(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.CenterStart),
text = stringResource(R.string.none),
style = BodyRegular,
color = colorResource(id = R.color.text_primary),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
if (isEditable) {
Image(
modifier = Modifier.align(Alignment.CenterEnd),
painter = painterResource(id = R.drawable.ic_arrow_forward_24),
contentDescription = "Change field object types icon"
)
}
}
}
@Composable
fun SectionItem(modifier: Modifier, text: String) {
Box(modifier = modifier) {
Text(
modifier = Modifier
.padding(start = 20.dp, bottom = 8.dp)
.align(Alignment.BottomStart),
text = text,
style = Caption1Regular,
color = colorResource(id = R.color.text_secondary)
)
}
}
@DefaultPreviews
@Composable
private fun MyPreview() {
EditFieldScreen(
modifier = Modifier.fillMaxWidth(),
uiFieldEditOrNewState = UiFieldEditOrNewState.Visible.Edit(
item = createDummyFieldDraggableItem()
),
fieldEvent = {}
)
}
@DefaultPreviews
@Composable
private fun MyPreviewOnlyPreview() {
EditFieldScreen(
modifier = Modifier.fillMaxWidth(),
uiFieldEditOrNewState = UiFieldEditOrNewState.Visible.ViewOnly(
item = createDummyFieldDraggableItem(
isEditableField = false
)
),
fieldEvent = {}
)
}

View file

@ -0,0 +1,113 @@
package com.anytypeio.anytype.feature_object_type.fields.ui
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SheetState
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
import com.anytypeio.anytype.core_ui.views.BodyCalloutRegular
import com.anytypeio.anytype.core_ui.views.ButtonSecondary
import com.anytypeio.anytype.core_ui.views.ButtonSize
import com.anytypeio.anytype.core_ui.views.HeadlineHeading
import com.anytypeio.anytype.core_ui.widgets.dv.DragHandle
import com.anytypeio.anytype.feature_object_type.fields.FieldEvent
import com.anytypeio.anytype.feature_object_type.fields.UiLocalsFieldsInfoState
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SectionLocalFieldsInfo(
modifier: Modifier,
state: UiLocalsFieldsInfoState,
fieldEvent: (FieldEvent) -> Unit
) {
val bottomSheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
)
if (state is UiLocalsFieldsInfoState.Visible) {
LocalInfoScreen(
modifier = modifier,
bottomSheetState = bottomSheetState,
onDismiss = {
fieldEvent(FieldEvent.FieldLocalInfo.OnDismiss)
}
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LocalInfoScreen(
modifier: Modifier,
bottomSheetState: SheetState,
onDismiss: () -> Unit
) {
ModalBottomSheet(
modifier = modifier,
dragHandle = { DragHandle() },
scrimColor = colorResource(id = R.color.modal_screen_outside_background),
containerColor = colorResource(id = R.color.background_primary),
shape = RoundedCornerShape(16.dp),
sheetState = bottomSheetState,
onDismissRequest = {
onDismiss()
},
) {
Spacer(modifier = Modifier.height(20.dp))
Text(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp),
textAlign = TextAlign.Center,
style = HeadlineHeading,
color = colorResource(id = R.color.text_primary),
text = stringResource(R.string.object_type_fields_local_info_title)
)
Spacer(modifier = Modifier.height(7.dp))
Text(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp),
textAlign = TextAlign.Center,
style = BodyCalloutRegular,
color = colorResource(id = R.color.text_primary),
text = stringResource(R.string.object_type_fields_local_info_description)
)
Spacer(modifier = Modifier.height(30.dp))
ButtonSecondary(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp),
text = stringResource(R.string.object_type_fields_local_info_button),
size = ButtonSize.LargeSecondary,
onClick = {
onDismiss()
}
)
Spacer(modifier = Modifier.height(10.dp))
}
}
@DefaultPreviews
@Composable
fun SectionLocalFieldsInfoPreview() {
SectionLocalFieldsInfo(
modifier = Modifier.fillMaxWidth(),
state = UiLocalsFieldsInfoState.Visible,
fieldEvent = {}
)
}

View file

@ -0,0 +1,775 @@
package com.anytypeio.anytype.feature_object_type.fields.ui
import android.os.Build
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_models.RelationFormat
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
import com.anytypeio.anytype.core_ui.common.ReorderHapticFeedback
import com.anytypeio.anytype.core_ui.common.ReorderHapticFeedbackType
import com.anytypeio.anytype.core_ui.common.rememberReorderHapticFeedback
import com.anytypeio.anytype.core_ui.extensions.simpleIcon
import com.anytypeio.anytype.core_ui.foundation.Dragger
import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable
import com.anytypeio.anytype.core_ui.views.BodyCalloutMedium
import com.anytypeio.anytype.core_ui.views.BodyCalloutRegular
import com.anytypeio.anytype.core_ui.views.BodyRegular
import com.anytypeio.anytype.core_ui.views.Caption1Medium
import com.anytypeio.anytype.core_ui.views.Title1
import com.anytypeio.anytype.core_ui.widgets.ListWidgetObjectIcon
import com.anytypeio.anytype.core_utils.insets.EDGE_TO_EDGE_MIN_SDK
import com.anytypeio.anytype.feature_object_type.R
import com.anytypeio.anytype.feature_object_type.fields.FieldEvent
import com.anytypeio.anytype.feature_object_type.fields.FieldEvent.*
import com.anytypeio.anytype.feature_object_type.fields.FieldEvent.FieldItemMenu.*
import com.anytypeio.anytype.feature_object_type.fields.UiAddFieldsScreenState
import com.anytypeio.anytype.feature_object_type.fields.UiFieldEditOrNewState
import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListItem
import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListItem.Section
import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListState
import com.anytypeio.anytype.feature_object_type.fields.UiLocalsFieldsInfoState
import com.anytypeio.anytype.feature_object_type.ui.UiIconState
import com.anytypeio.anytype.feature_object_type.ui.UiTitleState
import com.anytypeio.anytype.presentation.objects.ObjectIcon
import kotlinx.coroutines.delay
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.ReorderableLazyListState
import sh.calvin.reorderable.rememberReorderableLazyListState
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun FieldsMainScreen(
uiFieldsListState: UiFieldsListState,
uiTitleState: UiTitleState,
uiIconState: UiIconState,
uiFieldEditOrNewState: UiFieldEditOrNewState,
uiFieldLocalInfoState: UiLocalsFieldsInfoState,
uiAddFieldsScreenState: UiAddFieldsScreenState,
fieldEvent: (FieldEvent) -> Unit
) {
val hapticFeedback = rememberReorderHapticFeedback()
val lazyListState = rememberLazyListState()
val reorderableLazyColumnState = rememberReorderableLazyListState(lazyListState) { from, to ->
fieldEvent(DragEvent.OnMove(from.key as String, to.key as String))
hapticFeedback.performHapticFeedback(ReorderHapticFeedbackType.MOVE)
}
var isDragging by remember { mutableStateOf(false) }
LaunchedEffect(reorderableLazyColumnState.isAnyItemDragging) {
if (reorderableLazyColumnState.isAnyItemDragging) {
isDragging = true
// Optional: Add a small delay to avoid triggering on very short drags
delay(50)
} else if (isDragging) {
isDragging = false
fieldEvent(DragEvent.OnDragEnd)
hapticFeedback.performHapticFeedback(ReorderHapticFeedbackType.MOVE)
}
}
Scaffold(
modifier = Modifier
.nestedScroll(rememberNestedScrollInteropConnection())
.background(
color = colorResource(id = R.color.widget_background),
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
)
.fillMaxSize(),
containerColor = colorResource(id = R.color.transparent_black),
contentColor = colorResource(id = R.color.widget_background),
topBar = {
TopBar(
modifier = Modifier.fillMaxWidth(),
uiTitleState = uiTitleState,
uiIconState = uiIconState
)
},
content = { paddingValues ->
val contentModifier = if (Build.VERSION.SDK_INT >= EDGE_TO_EDGE_MIN_SDK) {
Modifier
.windowInsetsPadding(WindowInsets.navigationBars)
.fillMaxSize()
.padding(top = paddingValues.calculateTopPadding())
} else {
Modifier
.fillMaxSize()
.padding(paddingValues)
}
LazyColumn(
modifier = contentModifier,
state = lazyListState
) {
items(
count = uiFieldsListState.items.size,
key = { index -> uiFieldsListState.items[index].id },
contentType = { index -> getContentType(uiFieldsListState.items[index]) },
itemContent = { index ->
val item = uiFieldsListState.items[index]
when (item) {
is UiFieldsListItem.Item.Draggable -> {
FieldItemDraggable(
modifier = commonItemModifier(),
item = item,
reorderingState = reorderableLazyColumnState,
fieldEvent = fieldEvent,
hapticFeedback = hapticFeedback
)
}
is UiFieldsListItem.Item.Local -> {
FieldItemLocal(
modifier = commonItemModifier(),
item = item,
fieldEvent = fieldEvent
)
}
is Section.SideBar -> {
SectionItem(
item = item,
reorderingState = reorderableLazyColumnState,
fieldEvent = fieldEvent,
isReorderable = true,
onAddIconClick = {
fieldEvent(FieldEvent.Section.OnAddToSidebarIconClick)
}
)
}
is Section.Hidden -> {
SectionItem(
item = item,
reorderingState = reorderableLazyColumnState,
fieldEvent = fieldEvent,
isReorderable = true
)
}
is Section.Header -> {
SectionItem(
item = item,
reorderingState = reorderableLazyColumnState,
fieldEvent = fieldEvent,
isReorderable = false,
onAddIconClick = {
fieldEvent(FieldEvent.Section.OnAddToHeaderIconClick)
}
)
}
is Section.Local,
is Section.File -> {
SectionItem(
item = item,
reorderingState = reorderableLazyColumnState,
fieldEvent = fieldEvent,
isReorderable = false
)
}
is Section.LibraryFields -> TODO()
is Section.SpaceFields -> TODO()
}
}
)
item {
Spacer(modifier = Modifier.height(60.dp))
}
}
}
)
if (uiFieldEditOrNewState is UiFieldEditOrNewState.Visible) {
EditFieldScreen(
modifier = Modifier.fillMaxWidth(),
uiFieldEditOrNewState = uiFieldEditOrNewState,
fieldEvent = fieldEvent
)
}
if (uiFieldLocalInfoState is UiLocalsFieldsInfoState.Visible) {
SectionLocalFieldsInfo(
modifier = Modifier.fillMaxWidth(),
state = uiFieldLocalInfoState,
fieldEvent = fieldEvent
)
}
if (uiAddFieldsScreenState is UiAddFieldsScreenState.Visible) {
AddFieldScreen(
state = uiAddFieldsScreenState,
fieldEvent = fieldEvent
)
}
}
/** Returns a content type string based on the item type. **/
private fun getContentType(item: UiFieldsListItem): String {
return when (item) {
is UiFieldsListItem.Item.Draggable -> FieldsItemsContentType.FIELD_ITEM_DRAGGABLE
is UiFieldsListItem.Item.Local -> FieldsItemsContentType.FIELD_ITEM_LOCAL
is Section.SideBar -> FieldsItemsContentType.SECTION_SIDEBAR
is Section.Header -> FieldsItemsContentType.SECTION_HEADER
is Section.Hidden -> FieldsItemsContentType.SECTION_HIDDEN
is Section.Local -> FieldsItemsContentType.SECTION_LOCAL
is Section.File -> FieldsItemsContentType.SECTION_FILE
is Section.LibraryFields -> "content_type_section_library_fields"
is Section.SpaceFields -> "content_type_section_space_fields"
}
}
/** A common modifier for list items. **/
@Composable
fun LazyItemScope.commonItemModifier() = Modifier
.height(52.dp)
.fillMaxWidth()
.padding(horizontal = 20.dp)
.bottomBorder()
.animateItem()
@Composable
private fun TopBar(
modifier: Modifier,
uiTitleState: UiTitleState,
uiIconState: UiIconState,
) {
val modifier = if (Build.VERSION.SDK_INT >= EDGE_TO_EDGE_MIN_SDK) {
modifier.windowInsetsPadding(WindowInsets.statusBars)
} else {
modifier
}
Column(
modifier = modifier
.background(
color = colorResource(id = R.color.widget_background),
shape = RoundedCornerShape(16.dp, 16.dp, 0.dp, 0.dp)
)
) {
Dragger(
modifier = Modifier
.padding(vertical = 6.dp)
.align(Alignment.CenterHorizontally)
)
Box(
modifier = Modifier
.fillMaxWidth()
.height(48.dp)
) {
Text(
modifier = Modifier
.wrapContentSize()
.align(Alignment.Center),
text = stringResource(R.string.object_type_fields_title),
style = Title1,
color = colorResource(R.color.text_primary)
)
}
InfoBar(
modifier = Modifier
.fillMaxWidth()
.height(36.dp)
.background(color = colorResource(R.color.shape_transparent_secondary)),
uiTitleState = uiTitleState,
uiIconState = uiIconState
)
}
}
@Composable
private fun InfoBar(modifier: Modifier, uiTitleState: UiTitleState, uiIconState: UiIconState) {
Row(
modifier = modifier.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = CenterVertically
) {
Text(
text = stringResource(R.string.object_type_fields_info_text),
style = Caption1Medium,
color = colorResource(id = R.color.text_primary),
)
ListWidgetObjectIcon(
modifier = Modifier
.padding(start = 4.dp)
.size(18.dp),
icon = uiIconState.icon,
backgroundColor = R.color.transparent_black
)
Text(
modifier = Modifier.padding(start = 4.dp),
text = uiTitleState.title,
style = Caption1Medium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = colorResource(id = R.color.text_primary),
)
}
}
@Composable
private fun LazyItemScope.SectionItem(
item: UiFieldsListItem.Section,
reorderingState: ReorderableLazyListState,
isReorderable: Boolean = true,
onAddIconClick: () -> Unit = {},
fieldEvent: (FieldEvent) -> Unit
) {
val (title, textColor) = when (item) {
is Section.Header -> stringResource(R.string.object_type_fields_section_header) to colorResource(
id = R.color.text_secondary
)
is Section.SideBar ->
stringResource(R.string.object_type_fields_section_fields_menu) to colorResource(
id = R.color.text_secondary
)
is Section.Hidden -> stringResource(R.string.object_type_fields_section_hidden) to colorResource(
id = R.color.text_secondary
)
is Section.Local -> stringResource(R.string.object_type_fields_section_local_fields) to colorResource(
id = R.color.text_primary
)
is Section.LibraryFields -> TODO()
is Section.SpaceFields -> TODO()
is Section.File -> stringResource(R.string.object_type_fields_section_file) to colorResource(
id = R.color.text_secondary
)
}
ReorderableItem(
state = reorderingState,
key = item.id,
enabled = isReorderable
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(52.dp)
) {
Text(
modifier = Modifier
.padding(bottom = 7.dp, start = 20.dp)
.align(Alignment.BottomStart),
text = title,
style = BodyCalloutMedium,
color = textColor,
)
if (item.canAdd) {
Box(
modifier = Modifier
.align(Alignment.BottomEnd)
.width(54.dp)
.height(40.dp)
.noRippleThrottledClickable {
onAddIconClick()
}
) {
Image(
modifier = Modifier
.padding(bottom = 6.dp, end = 20.dp)
.wrapContentSize()
.align(Alignment.BottomEnd),
painter = painterResource(R.drawable.ic_default_plus),
contentDescription = "$title plus button"
)
}
}
if (item is Section.Local) {
Box(
modifier = Modifier
.align(Alignment.BottomEnd)
.height(37.dp)
.width(44.dp)
.noRippleThrottledClickable {
fieldEvent(FieldEvent.Section.OnLocalInfoClick)
}
) {
Image(
modifier = Modifier
.padding(bottom = 9.dp, end = 20.dp)
.wrapContentSize()
.align(Alignment.BottomEnd),
painter = painterResource(R.drawable.ic_section_local_fields),
contentDescription = "Section local fields info"
)
}
}
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun FieldItemLocal(
modifier: Modifier,
item: UiFieldsListItem.Item.Local,
fieldEvent: (FieldEvent) -> Unit
) {
val isMenuExpanded = remember { mutableStateOf(false) }
Row(
modifier = modifier
.combinedClickable(
onClick = { fieldEvent(OnFieldItemClick(item = item)) },
onLongClick = { isMenuExpanded.value = true }
),
verticalAlignment = CenterVertically
) {
val formatIcon = item.format.simpleIcon()
if (formatIcon != null) {
Image(
modifier = Modifier
.padding(end = 10.dp)
.size(24.dp),
painter = painterResource(id = formatIcon),
contentDescription = "Relation format icon",
)
}
Text(
modifier = Modifier
.fillMaxWidth()
.weight(1.0f)
.padding(end = 16.dp),
text = item.fieldTitle,
style = BodyRegular,
color = colorResource(id = R.color.text_primary),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Image(
modifier = Modifier
.size(24.dp)
.noRippleThrottledClickable {
isMenuExpanded.value = true
},
painter = painterResource(R.drawable.ic_space_list_dots),
contentDescription = "Local item menu"
)
ItemDropDownMenu(
item = item,
showMenu = isMenuExpanded.value,
onDismissRequest = {
isMenuExpanded.value = false
},
onFieldEvent = {
isMenuExpanded.value = false
fieldEvent(it)
}
)
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun LazyItemScope.FieldItemDraggable(
modifier: Modifier,
reorderingState: ReorderableLazyListState,
hapticFeedback: ReorderHapticFeedback,
item: UiFieldsListItem.Item.Draggable,
fieldEvent: (FieldEvent) -> Unit
) {
val isMenuExpanded = remember { mutableStateOf(false) }
ReorderableItem(
state = reorderingState,
key = item.id,
) { isDragging ->
Row(
modifier = modifier,
verticalAlignment = CenterVertically
) {
val formatIcon = item.format.simpleIcon()
if (formatIcon != null) {
Image(
modifier = Modifier
.padding(end = 10.dp)
.size(24.dp),
painter = painterResource(id = formatIcon),
contentDescription = "Relation format icon",
)
}
Box(
modifier = Modifier
.weight(1f) // fill remaining space
.combinedClickable(
onClick = {
// normal click => open/edit
fieldEvent(OnFieldItemClick(item = item))
},
onLongClick = {
// show your menu, only if NOT dragging
if (item.canDelete) {
isMenuExpanded.value = true
}
}
)
.padding(end = 16.dp)
) {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(end = 16.dp),
text = item.fieldTitle,
style = BodyRegular,
color = colorResource(id = R.color.text_primary),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Image(
modifier = Modifier
.size(24.dp)
.draggableHandle(
onDragStarted = {
hapticFeedback.performHapticFeedback(ReorderHapticFeedbackType.START)
},
onDragStopped = {
hapticFeedback.performHapticFeedback(ReorderHapticFeedbackType.END)
}
),
painter = painterResource(R.drawable.ic_dnd),
contentDescription = "Icon drag"
)
ItemDropDownMenu(
item = item,
showMenu = isMenuExpanded.value,
onDismissRequest = {
isMenuExpanded.value = false
},
onFieldEvent = {
isMenuExpanded.value = false
fieldEvent(it)
}
)
}
}
}
@Composable
fun Modifier.bottomBorder(
strokeWidth: Dp = 0.5.dp,
color: Color = colorResource(R.color.shape_primary)
) = composed(
factory = {
val density = LocalDensity.current
val strokeWidthPx = density.run { strokeWidth.toPx() }
Modifier.drawBehind {
val width = size.width
val height = size.height - strokeWidthPx / 2
drawLine(
color = color,
start = Offset(x = 0f, y = height),
end = Offset(x = width, y = height),
strokeWidth = strokeWidthPx
)
}
}
)
@Composable
fun ItemDropDownMenu(
item: UiFieldsListItem.Item,
showMenu: Boolean,
onDismissRequest: () -> Unit,
onFieldEvent: (FieldEvent) -> Unit,
) {
DropdownMenu(
modifier = Modifier
.width(244.dp),
expanded = showMenu,
offset = DpOffset(x = 0.dp, y = 0.dp),
onDismissRequest = {
onDismissRequest()
},
shape = RoundedCornerShape(10.dp),
containerColor = colorResource(id = R.color.background_secondary),
) {
when (item) {
is UiFieldsListItem.Item.Draggable -> {
DropdownMenuItem(
text = {
Text(
text = stringResource(R.string.object_type_fields_menu_delete),
style = BodyCalloutRegular,
color = colorResource(id = R.color.palette_system_red)
)
},
onClick = {
onFieldEvent(OnDeleteFromTypeClick(item))
},
)
}
is UiFieldsListItem.Item.Local -> {
DropdownMenuItem(
text = {
Text(
text = stringResource(R.string.object_type_fields_menu_add_to_type),
style = BodyCalloutRegular,
color = colorResource(id = R.color.text_primary)
)
},
onClick = {
onFieldEvent(OnAddLocalToTypeClick(item))
},
)
// DropdownMenuItem(
// text = {
// Text(
// text = stringResource(R.string.object_type_fields_menu_remove),
// style = BodyCalloutRegular,
// color = colorResource(id = R.color.palette_system_red)
// )
// },
// onClick = {
// onFieldEvent(FieldItemMenu.OnRemoveLocalClick(item))
// },
// )
}
}
}
}
@DefaultPreviews
@Composable
fun PreviewTypeFieldsMainScreen() {
FieldsMainScreen(
uiTitleState = UiTitleState(title = "Page", isEditable = false),
uiIconState = UiIconState(icon = ObjectIcon.Empty.ObjectType, isEditable = false),
uiFieldsListState = UiFieldsListState(
items = listOf(
UiFieldsListItem.Section.Header(),
UiFieldsListItem.Item.Draggable(
id = "id1",
fieldKey = "key1",
fieldTitle = "Status",
format = RelationFormat.STATUS,
canDelete = true,
isEditableField = true
),
UiFieldsListItem.Item.Draggable(
id = "id2",
fieldKey = "key2",
fieldTitle = "Very long field title, just to test how it looks",
format = RelationFormat.LONG_TEXT,
canDelete = true,
isEditableField = true
),
UiFieldsListItem.Section.SideBar(
canAdd = true
),
UiFieldsListItem.Item.Draggable(
id = "id3",
fieldKey = "key3",
fieldTitle = "Links",
format = RelationFormat.URL,
isEditableField = true,
canDelete = true
),
UiFieldsListItem.Item.Draggable(
id = "id4",
fieldKey = "key4",
fieldTitle = "Very long field title, just to test how it looks",
format = RelationFormat.DATE,
isEditableField = true,
canDelete = true
),
UiFieldsListItem.Section.Hidden(),
UiFieldsListItem.Item.Draggable(
id = "id555",
fieldKey = "key555",
fieldTitle = "Hidden field",
format = RelationFormat.LONG_TEXT,
isEditableField = true,
canDelete = true
),
UiFieldsListItem.Section.Local(),
UiFieldsListItem.Item.Local(
id = "id5",
fieldKey = "key5",
fieldTitle = "Local field",
format = RelationFormat.LONG_TEXT,
isEditableField = true
),
UiFieldsListItem.Item.Local(
id = "id6",
fieldKey = "key6",
fieldTitle = "Local Very long field title, just to test how it looks",
format = RelationFormat.LONG_TEXT,
isEditableField = true
)
)
),
fieldEvent = {},
uiFieldEditOrNewState = UiFieldEditOrNewState.Hidden,
uiFieldLocalInfoState = UiLocalsFieldsInfoState.Hidden,
uiAddFieldsScreenState = UiAddFieldsScreenState.Hidden
)
}
object FieldsItemsContentType {
const val FIELD_ITEM_DRAGGABLE = "content_type_field_item_draggable"
const val FIELD_ITEM_DEFAULT = "content_type_field_item_default"
const val FIELD_ITEM_LOCAL = "content_type_field_item_local"
const val SECTION_HEADER = "content_type_section_header"
const val SECTION_SIDEBAR = "content_type_section_sidebar"
const val SECTION_HIDDEN = "content_type_section_hidden"
const val SECTION_LOCAL = "content_type_section_local"
const val SECTION_FILE = "content_type_section_file"
}

View file

@ -0,0 +1,39 @@
package com.anytypeio.anytype.feature_object_type.ui
import com.anytypeio.anytype.core_models.RelationFormat
import com.anytypeio.anytype.feature_object_type.fields.UiFieldObjectItem
import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListItem
import com.anytypeio.anytype.presentation.objects.ObjectIcon
fun createDummyFieldDraggableItem(isEditableField: Boolean = true): UiFieldsListItem.Item.Draggable {
return UiFieldsListItem.Item.Draggable(
id = "dummyId",
fieldKey = "dummyKey",
fieldTitle = "Field Title",
format = RelationFormat.OBJECT,
limitObjectTypes = listOf(
UiFieldObjectItem(
id = "dummyObjectId1",
key = "dummyKey1",
title = "Dummy Object Type 1",
icon = ObjectIcon.Empty.ObjectType,
),
UiFieldObjectItem(
id = "dummyObjectId1",
key = "dummyKey1",
title = "Dummy Object Type 1",
icon = ObjectIcon.Empty.ObjectType,
),
UiFieldObjectItem(
id = "dummyObjectId1",
key = "dummyKey1",
title = "Dummy Object Type 1",
icon = ObjectIcon.Empty.ObjectType,
),
),
isEditableField = isEditableField,
canDelete = true
)
}

View file

@ -0,0 +1,462 @@
package com.anytypeio.anytype.feature_object_type.ui
import android.os.Build
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.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Text
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.multiplayer.P2PStatusUpdate
import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncAndP2PStatusState
import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncError
import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncNetwork
import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncStatus
import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncUpdate
import com.anytypeio.anytype.core_models.primitives.TypeId
import com.anytypeio.anytype.core_models.primitives.TypeKey
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
import com.anytypeio.anytype.core_ui.extensions.swapList
import com.anytypeio.anytype.core_ui.foundation.Divider
import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable
import com.anytypeio.anytype.core_ui.lists.objects.ListItemLoading
import com.anytypeio.anytype.core_ui.lists.objects.ObjectsListItem
import com.anytypeio.anytype.core_ui.lists.objects.UiContentState
import com.anytypeio.anytype.core_ui.lists.objects.UiObjectsListState
import com.anytypeio.anytype.core_ui.syncstatus.SpaceSyncStatusScreen
import com.anytypeio.anytype.core_ui.views.ButtonSecondary
import com.anytypeio.anytype.core_ui.views.ButtonSize
import com.anytypeio.anytype.core_ui.views.Relations3
import com.anytypeio.anytype.core_ui.views.Title2
import com.anytypeio.anytype.core_utils.insets.EDGE_TO_EDGE_MIN_SDK
import com.anytypeio.anytype.feature_object_type.R
import com.anytypeio.anytype.feature_object_type.ui.alerts.DeleteAlertScreen
import com.anytypeio.anytype.feature_object_type.ui.header.HorizontalButtons
import com.anytypeio.anytype.feature_object_type.ui.header.IconAndTitleWidget
import com.anytypeio.anytype.feature_object_type.ui.header.TopToolbar
import com.anytypeio.anytype.feature_object_type.ui.layouts.TypeLayoutsScreen
import com.anytypeio.anytype.feature_object_type.ui.objects.ObjectsHeader
import com.anytypeio.anytype.feature_object_type.ui.templates.TemplatesScreen
import com.anytypeio.anytype.presentation.editor.cover.CoverColor
import com.anytypeio.anytype.presentation.objects.ObjectIcon
import com.anytypeio.anytype.presentation.objects.UiObjectsListItem
import com.anytypeio.anytype.presentation.sync.SyncStatusWidgetState
import com.anytypeio.anytype.presentation.templates.TemplateView
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ObjectTypeMainScreen(
//top bar
uiEditButtonState: UiEditButton,
uiSyncStatusBadgeState: UiSyncStatusBadgeState,
uiSyncStatusState: SyncStatusWidgetState,
//header
uiIconState: UiIconState,
uiTitleState: UiTitleState,
//layout and fields buttons
uiFieldsButtonState: UiFieldsButtonState,
uiLayoutButtonState: UiLayoutButtonState,
uiLayoutTypeState: UiLayoutTypeState,
//templates header
uiTemplatesHeaderState: UiTemplatesHeaderState,
uiTemplatesAddIconState: UiTemplatesAddIconState,
//templates list
uiTemplatesListState: UiTemplatesListState,
//objects header
uiObjectsHeaderState: UiObjectsHeaderState,
uiObjectsAddIconState: UiObjectsAddIconState,
uiObjectsSettingsIconState: UiObjectsSettingsIconState,
uiObjectsMenuState: UiMenuState,
//objects list
uiObjectsListState: UiObjectsListState,
uiContentState: UiContentState,
//delete alert
uiDeleteAlertState: UiDeleteAlertState,
//events
onTypeEvent: (TypeEvent) -> Unit
) {
val objects = remember { mutableStateListOf<UiObjectsListItem>() }
objects.swapList(uiObjectsListState.items)
val topAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(
state = rememberTopAppBarState()
)
Scaffold(
modifier = Modifier
.fillMaxSize()
.nestedScroll(topAppBarScrollBehavior.nestedScrollConnection),
containerColor = colorResource(id = R.color.background_primary),
contentColor = colorResource(id = R.color.background_primary),
topBar = {
TopBarContent(
uiSyncStatusBadgeState = uiSyncStatusBadgeState,
uiEditButtonState = uiEditButtonState,
uiTitleState = uiTitleState,
topBarScrollBehavior = topAppBarScrollBehavior,
onTypeEvent = onTypeEvent
)
},
content = { paddingValues ->
MainContent(
paddingValues = paddingValues,
uiIconState = uiIconState,
uiTitleState = uiTitleState,
uiFieldsButtonState = uiFieldsButtonState,
uiLayoutButtonState = uiLayoutButtonState,
uiTemplatesHeaderState = uiTemplatesHeaderState,
uiTemplatesAddIconState = uiTemplatesAddIconState,
uiTemplatesListState = uiTemplatesListState,
uiObjectsHeaderState = uiObjectsHeaderState,
uiObjectsAddIconState = uiObjectsAddIconState,
uiObjectsSettingsIconState = uiObjectsSettingsIconState,
uiObjectsMenuState = uiObjectsMenuState,
objects = objects,
onTypeEvent = onTypeEvent
)
}
)
BottomSyncStatus(
uiSyncStatusState = uiSyncStatusState,
onDismiss = { onTypeEvent(TypeEvent.OnSyncStatusDismiss) }
)
if (uiDeleteAlertState is UiDeleteAlertState.Show) {
DeleteAlertScreen(
onTypeEvent = onTypeEvent
)
}
if (uiLayoutTypeState is UiLayoutTypeState.Visible) {
TypeLayoutsScreen(
modifier = Modifier.fillMaxWidth(),
uiState = uiLayoutTypeState,
onTypeEvent = onTypeEvent
)
}
}
@Composable
private fun MainContent(
paddingValues: PaddingValues,
uiIconState: UiIconState,
uiTitleState: UiTitleState,
uiFieldsButtonState: UiFieldsButtonState,
uiLayoutButtonState: UiLayoutButtonState,
uiTemplatesHeaderState: UiTemplatesHeaderState,
uiTemplatesAddIconState: UiTemplatesAddIconState,
uiTemplatesListState: UiTemplatesListState,
uiObjectsHeaderState: UiObjectsHeaderState,
uiObjectsAddIconState: UiObjectsAddIconState,
uiObjectsSettingsIconState: UiObjectsSettingsIconState,
uiObjectsMenuState: UiMenuState,
objects: List<UiObjectsListItem>,
onTypeEvent: (TypeEvent) -> Unit
) {
// Adjust content modifier based on SDK version for proper insets handling
val contentModifier = if (Build.VERSION.SDK_INT >= EDGE_TO_EDGE_MIN_SDK) {
Modifier
.windowInsetsPadding(WindowInsets.navigationBars)
.fillMaxSize()
.padding(top = paddingValues.calculateTopPadding())
} else {
Modifier
.fillMaxSize()
.padding(paddingValues)
}
LazyColumn(modifier = contentModifier) {
item {
IconAndTitleWidget(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(top = 32.dp)
.padding(horizontal = 20.dp),
uiIconState = uiIconState,
uiTitleState = uiTitleState,
onTypeEvent = onTypeEvent
)
Spacer(modifier = Modifier.height(20.dp))
}
item {
HorizontalButtons(
modifier = Modifier
.fillMaxWidth()
.height(36.dp)
.padding(horizontal = 20.dp),
uiFieldsButtonState = uiFieldsButtonState,
uiLayoutButtonState = uiLayoutButtonState,
onTypeEvent = onTypeEvent
)
}
if (uiTemplatesHeaderState is UiTemplatesHeaderState.Visible) {
item {
TemplatesScreen(
uiTemplatesHeaderState = uiTemplatesHeaderState,
uiTemplatesAddIconState = uiTemplatesAddIconState,
uiTemplatesListState = uiTemplatesListState,
onTypeEvent = onTypeEvent
)
}
}
item {
ObjectsHeader(
modifier = Modifier
.fillMaxWidth()
.height(48.dp)
.padding(horizontal = 20.dp),
uiObjectsHeaderState = uiObjectsHeaderState,
uiObjectsAddIconState = uiObjectsAddIconState,
uiObjectsSettingsIconState = uiObjectsSettingsIconState,
uiObjectsMenuState = uiObjectsMenuState,
onTypeEvent = onTypeEvent
)
}
if (objects.isEmpty()) {
item {
EmptyScreen(
modifier = Modifier.padding(top = 18.dp)
)
}
} else {
items(
count = objects.size,
key = { index -> objects[index].id },
contentType = { index ->
when (objects[index]) {
is UiObjectsListItem.Loading -> "loading"
is UiObjectsListItem.Item -> "item"
}
}
) { index ->
when (val item = objects[index]) {
is UiObjectsListItem.Item -> {
ObjectsListItem(
modifier = Modifier
.fillMaxWidth()
.animateItem()
.padding(horizontal = 4.dp)
.noRippleThrottledClickable {
onTypeEvent(TypeEvent.OnObjectItemClick(item))
},
item = item
)
Divider(paddingStart = 20.dp, paddingEnd = 20.dp)
}
is UiObjectsListItem.Loading -> {
ListItemLoading(modifier = Modifier)
}
}
}
}
// Objects menu actions
when (val itemSet = uiObjectsMenuState.objSetItem) {
UiMenuSetItem.CreateSet -> {
item {
ButtonSecondary(
modifier = Modifier
.fillMaxWidth()
.padding(top = 12.dp, start = 20.dp, end = 20.dp),
text = stringResource(R.string.object_type_objects_menu_create_set),
size = ButtonSize.Large,
onClick = { onTypeEvent(TypeEvent.OnCreateSetClick) }
)
}
}
is UiMenuSetItem.OpenSet -> {
item {
ButtonSecondary(
modifier = Modifier
.fillMaxWidth()
.padding(top = 12.dp, start = 20.dp, end = 20.dp),
text = stringResource(R.string.object_type_objects_menu_open_set),
size = ButtonSize.Large,
onClick = { onTypeEvent(TypeEvent.OnOpenSetClick(setId = itemSet.setId)) }
)
}
}
UiMenuSetItem.Hidden -> Unit
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TopBarContent(
uiSyncStatusBadgeState: UiSyncStatusBadgeState,
uiEditButtonState: UiEditButton,
uiTitleState: UiTitleState,
topBarScrollBehavior: TopAppBarScrollBehavior,
onTypeEvent: (TypeEvent) -> Unit
) {
// Use windowInsetsPadding if running on a recent SDK
val modifier = if (Build.VERSION.SDK_INT >= EDGE_TO_EDGE_MIN_SDK) {
Modifier
.windowInsetsPadding(WindowInsets.statusBars)
.fillMaxWidth()
} else {
Modifier.fillMaxWidth()
}
Column(modifier = modifier) {
TopToolbar(
modifier = Modifier
.fillMaxWidth()
.height(48.dp),
uiSyncStatusBadgeState = uiSyncStatusBadgeState,
uiEditButtonState = uiEditButtonState,
uiTitleState = uiTitleState,
onTypeEvent = onTypeEvent,
topBarScrollBehavior = topBarScrollBehavior
)
}
}
@Composable
private fun BottomSyncStatus(
uiSyncStatusState: SyncStatusWidgetState,
onDismiss: () -> Unit
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.BottomCenter
) {
SpaceSyncStatusScreen(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.windowInsetsPadding(WindowInsets.navigationBars),
modifierCard = Modifier.padding(start = 8.dp, end = 8.dp, bottom = 16.dp),
uiState = uiSyncStatusState,
onDismiss = onDismiss,
onUpdateAppClick = {}
)
}
}
@Composable
fun EmptyScreen(modifier: Modifier) {
Column(modifier = modifier) {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 32.dp),
text = stringResource(R.string.object_type_empty_items_title),
color = colorResource(id = R.color.text_secondary),
style = Title2,
textAlign = TextAlign.Center
)
Text(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 32.dp),
text = stringResource(R.string.object_type_empty_items_subtitle),
color = colorResource(id = R.color.text_secondary),
style = Relations3,
textAlign = TextAlign.Center
)
}
}
@DefaultPreviews
@Composable
fun ObjectTypeMainScreenPreview() {
val spaceSyncUpdate = SpaceSyncUpdate.Update(
id = "1",
status = SpaceSyncStatus.SYNCING,
network = SpaceSyncNetwork.ANYTYPE,
error = SpaceSyncError.NULL,
syncingObjectsCounter = 2
)
ObjectTypeMainScreen(
uiSyncStatusBadgeState = UiSyncStatusBadgeState.Visible(
status = SpaceSyncAndP2PStatusState.Success(
spaceSyncUpdate = spaceSyncUpdate,
p2PStatusUpdate = P2PStatusUpdate.Initial
)
),
uiSyncStatusState = SyncStatusWidgetState.Hidden,
uiIconState = UiIconState(icon = ObjectIcon.Empty.Page, isEditable = true),
uiTitleState = UiTitleState(title = "title", isEditable = true),
uiFieldsButtonState = UiFieldsButtonState.Visible(4),
uiLayoutButtonState = UiLayoutButtonState.Visible(layout = ObjectType.Layout.VIDEO),
uiTemplatesHeaderState = UiTemplatesHeaderState.Visible(count = "3"),
uiTemplatesAddIconState = UiTemplatesAddIconState.Visible,
uiTemplatesListState = UiTemplatesListState(
items = listOf(
TemplateView.Template(
id = "1",
name = "Template 1",
targetTypeId = TypeId("page"),
targetTypeKey = TypeKey("ot-page"),
layout = ObjectType.Layout.BASIC,
image = null,
emoji = ":)",
coverColor = CoverColor.RED,
coverGradient = null,
coverImage = null,
),
TemplateView.Template(
id = "2",
name = "Template 2",
targetTypeId = TypeId("note"),
targetTypeKey = TypeKey("ot-note"),
layout = ObjectType.Layout.NOTE,
image = null,
emoji = null,
coverColor = null,
coverGradient = null,
coverImage = null,
),
TemplateView.New(
targetTypeId = TypeId("32423"),
targetTypeKey = TypeKey("43232")
)
)
),
uiObjectsAddIconState = UiObjectsAddIconState.Visible,
uiObjectsHeaderState = UiObjectsHeaderState(count = "3"),
uiObjectsSettingsIconState = UiObjectsSettingsIconState.Visible,
uiObjectsListState = UiObjectsListState(emptyList()),
uiContentState = UiContentState.Idle(),
uiObjectsMenuState = UiMenuState.EMPTY,
uiDeleteAlertState = UiDeleteAlertState.Hidden,
uiEditButtonState = UiEditButton.Visible,
uiLayoutTypeState = UiLayoutTypeState.Hidden,
onTypeEvent = {}
)
}

View file

@ -0,0 +1,62 @@
package com.anytypeio.anytype.feature_object_type.ui
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ObjectType.Layout
import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncAndP2PStatusState
import com.anytypeio.anytype.presentation.objects.ObjectsListSort
import com.anytypeio.anytype.presentation.objects.UiObjectsListItem
import com.anytypeio.anytype.presentation.templates.TemplateView
sealed class TypeEvent {
//region TopBar
data class OnSyncStatusClick(val status: SpaceSyncAndP2PStatusState) : TypeEvent()
data object OnSyncStatusDismiss : TypeEvent()
data object OnMenuItemDeleteClick : TypeEvent()
data object OnAlertDeleteDismiss : TypeEvent()
data object OnAlertDeleteConfirm : TypeEvent()
data object OnBackClick : TypeEvent()
//endregion
//region Object Type Header
data object OnObjectTypeIconClick : TypeEvent()
data class OnObjectTypeTitleUpdate(val title: String) : TypeEvent()
//endregion
//region Sets
data object OnCreateSetClick : TypeEvent()
data class OnOpenSetClick(val setId: Id) : TypeEvent()
//endregion
//region Objects Header
data class OnSortClick(val sort: ObjectsListSort) : TypeEvent()
data object OnCreateObjectIconClick : TypeEvent()
//endregion
//region Objects list
data class OnObjectItemClick(val item: UiObjectsListItem) : TypeEvent()
//endregion
//region Templates
data object OnTemplatesAddIconClick : TypeEvent()
data class OnTemplateItemClick(val item: TemplateView) : TypeEvent()
sealed class OnTemplateMenuClick : TypeEvent() {
data class SetAsDefault(val item: TemplateView) : OnTemplateMenuClick()
data class Edit(val item: TemplateView) : OnTemplateMenuClick()
data class Duplicate(val item: TemplateView) : OnTemplateMenuClick()
data class Delete(val item: TemplateView) : OnTemplateMenuClick()
}
//endregion
//region Layout type
data object OnLayoutTypeDismiss : TypeEvent()
data class OnLayoutTypeItemClick(val item: Layout) : TypeEvent()
//endregion
data object OnLayoutButtonClick : TypeEvent()
data object OnFieldsButtonClick : TypeEvent()
data object OnObjectsSettingsIconClick : TypeEvent()
}

View file

@ -0,0 +1,211 @@
package com.anytypeio.anytype.feature_object_type.ui
import androidx.compose.runtime.Immutable
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Key
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncAndP2PStatusState
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.presentation.objects.MenuSortsItem
import com.anytypeio.anytype.presentation.objects.ObjectIcon
import com.anytypeio.anytype.presentation.objects.ObjectsListSort
import com.anytypeio.anytype.presentation.sync.SyncStatusWidgetState
import com.anytypeio.anytype.presentation.templates.TemplateView
data class ObjectTypeVmParams(
val objectId: Id,
val spaceId: SpaceId,
val withSubscriptions: Boolean,
val showHiddenFields: Boolean
)
sealed class ObjectTypeCommand {
data object Back : ObjectTypeCommand()
data class OpenTemplate(
val templateId: Id,
val typeId: Id,
val typeKey: Key,
val spaceId: Id
) : ObjectTypeCommand()
data object OpenEmojiPicker : ObjectTypeCommand()
data object OpenFieldsScreen : ObjectTypeCommand()
data class OpenAddFieldScreen(val typeId: Id, val space: Id, val isSet: Boolean = false) : ObjectTypeCommand()
}
//region OBJECT TYPE HEADER (title + icon)
data class UiTitleState(val title: String, val isEditable: Boolean) {
companion object {
val EMPTY = UiTitleState(title = "", isEditable = false)
}
}
data class UiIconState(val icon: ObjectIcon, val isEditable: Boolean) {
companion object {
val EMPTY = UiIconState(icon = ObjectIcon.None, isEditable = false)
}
}
//endregion
//region LAYOUTS
sealed class UiLayoutButtonState {
data object Hidden : UiLayoutButtonState()
data class Visible(val layout: ObjectType.Layout) : UiLayoutButtonState()
}
sealed class UiLayoutTypeState {
data object Hidden : UiLayoutTypeState()
data class Visible(
val layouts: List<ObjectType.Layout>,
val selectedLayout: ObjectType.Layout? = null
) : UiLayoutTypeState()
}
//endregion
sealed class UiFieldsButtonState {
data object Hidden : UiFieldsButtonState()
data class Visible(val count: Int) : UiFieldsButtonState()
}
//region MENU
@Immutable
sealed class UiMenuSetItem {
data object Hidden : UiMenuSetItem()
data object CreateSet : UiMenuSetItem()
@Immutable
data class OpenSet(val setId: Id) : UiMenuSetItem()
}
data class UiMenuState(
val container: MenuSortsItem.Container,
val sorts: List<MenuSortsItem.Sort>,
val types: List<MenuSortsItem.SortType>,
val objSetItem: UiMenuSetItem
) {
companion object {
val EMPTY = UiMenuState(
container = MenuSortsItem.Container(sort = ObjectsListSort.ByName()),
sorts = emptyList(),
types = emptyList(),
objSetItem = UiMenuSetItem.Hidden
)
}
}
@Immutable
sealed class UiSettingsMenuState {
data object Hidden : UiSettingsMenuState()
@Immutable
data class Visible(
val menuItems: List<UiSettingsMenuItem>
) : UiSettingsMenuState()
}
@Immutable
sealed class UiTemplatesMenuState {
data object Hidden : UiTemplatesMenuState()
@Immutable
data class Visible(
val menuItems: List<UiTemplatesMenuItem>
) : UiTemplatesMenuState()
}
@Immutable
enum class UiSettingsMenuItem {
DELETE
}
@Immutable
enum class UiTemplatesMenuItem {
DELETE, DUPLICATE
}
@Immutable
enum class UiObjectsMenuItem {
OPEN_SET, SORT_BY,
}
//endregion
//region TEMPLATES HEADER
sealed class UiTemplatesHeaderState {
data object Hidden : UiTemplatesHeaderState()
data class Visible(val count: String) : UiTemplatesHeaderState()
}
sealed class UiTemplatesAddIconState {
data object Hidden : UiTemplatesAddIconState()
data object Visible : UiTemplatesAddIconState()
}
//endregion
//region TEMPLATES LIST
data class UiTemplatesListState(
val items: List<TemplateView>
) {
companion object {
val EMPTY = UiTemplatesListState(items = emptyList())
}
}
//endregion
//region OBJECTS HEADER
data class UiObjectsHeaderState(val count: String) {
companion object {
val EMPTY = UiObjectsHeaderState(count = "")
}
}
sealed class UiObjectsAddIconState {
data object Hidden : UiObjectsAddIconState()
data object Visible : UiObjectsAddIconState()
}
sealed class UiObjectsSettingsIconState {
data object Hidden : UiObjectsSettingsIconState()
data object Visible : UiObjectsSettingsIconState()
}
//endregion
sealed class UiEditButton {
data object Hidden : UiEditButton()
data object Visible : UiEditButton()
}
//region ERRORS
sealed class UiErrorState {
data object Hidden : UiErrorState()
data class Show(val reason: Reason) : UiErrorState()
sealed class Reason {
data class ErrorGettingObjects(val msg: String) : Reason()
data class Other(val msg: String) : Reason()
}
}
//endregion
//region ALERTS
sealed class UiDeleteAlertState {
data object Hidden : UiDeleteAlertState()
data object Show : UiDeleteAlertState()
}
//endregion
//region SYNC STATUS
sealed class UiSyncStatusWidgetState {
data object Hidden : UiSyncStatusWidgetState()
data class Visible(val status: SyncStatusWidgetState) : UiSyncStatusWidgetState()
}
sealed class UiSyncStatusBadgeState {
data object Hidden : UiSyncStatusBadgeState()
data class Visible(val status: SpaceSyncAndP2PStatusState) : UiSyncStatusBadgeState()
}
//endregion

View file

@ -0,0 +1,254 @@
package com.anytypeio.anytype.feature_object_type.ui
import com.anytypeio.anytype.core_models.CoverType
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_models.RelationFormat
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.core_models.primitives.TypeId
import com.anytypeio.anytype.core_models.primitives.TypeKey
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.domain.primitives.FieldParser
import com.anytypeio.anytype.domain.resources.StringResourceProvider
import com.anytypeio.anytype.feature_object_type.fields.UiAddFieldItem
import com.anytypeio.anytype.feature_object_type.fields.UiFieldObjectItem
import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListItem
import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListItem.Item
import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListItem.Section
import com.anytypeio.anytype.presentation.editor.cover.CoverImageHashProvider
import com.anytypeio.anytype.presentation.mapper.objectIcon
import com.anytypeio.anytype.presentation.relations.BasicObjectCoverWrapper
import com.anytypeio.anytype.presentation.relations.getCover
import com.anytypeio.anytype.presentation.templates.TemplateView
//region Mapping
fun ObjectWrapper.Basic.toTemplateView(
objType: ObjectWrapper.Type,
urlBuilder: UrlBuilder,
coverImageHashProvider: CoverImageHashProvider,
): TemplateView.Template {
val coverContainer = if (coverType != CoverType.NONE) {
BasicObjectCoverWrapper(this)
.getCover(urlBuilder, coverImageHashProvider)
} else {
null
}
return TemplateView.Template(
id = id,
name = name.orEmpty(),
targetTypeId = TypeId(targetObjectType.orEmpty()),
emoji = if (!iconEmoji.isNullOrBlank()) iconEmoji else null,
image = iconImage?.takeIf { it.isNotBlank() }?.let { urlBuilder.thumbnail(it) },
layout = objType.recommendedLayout ?: ObjectType.Layout.BASIC,
coverColor = coverContainer?.coverColor,
coverImage = coverContainer?.coverImage,
coverGradient = coverContainer?.coverGradient,
isDefault = false,
targetTypeKey = TypeKey(objType.uniqueKey)
)
}
//endregion
/**
* Extension function to safely get a name for the relation.
* If the name is blank, returns a default untitled title.
*/
private fun ObjectWrapper.Relation.getName(stringResourceProvider: StringResourceProvider): String =
if (name.isNullOrBlank()) {
stringResourceProvider.getUntitledObjectTitle()
} else {
name!!
}
suspend fun buildUiFieldsList(
objType: ObjectWrapper.Type,
stringResourceProvider: StringResourceProvider,
fieldParser: FieldParser,
urlBuilder: UrlBuilder,
storeOfObjectTypes: StoreOfObjectTypes,
storeOfRelations: StoreOfRelations,
objTypeConflictingFields: List<Id>,
showHiddenFields: Boolean
): List<UiFieldsListItem> {
val parsedFields = fieldParser.getObjectTypeParsedFields(
objectType = objType,
storeOfRelations = storeOfRelations,
objectTypeConflictingFieldsIds = objTypeConflictingFields
)
// The mapping functions already skip the Relations.DESCRIPTION key.
val headerItems = parsedFields.header.mapNotNull {
mapToUiFieldsDraggableListItem(
field = it,
stringResourceProvider = stringResourceProvider,
fieldParser = fieldParser,
urlBuilder = urlBuilder,
storeOfObjectTypes = storeOfObjectTypes
)
}
val sidebarItems = parsedFields.sidebar.mapNotNull {
mapToUiFieldsDraggableListItem(
field = it,
stringResourceProvider = stringResourceProvider,
fieldParser = fieldParser,
urlBuilder = urlBuilder,
storeOfObjectTypes = storeOfObjectTypes
)
}
val hiddenItems = parsedFields.hidden.mapNotNull {
mapToUiFieldsDraggableListItem(
field = it,
stringResourceProvider = stringResourceProvider,
fieldParser = fieldParser,
urlBuilder = urlBuilder,
storeOfObjectTypes = storeOfObjectTypes
)
}
val conflictedItems = parsedFields.localWithoutSystem.mapNotNull {
mapToUiFieldsLocalListItem(
field = it,
stringResourceProvider = stringResourceProvider,
fieldParser = fieldParser,
urlBuilder = urlBuilder,
storeOfObjectTypes = storeOfObjectTypes
)
}
//this items goes to the Hidden section as draggable items
val conflictedSystemItems = parsedFields.localSystem.mapNotNull {
mapToUiFieldsDraggableListItem(
field = it,
stringResourceProvider = stringResourceProvider,
fieldParser = fieldParser,
urlBuilder = urlBuilder,
storeOfObjectTypes = storeOfObjectTypes
)
}
val fileRecommendedFields = parsedFields.file.mapNotNull {
mapToUiFieldsDraggableListItem(
field = it,
stringResourceProvider = stringResourceProvider,
fieldParser = fieldParser,
urlBuilder = urlBuilder,
storeOfObjectTypes = storeOfObjectTypes
)
}
return buildList {
add(Section.Header(canAdd = true))
addAll(headerItems)
add(Section.SideBar(canAdd = true))
addAll(sidebarItems)
//todo file fields are off for now
// if (fileRecommendedFields.isNotEmpty()) {
// add(Section.File(canAdd = false))
// addAll(fileRecommendedFields)
// }
if (showHiddenFields) {
add(Section.Hidden(canAdd = false))
addAll(hiddenItems)
addAll(conflictedSystemItems)
}
if (conflictedItems.isNotEmpty()) {
add(Section.Local(canAdd = false))
addAll(conflictedItems)
}
}
}
/**
* Shared helper to build the limit object types for a field.
*/
private suspend fun mapLimitObjectTypes(
relation: ObjectWrapper.Relation,
storeOfObjectTypes: StoreOfObjectTypes,
fieldParser: FieldParser,
urlBuilder: UrlBuilder
): List<UiFieldObjectItem> {
return if (relation.format == RelationFormat.OBJECT && relation.relationFormatObjectTypes.isNotEmpty()) {
relation.relationFormatObjectTypes.mapNotNull { key ->
storeOfObjectTypes.getByKey(key)?.let { objType ->
UiFieldObjectItem(
id = objType.id,
key = objType.uniqueKey,
title = fieldParser.getObjectName(objType),
icon = objType.objectIcon(urlBuilder)
)
}
}
} else {
emptyList()
}
}
/**
* Maps a field to a draggable UI list item.
* Returns null if the field key equals DESCRIPTION.
*/
private suspend fun mapToUiFieldsDraggableListItem(
field: ObjectWrapper.Relation,
stringResourceProvider: StringResourceProvider,
storeOfObjectTypes: StoreOfObjectTypes,
fieldParser: FieldParser,
urlBuilder: UrlBuilder
): UiFieldsListItem? {
if (field.key == Relations.DESCRIPTION) return null
val limitObjectTypes = mapLimitObjectTypes(field, storeOfObjectTypes, fieldParser, urlBuilder)
return Item.Draggable(
id = field.id,
fieldKey = field.key,
fieldTitle = field.getName(stringResourceProvider),
format = field.format,
limitObjectTypes = limitObjectTypes,
isEditableField = fieldParser.isFieldEditable(field),
canDelete = fieldParser.isFieldCanBeDeletedFromType(field)
)
}
/**
* Maps a field to a local UI list item.
* Returns null if the field key equals DESCRIPTION.
*/
private suspend fun mapToUiFieldsLocalListItem(
field: ObjectWrapper.Relation,
stringResourceProvider: StringResourceProvider,
storeOfObjectTypes: StoreOfObjectTypes,
fieldParser: FieldParser,
urlBuilder: UrlBuilder
): UiFieldsListItem? {
if (field.key == Relations.DESCRIPTION) return null
val limitObjectTypes = mapLimitObjectTypes(field, storeOfObjectTypes, fieldParser, urlBuilder)
return Item.Local(
id = field.id,
fieldKey = field.key,
fieldTitle = field.getName(stringResourceProvider),
format = field.format,
limitObjectTypes = limitObjectTypes,
isEditableField = fieldParser.isFieldEditable(field)
)
}
fun ObjectWrapper.Relation.mapToUiAddFieldListItem(
stringResourceProvider: StringResourceProvider
): UiAddFieldItem? {
val field = this
if (field.key == Relations.DESCRIPTION) return null
return UiAddFieldItem(
id = field.id,
fieldKey = field.key,
fieldTitle = field.getName(stringResourceProvider),
format = field.format
)
}

View file

@ -0,0 +1,80 @@
package com.anytypeio.anytype.feature_object_type.ui.alerts
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
import com.anytypeio.anytype.core_ui.foundation.AlertConfig
import com.anytypeio.anytype.core_ui.foundation.BUTTON_PRIMARY
import com.anytypeio.anytype.core_ui.foundation.BUTTON_SECONDARY
import com.anytypeio.anytype.core_ui.foundation.Dragger
import com.anytypeio.anytype.core_ui.foundation.GRADIENT_TYPE_RED
import com.anytypeio.anytype.core_ui.foundation.GenericAlert
import com.anytypeio.anytype.feature_object_type.R
import com.anytypeio.anytype.feature_object_type.ui.TypeEvent
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DeleteAlertScreen(
onTypeEvent: (TypeEvent) -> Unit
) {
val bottomSheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
)
ModalBottomSheet(
dragHandle = {
Column {
Spacer(modifier = Modifier.height(6.dp))
Dragger()
Spacer(modifier = Modifier.height(6.dp))
}
},
scrimColor = colorResource(id = R.color.modal_screen_outside_background),
containerColor = colorResource(id = R.color.background_secondary),
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
sheetState = bottomSheetState,
onDismissRequest = {
onTypeEvent(TypeEvent.OnAlertDeleteDismiss)
},
content = {
GenericAlert(
config = AlertConfig.WithTwoButtons(
title = stringResource(R.string.are_you_sure_delete_one_object),
description = stringResource(R.string.delete_irrevocably_one_object),
firstButtonText = stringResource(R.string.cancel),
secondButtonText = stringResource(R.string.delete),
icon = AlertConfig.Icon(
GRADIENT_TYPE_RED,
icon = R.drawable.ic_alert_error
),
firstButtonType = BUTTON_SECONDARY,
secondButtonType = BUTTON_PRIMARY,
),
onFirstButtonClicked = {
onTypeEvent(TypeEvent.OnAlertDeleteDismiss)
},
onSecondButtonClicked = {
onTypeEvent(TypeEvent.OnAlertDeleteConfirm)
}
)
}
)
}
@DefaultPreviews
@Composable
fun DeleteAlertScreenPreview() {
DeleteAlertScreen(
onTypeEvent = {}
)
}

View file

@ -0,0 +1,163 @@
package com.anytypeio.anytype.feature_object_type.ui.header
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable
import com.anytypeio.anytype.core_ui.views.HeadlineTitle
import com.anytypeio.anytype.core_ui.widgets.ListWidgetObjectIcon
import com.anytypeio.anytype.feature_object_type.ui.TypeEvent
import com.anytypeio.anytype.feature_object_type.ui.UiIconState
import com.anytypeio.anytype.feature_object_type.ui.UiTitleState
import com.anytypeio.anytype.presentation.objects.ObjectIcon
@Composable
fun IconAndTitleWidget(
modifier: Modifier,
uiIconState: UiIconState,
uiTitleState: UiTitleState,
onTypeEvent: (TypeEvent) -> Unit
) {
Row(modifier = modifier) {
ListWidgetObjectIcon(
modifier = Modifier
.size(32.dp)
.noRippleThrottledClickable {
if (uiIconState.isEditable) {
onTypeEvent.invoke(TypeEvent.OnObjectTypeIconClick)
}
},
icon = uiIconState.icon,
backgroundColor = R.color.amp_transparent
)
NameField(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
uiTitleState = uiTitleState,
onTypeEvent = onTypeEvent,
)
}
}
@Composable
fun NameField(
modifier: Modifier,
uiTitleState: UiTitleState,
onTypeEvent: (TypeEvent) -> Unit
) {
var innerValue by remember(uiTitleState.title) { mutableStateOf(uiTitleState.title) }
val focusManager = LocalFocusManager.current
val focusRequester = remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current
BasicTextField(
value = innerValue,
onValueChange = {
innerValue = it
onTypeEvent.invoke(
TypeEvent.OnObjectTypeTitleUpdate(
title = innerValue
)
)
},
textStyle = HeadlineTitle.copy(color = colorResource(id = R.color.text_primary)),
singleLine = false,
enabled = uiTitleState.isEditable,
cursorBrush = SolidColor(colorResource(id = R.color.text_primary)),
modifier = modifier
.padding(start = 12.dp, end = 20.dp)
.focusRequester(focusRequester),
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions {
keyboardController?.hide()
focusManager.clearFocus()
onTypeEvent.invoke(
TypeEvent.OnObjectTypeTitleUpdate(
title = innerValue
)
)
},
decorationBox = { innerTextField ->
Box(
modifier = Modifier
.fillMaxWidth()
.height(32.dp),
contentAlignment = Alignment.CenterStart
) {
if (innerValue.isEmpty()) {
Text(
modifier = Modifier.wrapContentSize(),
text = stringResource(id = R.string.untitled),
style = HeadlineTitle,
color = colorResource(id = R.color.text_tertiary),
)
}
}
innerTextField()
}
)
}
@DefaultPreviews
@Composable
fun IconAndTitleWidgetPreview() {
IconAndTitleWidget(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
onTypeEvent = {},
uiIconState = UiIconState(icon = ObjectIcon.Task(isChecked = false), isEditable = true),
uiTitleState = UiTitleState(
title = "I understand that contributing to this repository will require me to agree with the",
isEditable = true
)
)
}
@DefaultPreviews
@Composable
fun IconAndTitleEmptyWidgetPreview() {
IconAndTitleWidget(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
onTypeEvent = {},
uiIconState = UiIconState(icon = ObjectIcon.Task(isChecked = false), isEditable = true),
uiTitleState = UiTitleState(
title = "",
isEditable = true
)
)
}

View file

@ -0,0 +1,117 @@
package com.anytypeio.anytype.feature_object_type.ui.header
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.toLowerCase
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.common.DefaultPreviews
import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable
import com.anytypeio.anytype.core_ui.views.PreviewTitle2Medium
import com.anytypeio.anytype.feature_object_type.ui.TypeEvent
import com.anytypeio.anytype.feature_object_type.ui.UiFieldsButtonState
import com.anytypeio.anytype.feature_object_type.ui.UiLayoutButtonState
@Composable
fun HorizontalButtons(
modifier: Modifier,
uiLayoutButtonState: UiLayoutButtonState,
uiFieldsButtonState: UiFieldsButtonState,
onTypeEvent: (TypeEvent) -> Unit
) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
val modifierButton = Modifier
.height(40.dp)
.wrapContentWidth()
.border(
width = 1.dp,
color = colorResource(R.color.shape_primary),
shape = RoundedCornerShape(size = 8.dp)
)
if (uiLayoutButtonState is UiLayoutButtonState.Visible) {
Row(
modifier = modifierButton.noRippleThrottledClickable {
onTypeEvent(TypeEvent.OnLayoutButtonClick)
},
verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier
.wrapContentSize()
.padding(start = 12.dp),
text = stringResource(R.string.button_layout),
style = PreviewTitle2Medium,
color = colorResource(R.color.text_primary)
)
Text(
modifier = Modifier
.wrapContentSize()
.padding(start = 6.dp, end = 12.dp),
text = uiLayoutButtonState.layout.name.substring(0, 1).uppercase()
+ uiLayoutButtonState.layout.name.substring(1)
.toLowerCase(Locale.current),
style = PreviewTitle2Medium,
color = colorResource(R.color.glyph_active)
)
}
}
if (uiFieldsButtonState is UiFieldsButtonState.Visible) {
Row(
modifier = modifierButton.noRippleThrottledClickable {
onTypeEvent(TypeEvent.OnFieldsButtonClick)
},
verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier
.wrapContentSize()
.padding(start = 12.dp),
text = stringResource(R.string.button_fields),
style = PreviewTitle2Medium,
color = colorResource(R.color.text_primary)
)
Text(
modifier = Modifier
.wrapContentSize()
.padding(start = 6.dp, end = 12.dp),
text = uiFieldsButtonState.count.toString(),
style = PreviewTitle2Medium,
color = colorResource(R.color.glyph_active)
)
}
}
}
}
@DefaultPreviews
@Composable
fun HorizontalButtonsPreview() {
HorizontalButtons(
modifier = Modifier
.height(32.dp)
.fillMaxWidth()
.padding(start = 20.dp),
uiLayoutButtonState = UiLayoutButtonState.Visible(ObjectType.Layout.BASIC),
uiFieldsButtonState = UiFieldsButtonState.Visible(3),
onTypeEvent = {}
)
}

View file

@ -0,0 +1,196 @@
package com.anytypeio.anytype.feature_object_type.ui.header
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.IconButton
import androidx.compose.material.Text
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_models.multiplayer.P2PStatusUpdate
import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncAndP2PStatusState
import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncError
import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncNetwork
import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncStatus
import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncUpdate
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable
import com.anytypeio.anytype.core_ui.syncstatus.StatusBadge
import com.anytypeio.anytype.core_ui.views.BodyRegular
import com.anytypeio.anytype.core_ui.views.PreviewTitle2Regular
import com.anytypeio.anytype.feature_object_type.R
import com.anytypeio.anytype.feature_object_type.ui.UiEditButton
import com.anytypeio.anytype.feature_object_type.ui.UiSyncStatusBadgeState
import com.anytypeio.anytype.feature_object_type.ui.UiTitleState
import com.anytypeio.anytype.feature_object_type.ui.TypeEvent
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TopToolbar(
modifier: Modifier,
uiEditButtonState: UiEditButton,
uiSyncStatusBadgeState: UiSyncStatusBadgeState,
topBarScrollBehavior: TopAppBarScrollBehavior,
uiTitleState: UiTitleState,
onTypeEvent: (TypeEvent) -> Unit
) {
val isIconMenuExpanded = remember {
mutableStateOf(false)
}
CenterAlignedTopAppBar(
modifier = modifier.fillMaxWidth(),
expandedHeight = 48.dp,
scrollBehavior = topBarScrollBehavior,
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = colorResource(id = R.color.background_primary),
scrolledContainerColor = colorResource(id = R.color.background_primary),
titleContentColor = colorResource(id = R.color.palette_system_red)
),
title = {
if (topBarScrollBehavior.state.overlappedFraction > 0.7f) {
Box(
modifier = Modifier.fillMaxHeight(),
contentAlignment = Alignment.Center
) {
Text(
text = uiTitleState.title,
style = PreviewTitle2Regular,
color = colorResource(R.color.text_primary),
textAlign = TextAlign.Center
)
}
}
},
navigationIcon = {
Box(
modifier = Modifier
.width(56.dp)
.height(48.dp)
.noRippleThrottledClickable {
onTypeEvent(TypeEvent.OnBackClick)
},
contentAlignment = Alignment.Center
) {
Image(
modifier = Modifier.wrapContentSize(),
painter = painterResource(R.drawable.ic_default_top_back),
contentDescription = stringResource(R.string.content_desc_back_button)
)
}
},
actions = {
if (uiSyncStatusBadgeState is UiSyncStatusBadgeState.Visible) {
Box(
modifier = Modifier
.size(48.dp)
.noRippleThrottledClickable {
onTypeEvent(
TypeEvent.OnSyncStatusClick(
status = uiSyncStatusBadgeState.status
)
)
},
) {
StatusBadge(
status = uiSyncStatusBadgeState.status,
modifier = Modifier
.align(Alignment.Center)
)
}
}
if (uiEditButtonState is UiEditButton.Visible) {
IconButton(
modifier = Modifier
.size(48.dp),
onClick = {
isIconMenuExpanded.value = !isIconMenuExpanded.value
}
) {
Image(
modifier = Modifier.size(24.dp),
painter = painterResource(id = R.drawable.ic_space_list_dots),
contentDescription = "More options"
)
DropdownMenu(
modifier = Modifier
.width(244.dp),
expanded = isIconMenuExpanded.value,
offset = DpOffset(x = 0.dp, y = 0.dp),
onDismissRequest = {
isIconMenuExpanded.value = false
},
shape = RoundedCornerShape(10.dp),
containerColor = colorResource(id = R.color.background_secondary),
) {
DropdownMenuItem(
text = {
Text(
text = stringResource(R.string.object_type_settings_item_remove),
style = BodyRegular,
color = colorResource(id = R.color.palette_system_red)
)
},
onClick = {
onTypeEvent(TypeEvent.OnMenuItemDeleteClick)
isIconMenuExpanded.value = false
},
)
}
}
}
}
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@DefaultPreviews
fun TopToolbarPreview() {
val topAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(
state = rememberTopAppBarState()
)
val spaceSyncUpdate = SpaceSyncUpdate.Update(
id = "1",
status = SpaceSyncStatus.SYNCING,
network = SpaceSyncNetwork.ANYTYPE,
error = SpaceSyncError.NULL,
syncingObjectsCounter = 2
)
TopToolbar(
modifier = Modifier.fillMaxWidth(),
uiSyncStatusBadgeState = UiSyncStatusBadgeState.Visible(
status = SpaceSyncAndP2PStatusState.Success(
spaceSyncUpdate = spaceSyncUpdate,
p2PStatusUpdate = P2PStatusUpdate.Initial
)
),
uiEditButtonState = UiEditButton.Visible,
onTypeEvent = {},
topBarScrollBehavior = topAppBarScrollBehavior,
uiTitleState = UiTitleState(title = "Page", isEditable = true)
)
}

View file

@ -0,0 +1,341 @@
package com.anytypeio.anytype.feature_object_type.ui.layouts
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.toLowerCase
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
import com.anytypeio.anytype.core_ui.foundation.Dragger
import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable
import com.anytypeio.anytype.core_ui.views.AvatarTitle
import com.anytypeio.anytype.core_ui.views.Title1
import com.anytypeio.anytype.core_ui.views.fontInterRegular
import com.anytypeio.anytype.feature_object_type.R
import com.anytypeio.anytype.feature_object_type.ui.UiLayoutTypeState
import com.anytypeio.anytype.feature_object_type.ui.TypeEvent
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TypeLayoutsScreen(
modifier: Modifier,
uiState: UiLayoutTypeState.Visible,
onTypeEvent: (TypeEvent) -> Unit
) {
val bottomSheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
)
ModalBottomSheet(
modifier = modifier,
dragHandle = {
Column {
Spacer(modifier = Modifier.height(6.dp))
Dragger()
Spacer(modifier = Modifier.height(6.dp))
}
},
scrimColor = colorResource(id = R.color.modal_screen_outside_background),
containerColor = colorResource(id = R.color.background_secondary),
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
sheetState = bottomSheetState,
onDismissRequest = {
onTypeEvent(TypeEvent.OnLayoutTypeDismiss)
}
) {
Spacer(modifier = Modifier.height(12.dp))
Text(
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
text = stringResource(id = R.string.layout_type),
style = Title1,
color = colorResource(id = R.color.text_primary)
)
Spacer(modifier = Modifier.height(20.dp))
LazyRow(
modifier = Modifier
.fillMaxWidth()
.height(252.dp),
contentPadding = PaddingValues(start = 20.dp, end = 20.dp),
horizontalArrangement = Arrangement.spacedBy(
space = 12.dp,
alignment = Alignment.End
),
) {
items(
count = uiState.layouts.size,
key = { index -> uiState.layouts[index].code },
itemContent = {
val item = uiState.layouts[it]
val (borderWidth, borderColor) = if (item.code == uiState.selectedLayout?.code) {
2.dp to colorResource(id = R.color.palette_system_amber_50)
} else {
1.dp to colorResource(id = R.color.shape_secondary)
}
Column(
modifier = Modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
TemplateItemContent(
modifier = Modifier
.width(120.dp)
.height(224.dp)
.border(
width = borderWidth,
color = borderColor,
shape = RoundedCornerShape(12.dp)
)
.padding(horizontal = 16.dp)
.noRippleThrottledClickable{
onTypeEvent(TypeEvent.OnLayoutTypeItemClick(item))
onTypeEvent(TypeEvent.OnLayoutTypeDismiss)
},
item = item
)
Text(
modifier = Modifier.padding(top = 4.dp),
text = item.name.substring(0, 1).uppercase()
+ item.name.substring(1)
.toLowerCase(Locale.current),
style = TextStyle(
fontFamily = fontInterRegular,
fontWeight = FontWeight.W500,
fontSize = 13.sp,
letterSpacing = (-0.024).em
),
color = colorResource(id = R.color.text_primary)
)
Spacer(modifier = Modifier.height(32.dp))
}
}
)
}
}
}
@Composable
private fun TemplateItemContent(
modifier: Modifier,
item: ObjectType.Layout
) {
when (item) {
ObjectType.Layout.BASIC -> {
Column(
modifier = modifier
) {
Spacer(modifier = Modifier.height(28.dp))
Box(
modifier = Modifier
.size(40.dp)
.background(
shape = RoundedCornerShape(5.dp),
color = colorResource(id = R.color.shape_tertiary)
),
contentAlignment = Alignment.Center
) {
Image(
modifier = Modifier.wrapContentSize(),
painter = painterResource(R.drawable.ic_type_layout_basic_icon),
contentDescription = "Basic layout icon"
)
}
Spacer(modifier = Modifier.height(14.dp))
Box(
modifier = Modifier
.width(64.dp)
.height(8.dp)
.background(
color = colorResource(id = R.color.shape_secondary),
shape = RoundedCornerShape(size = 1.dp)
)
)
Spacer(modifier = Modifier.height(8.dp))
Box(
modifier = Modifier
.width(24.dp)
.height(4.dp)
.background(
color = colorResource(id = R.color.shape_secondary),
shape = RoundedCornerShape(size = 1.dp)
)
)
Spacer(modifier = Modifier.height(16.dp))
BasicBlocks()
}
}
ObjectType.Layout.PROFILE -> {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(28.dp))
Box(
modifier = Modifier
.size(40.dp)
.background(
shape = CircleShape,
color = colorResource(id = R.color.shape_tertiary)
),
contentAlignment = Alignment.Center
) {
Text(
modifier = Modifier.wrapContentSize(),
text = "N",
style = AvatarTitle.copy(
fontSize = 24.sp
),
color = colorResource(id = R.color.glyph_active),
)
}
Spacer(modifier = Modifier.height(10.dp))
Box(
modifier = Modifier
.width(64.dp)
.height(8.dp)
.background(
color = colorResource(id = R.color.shape_secondary),
shape = RoundedCornerShape(size = 1.dp)
)
)
Spacer(modifier = Modifier.height(8.dp))
Box(
modifier = Modifier
.width(24.dp)
.height(4.dp)
.background(
color = colorResource(id = R.color.shape_secondary),
shape = RoundedCornerShape(size = 1.dp)
)
)
Spacer(modifier = Modifier.height(16.dp))
BasicBlocks()
}
}
ObjectType.Layout.TODO -> {
Column(
modifier = modifier
) {
Spacer(modifier = Modifier.height(40.dp))
Image(
modifier = Modifier.wrapContentSize(),
painter = painterResource(R.drawable.ic_type_layout_todo_icon),
contentDescription = "Todo layout icon"
)
Spacer(modifier = Modifier.height(8.dp))
Box(
modifier = Modifier
.width(48.dp)
.height(4.dp)
.background(
color = colorResource(id = R.color.shape_secondary),
shape = RoundedCornerShape(size = 1.dp)
)
)
Spacer(modifier = Modifier.height(16.dp))
BasicBlocks()
}
}
ObjectType.Layout.NOTE -> {
Column(
modifier = modifier
) {
Spacer(modifier = Modifier.height(46.dp))
Box(
modifier = Modifier
.width(24.dp)
.height(4.dp)
.background(
color = colorResource(id = R.color.shape_secondary),
shape = RoundedCornerShape(size = 1.dp)
)
)
Spacer(modifier = Modifier.height(16.dp))
BasicBlocks()
}
}
else -> {
//do nothing
}
}
}
@Composable
private fun ColumnScope.BasicBlocks() {
repeat(3) {
Box(
modifier = Modifier
.width(88.dp)
.height(6.dp)
.background(
color = colorResource(id = R.color.shape_secondary),
shape = RoundedCornerShape(size = 1.dp)
)
)
Spacer(modifier = Modifier.height(6.dp))
}
Box(
modifier = Modifier
.width(64.dp)
.height(6.dp)
.background(
color = colorResource(id = R.color.shape_secondary),
shape = RoundedCornerShape(size = 1.dp)
)
)
}
@DefaultPreviews
@Composable
fun TypeLayoutsScreenPreview() {
TypeLayoutsScreen(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
uiState = UiLayoutTypeState.Visible(
layouts = listOf(
ObjectType.Layout.PROFILE,
ObjectType.Layout.BASIC,
ObjectType.Layout.TODO,
ObjectType.Layout.NOTE
),
selectedLayout = ObjectType.Layout.BASIC
),
onTypeEvent = {})
}

View file

@ -0,0 +1,210 @@
package com.anytypeio.anytype.feature_object_type.ui.objects
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_models.DVSortType
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
import com.anytypeio.anytype.core_ui.foundation.Divider
import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable
import com.anytypeio.anytype.core_ui.lists.objects.menu.ObjectsListMenuItem
import com.anytypeio.anytype.core_ui.lists.objects.menu.ObjectsListSortingMenuContainer
import com.anytypeio.anytype.core_ui.views.BodyBold
import com.anytypeio.anytype.core_ui.views.PreviewTitle1Regular
import com.anytypeio.anytype.feature_object_type.R
import com.anytypeio.anytype.feature_object_type.ui.TypeEvent
import com.anytypeio.anytype.feature_object_type.ui.UiMenuSetItem
import com.anytypeio.anytype.feature_object_type.ui.UiMenuState
import com.anytypeio.anytype.feature_object_type.ui.UiObjectsAddIconState
import com.anytypeio.anytype.feature_object_type.ui.UiObjectsHeaderState
import com.anytypeio.anytype.feature_object_type.ui.UiObjectsSettingsIconState
import com.anytypeio.anytype.presentation.objects.MenuSortsItem
import com.anytypeio.anytype.presentation.objects.ObjectsListSort
import timber.log.Timber
@Composable
fun ObjectsHeader(
modifier: Modifier,
uiObjectsHeaderState: UiObjectsHeaderState,
uiObjectsAddIconState: UiObjectsAddIconState,
uiObjectsSettingsIconState: UiObjectsSettingsIconState,
uiObjectsMenuState: UiMenuState,
onTypeEvent: (TypeEvent) -> Unit
) {
var isMenuExpanded by remember { mutableStateOf(false) }
var isSortingExpanded by remember { mutableStateOf(false) }
Box(modifier = modifier) {
Row(
modifier = Modifier.matchParentSize(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier
.wrapContentSize()
.align(Alignment.CenterVertically),
text = stringResource(R.string.objects),
style = BodyBold,
color = colorResource(R.color.text_primary)
)
Text(
modifier = Modifier
.wrapContentSize()
.padding(start = 8.dp),
text = uiObjectsHeaderState.count,
style = PreviewTitle1Regular,
color = colorResource(R.color.text_secondary)
)
}
Row(
modifier = Modifier.align(Alignment.CenterEnd)
) {
if (uiObjectsSettingsIconState is UiObjectsSettingsIconState.Visible) {
Box(
modifier = Modifier
.height(48.dp)
.width(40.dp)
.noRippleThrottledClickable {
isMenuExpanded = !isMenuExpanded
},
contentAlignment = Alignment.CenterEnd
) {
Image(
modifier = Modifier.wrapContentSize(),
painter = painterResource(R.drawable.ic_space_list_dots),
contentDescription = "Objects settings icon"
)
}
DropdownMenu(
modifier = Modifier.width(252.dp),
expanded = isMenuExpanded,
onDismissRequest = { isMenuExpanded = false },
shape = RoundedCornerShape(size = 16.dp),
containerColor = colorResource(id = R.color.background_primary),
shadowElevation = 5.dp
) {
when (val item = uiObjectsMenuState.objSetItem) {
UiMenuSetItem.CreateSet -> {
ObjectsListMenuItem(
title = stringResource(R.string.object_type_objects_menu_create_set),
isSelected = false,
modifier = Modifier
.clickable { onTypeEvent(TypeEvent.OnCreateSetClick) }
)
Divider(
height = 8.dp,
paddingStart = 0.dp,
paddingEnd = 0.dp,
color = colorResource(R.color.shape_secondary)
)
}
is UiMenuSetItem.OpenSet -> {
ObjectsListMenuItem(
title = stringResource(R.string.object_type_objects_menu_open_set),
isSelected = false,
modifier = Modifier
.clickable { onTypeEvent(TypeEvent.OnOpenSetClick(setId = item.setId)) }
)
Divider(
height = 8.dp,
paddingStart = 0.dp,
paddingEnd = 0.dp,
color = colorResource(R.color.shape_secondary)
)
}
UiMenuSetItem.Hidden -> {}
}
ObjectsListSortingMenuContainer(
container = uiObjectsMenuState.container,
sorts = uiObjectsMenuState.sorts,
types = uiObjectsMenuState.types,
sortingExpanded = isSortingExpanded,
onSortClick = {
onTypeEvent(TypeEvent.OnSortClick(it))
},
onChangeSortExpandedState = { isSortingExpanded = it }
)
}
}
if (uiObjectsAddIconState is UiObjectsAddIconState.Visible) {
Box(
modifier = Modifier
.padding(start = 8.dp)
.height(48.dp)
.width(32.dp)
.noRippleThrottledClickable {
onTypeEvent(TypeEvent.OnCreateObjectIconClick)
},
contentAlignment = Alignment.CenterEnd
) {
Image(
modifier = Modifier.wrapContentSize(),
painter = painterResource(R.drawable.ic_default_plus),
contentDescription = "Add",
)
}
}
}
}
}
@DefaultPreviews
@Composable
fun ObjectsHeaderPreview() {
ObjectsHeader(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
uiObjectsHeaderState = UiObjectsHeaderState("3"),
uiObjectsAddIconState = UiObjectsAddIconState.Visible,
uiObjectsSettingsIconState = UiObjectsSettingsIconState.Visible,
uiObjectsMenuState = UiMenuState(
container = MenuSortsItem.Container(
sort = ObjectsListSort.ByName(isSelected = true)
),
sorts = listOf(
MenuSortsItem.Sort(
sort = ObjectsListSort.ByName(isSelected = true)
),
),
types = listOf(
MenuSortsItem.SortType(
sort = ObjectsListSort.ByName(isSelected = true),
sortType = DVSortType.DESC,
isSelected = true
),
MenuSortsItem.SortType(
sort = ObjectsListSort.ByDateCreated(isSelected = false),
sortType = DVSortType.ASC,
isSelected = false
),
),
objSetItem = UiMenuSetItem.CreateSet
),
onTypeEvent = {}
)
}

View file

@ -0,0 +1,193 @@
package com.anytypeio.anytype.feature_object_type.ui.templates
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.border
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Text
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.foundation.Divider
import com.anytypeio.anytype.core_ui.views.BodyRegular
import com.anytypeio.anytype.core_ui.widgets.TemplateItemContent
import com.anytypeio.anytype.feature_object_type.ui.UiTemplatesListState
import com.anytypeio.anytype.feature_object_type.ui.TypeEvent
import com.anytypeio.anytype.presentation.templates.TemplateView
import timber.log.Timber
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun TemplatesList(
uiTemplatesListState: UiTemplatesListState,
onTypeEvent: (TypeEvent) -> Unit
) {
Timber.d("TemplatesList :$uiTemplatesListState")
val scrollState = rememberLazyListState()
val interactionSource = remember { MutableInteractionSource() }
LazyRow(
state = scrollState,
modifier = Modifier
.wrapContentHeight()
.fillMaxWidth(),
contentPadding = PaddingValues(start = 20.dp, end = 20.dp),
horizontalArrangement = Arrangement.spacedBy(5.dp)
) {
items(
count = uiTemplatesListState.items.size,
key = { index ->
val item = uiTemplatesListState.items[index]
when (item) {
is TemplateView.Blank -> item.id
is TemplateView.New -> "new"
is TemplateView.Template -> item.id
}
},
itemContent = {
var isMenuExpanded by remember { mutableStateOf(false) }
val item = uiTemplatesListState.items[it]
Box(
modifier = Modifier
.border(
width = 1.dp,
color = colorResource(id = R.color.shape_secondary),
shape = RoundedCornerShape(size = 16.dp)
)
.height(224.dp)
.width(120.dp)
.combinedClickable(
interactionSource = interactionSource,
indication = ripple(bounded = false, radius = 24.dp),
onClick = {
onTypeEvent(TypeEvent.OnTemplateItemClick(item))
},
onLongClick = {
if (item is TemplateView.Template) {
isMenuExpanded = true
}
},
enabled = true,
)
) {
TemplateItemContent(
item = item,
showDefaultIcon = true
)
DropdownMenu(
modifier = Modifier.width(244.dp),
expanded = isMenuExpanded,
onDismissRequest = { isMenuExpanded = false },
shape = RoundedCornerShape(size = 10.dp),
containerColor = colorResource(id = R.color.background_primary),
shadowElevation = 5.dp,
offset = DpOffset(
x = 20.dp,
y = (-300).dp
)
) {
if (!item.isDefault) {
DropdownMenuItem(
modifier = Modifier.height(44.dp),
onClick = {
onTypeEvent(TypeEvent.OnTemplateMenuClick.SetAsDefault(item))
isMenuExpanded = false
}
) {
Text(
text = stringResource(R.string.object_type_templates_menu_set_default),
style = BodyRegular,
color = colorResource(id = R.color.text_primary),
modifier = Modifier
)
}
Divider(
height = 0.5.dp,
paddingStart = 0.dp,
paddingEnd = 0.dp,
color = colorResource(R.color.shape_primary)
)
}
DropdownMenuItem(
modifier = Modifier.height(44.dp),
onClick = {
onTypeEvent(TypeEvent.OnTemplateMenuClick.Edit(item))
isMenuExpanded = false
}
) {
Text(
text = stringResource(R.string.object_type_templates_menu_edit),
style = BodyRegular,
color = colorResource(id = R.color.text_primary),
modifier = Modifier
)
}
Divider(
height = 0.5.dp,
paddingStart = 0.dp,
paddingEnd = 0.dp,
color = colorResource(R.color.shape_primary)
)
DropdownMenuItem(
modifier = Modifier.height(44.dp),
onClick = {
onTypeEvent(TypeEvent.OnTemplateMenuClick.Duplicate(item))
isMenuExpanded = false
}
) {
Text(
text = stringResource(R.string.object_type_templates_menu_duplicate),
style = BodyRegular,
color = colorResource(id = R.color.text_primary),
modifier = Modifier
)
}
Divider(
height = 0.5.dp,
paddingStart = 0.dp,
paddingEnd = 0.dp,
color = colorResource(R.color.shape_primary)
)
DropdownMenuItem(
modifier = Modifier.height(44.dp),
onClick = {
onTypeEvent(TypeEvent.OnTemplateMenuClick.Delete(item))
isMenuExpanded = false
}
) {
Text(
text = stringResource(R.string.object_type_templates_menu_delete),
style = BodyRegular,
color = colorResource(id = R.color.palette_system_red),
modifier = Modifier
)
}
}
}
}
)
}
}

View file

@ -0,0 +1,121 @@
package com.anytypeio.anytype.feature_object_type.ui.templates
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable
import com.anytypeio.anytype.core_ui.views.BodyBold
import com.anytypeio.anytype.core_ui.views.PreviewTitle1Regular
import com.anytypeio.anytype.feature_object_type.R
import com.anytypeio.anytype.feature_object_type.ui.TypeEvent
import com.anytypeio.anytype.feature_object_type.ui.TypeEvent.OnTemplatesAddIconClick
import com.anytypeio.anytype.feature_object_type.ui.UiTemplatesAddIconState
import com.anytypeio.anytype.feature_object_type.ui.UiTemplatesHeaderState
import com.anytypeio.anytype.feature_object_type.ui.UiTemplatesListState
@Composable
fun LazyItemScope.TemplatesScreen(
uiTemplatesHeaderState: UiTemplatesHeaderState.Visible,
uiTemplatesAddIconState: UiTemplatesAddIconState,
uiTemplatesListState: UiTemplatesListState,
onTypeEvent: (TypeEvent) -> Unit
) {
Spacer(
modifier = Modifier.height(44.dp)
)
TemplatesHeader(
modifier = Modifier
.fillMaxWidth()
.height(48.dp),
uiTemplatesHeaderState = uiTemplatesHeaderState,
uiTemplatesAddIconState = uiTemplatesAddIconState,
onTypeEvent = onTypeEvent
)
Spacer(
modifier = Modifier.height(12.dp)
)
TemplatesList(
uiTemplatesListState = uiTemplatesListState,
onTypeEvent = onTypeEvent
)
Spacer(
modifier = Modifier.height(32.dp)
)
}
@Composable
fun TemplatesHeader(
modifier: Modifier,
uiTemplatesHeaderState: UiTemplatesHeaderState.Visible,
uiTemplatesAddIconState: UiTemplatesAddIconState,
onTypeEvent: (TypeEvent) -> Unit
) {
Box(
modifier = modifier.padding(horizontal = 20.dp),
) {
Row(modifier = Modifier.wrapContentWidth().align(Alignment.CenterStart)) {
Text(
modifier = Modifier
.wrapContentSize()
.align(Alignment.CenterVertically),
text = stringResource(R.string.templates),
style = BodyBold,
color = colorResource(R.color.text_primary)
)
Text(
modifier = Modifier
.wrapContentSize()
.padding(start = 8.dp),
text = uiTemplatesHeaderState.count,
style = PreviewTitle1Regular,
color = colorResource(R.color.text_secondary)
)
}
if (uiTemplatesAddIconState is UiTemplatesAddIconState.Visible) {
Image(
modifier = Modifier
.size(48.dp)
.align(Alignment.CenterEnd)
.noRippleThrottledClickable {
onTypeEvent(OnTemplatesAddIconClick)
},
painter = painterResource(R.drawable.ic_default_plus),
contentDescription = "Add",
contentScale = ContentScale.Inside
)
}
}
}
@DefaultPreviews
@Composable
fun TemplatesHeaderPreview() {
TemplatesHeader(
modifier = Modifier
.fillMaxWidth()
.height(48.dp),
uiTemplatesHeaderState = UiTemplatesHeaderState.Visible(
count = "2"
),
uiTemplatesAddIconState = UiTemplatesAddIconState.Visible,
) { }
}

View file

@ -0,0 +1,126 @@
package com.anytypeio.anytype.feature_object_type.viewmodel
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.DVSortType
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.ObjectTypeUniqueKeys
import com.anytypeio.anytype.core_models.RelationFormat
import com.anytypeio.anytype.core_models.Relations
fun filtersForSearch(
objectTypeId: Id
): List<DVFilter> {
val filters = buildList {
addAll(buildDeletedFilter())
add(buildTemplateFilter())
add(buildTypeIdFilter(listOf(objectTypeId)))
}
return filters
}
fun filtersForSetsSearch(
objectTypeId: Id
): List<DVFilter> {
val filters = buildList {
addAll(buildDeletedFilter())
add(
DVFilter(
relation = Relations.LAYOUT,
condition = DVFilterCondition.IN,
value = listOf(ObjectType.Layout.SET.code.toDouble())
)
)
add(
DVFilter(
relation = Relations.SET_OF,
condition = DVFilterCondition.EQUAL,
value = objectTypeId
)
)
}
return filters
}
fun filtersForTemplatesSearch(
objectTypeId: Id
): List<DVFilter> {
val filters = buildList {
addAll(buildDeletedFilter())
add(
DVFilter(
relation = Relations.TYPE_UNIQUE_KEY,
condition = DVFilterCondition.EQUAL,
value = ObjectTypeUniqueKeys.TEMPLATE
)
)
add(
DVFilter(
relation = Relations.TARGET_OBJECT_TYPE,
condition = DVFilterCondition.EQUAL,
value = objectTypeId
)
)
}
return filters
}
fun sortForSetSearch() = DVSort(
relationKey = Relations.CREATED_DATE,
type = DVSortType.DESC,
includeTime = true,
relationFormat = RelationFormat.DATE
)
fun sortForTemplatesSearch() = DVSort(
relationKey = Relations.LAST_MODIFIED_DATE,
type = DVSortType.DESC,
includeTime = true,
relationFormat = RelationFormat.DATE
)
private fun buildTypeIdFilter(ids: List<Id>): DVFilter = DVFilter(
relation = Relations.TYPE,
condition = DVFilterCondition.IN,
value = ids
)
private fun buildDeletedFilter(): List<DVFilter> {
return listOf(
DVFilter(
relation = Relations.IS_ARCHIVED,
condition = DVFilterCondition.NOT_EQUAL,
value = true
),
DVFilter(
relation = Relations.IS_HIDDEN,
condition = DVFilterCondition.NOT_EQUAL,
value = true
),
DVFilter(
relation = Relations.IS_DELETED,
condition = DVFilterCondition.NOT_EQUAL,
value = true
),
DVFilter(
relation = Relations.IS_HIDDEN_DISCOVERY,
condition = DVFilterCondition.NOT_EQUAL,
value = true
)
)
}
private fun buildSpaceIdFilter(spaces: List<Id>): DVFilter = DVFilter(
relation = Relations.SPACE_ID,
condition = DVFilterCondition.IN,
value = spaces
)
private fun buildTemplateFilter(): DVFilter = DVFilter(
relation = Relations.TYPE_UNIQUE_KEY,
condition = DVFilterCondition.NOT_EQUAL,
value = ObjectTypeUniqueKeys.TEMPLATE
)

View file

@ -0,0 +1,77 @@
package com.anytypeio.anytype.feature_object_type.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.domain.block.interactor.sets.CreateObjectSet
import com.anytypeio.anytype.domain.event.interactor.SpaceSyncAndP2PStatusProvider
import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider
import com.anytypeio.anytype.domain.`object`.DuplicateObjects
import com.anytypeio.anytype.domain.`object`.SetObjectDetails
import com.anytypeio.anytype.domain.objects.DeleteObjects
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
import com.anytypeio.anytype.domain.objects.StoreOfRelations
import com.anytypeio.anytype.domain.page.CreateObject
import com.anytypeio.anytype.domain.primitives.FieldParser
import com.anytypeio.anytype.domain.primitives.GetObjectTypeConflictingFields
import com.anytypeio.anytype.domain.primitives.SetObjectTypeHeaderRecommendedFields
import com.anytypeio.anytype.domain.primitives.SetObjectTypeRecommendedFields
import com.anytypeio.anytype.domain.resources.StringResourceProvider
import com.anytypeio.anytype.domain.templates.CreateTemplate
import com.anytypeio.anytype.feature_object_type.ui.ObjectTypeVmParams
import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate
import com.anytypeio.anytype.presentation.editor.cover.CoverImageHashProvider
import javax.inject.Inject
class ObjectTypeVMFactory @Inject constructor(
private val vmParams: ObjectTypeVmParams,
private val analytics: Analytics,
private val urlBuilder: UrlBuilder,
private val analyticSpaceHelperDelegate: AnalyticSpaceHelperDelegate,
private val userPermissionProvider: UserPermissionProvider,
private val storeOfRelations: StoreOfRelations,
private val storeOfObjectTypes: StoreOfObjectTypes,
private val storelessSubscriptionContainer: StorelessSubscriptionContainer,
private val spaceSyncAndP2PStatusProvider: SpaceSyncAndP2PStatusProvider,
private val createObject: CreateObject,
private val fieldParser: FieldParser,
private val coverImageHashProvider: CoverImageHashProvider,
private val deleteObjects: DeleteObjects,
private val setObjectDetails: SetObjectDetails,
private val createObjectSet: CreateObjectSet,
private val stringResourceProvider: StringResourceProvider,
private val createTemplate: CreateTemplate,
private val duplicateObjects: DuplicateObjects,
private val getObjectTypeConflictingFields: GetObjectTypeConflictingFields,
private val objectTypeSetRecommendedFields: SetObjectTypeRecommendedFields,
private val objectTypeSetHeaderRecommendedFields: SetObjectTypeHeaderRecommendedFields
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T =
ObjectTypeViewModel(
vmParams = vmParams,
analytics = analytics,
urlBuilder = urlBuilder,
analyticSpaceHelperDelegate = analyticSpaceHelperDelegate,
userPermissionProvider = userPermissionProvider,
storeOfRelations = storeOfRelations,
storeOfObjectTypes = storeOfObjectTypes,
storelessSubscriptionContainer = storelessSubscriptionContainer,
spaceSyncAndP2PStatusProvider = spaceSyncAndP2PStatusProvider,
createObject = createObject,
fieldParser = fieldParser,
coverImageHashProvider = coverImageHashProvider,
deleteObjects = deleteObjects,
setObjectDetails = setObjectDetails,
createObjectSet = createObjectSet,
stringResourceProvider = stringResourceProvider,
createTemplate = createTemplate,
duplicateObjects = duplicateObjects,
getObjectTypeConflictingFields = getObjectTypeConflictingFields,
objectTypeSetRecommendedFields = objectTypeSetRecommendedFields,
objectTypeSetHeaderRecommendedFields = objectTypeSetHeaderRecommendedFields
) as T
}

View file

@ -0,0 +1,241 @@
package com.anytypeio.anytype.feature_object_type
import android.util.Log
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.RelationFormat
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.core_models.StubObjectType
import com.anytypeio.anytype.core_models.StubRelationObject
import com.anytypeio.anytype.domain.config.Gateway
import com.anytypeio.anytype.domain.debugging.Logger
import com.anytypeio.anytype.domain.misc.DateProvider
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.objects.DefaultStoreOfObjectTypes
import com.anytypeio.anytype.domain.objects.DefaultStoreOfRelations
import com.anytypeio.anytype.domain.objects.GetDateObjectByTimestamp
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
import com.anytypeio.anytype.domain.objects.StoreOfRelations
import com.anytypeio.anytype.domain.primitives.FieldParser
import com.anytypeio.anytype.domain.primitives.FieldParserImpl
import com.anytypeio.anytype.domain.resources.StringResourceProvider
import com.anytypeio.anytype.test_utils.MockDataFactory
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlinx.coroutines.test.runTest
import net.lachlanmckee.timberjunit.TimberTestRule
import org.junit.Before
import org.junit.Rule
import org.mockito.Mock
import org.mockito.MockitoAnnotations
class TestFieldsMappping {
@get:Rule
val timberTestRule: TimberTestRule = TimberTestRule.builder()
.minPriority(Log.DEBUG)
.showThread(true)
.showTimestamp(false)
.onlyLogWhenTestFails(true)
.build()
val space = MockDataFactory.randomUuid()
val field1 = StubRelationObject(
id = "field1_id",
key = "field1_key",
name = "Field 1, text",
format = RelationFormat.LONG_TEXT,
spaceId = space
)
val field2 = StubRelationObject(
id = "field2_id",
key = "field2_key",
name = "Field 2, number",
format = RelationFormat.NUMBER,
spaceId = space
)
val field3 = StubRelationObject(
id = "field3_id",
key = "field3_key",
name = "Field 3, date",
format = RelationFormat.DATE,
spaceId = space
)
val field4 = StubRelationObject(
id = "field4_id",
key = "field4_key",
name = "Field 4, checkbox",
format = RelationFormat.CHECKBOX,
spaceId = space
)
val field5 = StubRelationObject(
id = "field5_id",
key = "field5_key",
name = "Field 5, Status",
format = RelationFormat.STATUS,
spaceId = space
)
val fieldCreatedDate = StubRelationObject(
id = "bafyreihas6lc5knc67lbeohaxjgfjzi3oazs2yvh7gbotcktefjynjqndq",
key = Relations.CREATED_DATE,
name = "Field Creation date",
format = RelationFormat.DATE,
isHidden = false,
isReadOnly = true,
isReadOnlyValue = true,
spaceId = space,
sourceObject = "_brcreatedDate"
)
val fieldAssigneeObjType1 = StubObjectType()
val fieldAssigneeObjType2 = StubObjectType()
val fieldAssignee = StubRelationObject(
id = "bafyreibrqycr2w5q2db76f5l6hxfljwrgkrpqbulks6ppxsfu4hq5lwmue",
key = "assignee",
name = "Field Assignee",
format = RelationFormat.OBJECT,
objectTypes = listOf(
fieldAssigneeObjType1.id,
fieldAssigneeObjType2.id
),
isHidden = false,
isReadOnly = false,
isReadOnlyValue = false,
spaceId = space,
sourceObject = "_brassignee"
)
val allSpaceRelations =
listOf(field1, field2, field3, field4, field5, fieldCreatedDate, fieldAssignee)
val featuredFields = listOf(field1, field2, field5).map { it.id }
val sidebarFields = listOf(field3, field4, field5).map { it.id }
val hiddenFields = listOf(field5).map { it.id }
val testObjectType = StubObjectType(
id = "test_object_type_id",
uniqueKey = "test_object_type_unique_key",
name = "Test custom object type",
recommendedRelations = sidebarFields,
recommendedFeaturedRelations = featuredFields,
recommendedHiddenRelations = hiddenFields,
layout = ObjectType.Layout.OBJECT_TYPE.code.toDouble(),
recommendedLayout = ObjectType.Layout.TODO.code.toDouble(),
space = space
)
@Mock
lateinit var stringResourceProvider: StringResourceProvider
@Mock
lateinit var dateProvider: DateProvider
@Mock
lateinit var logger: Logger
@Mock
lateinit var getDateObjectByTimestamp: GetDateObjectByTimestamp
@Mock
lateinit var gateway: Gateway
lateinit var storeOfRelations: StoreOfRelations
lateinit var storeOfObjectTypes: StoreOfObjectTypes
lateinit var fieldParser: FieldParser
private lateinit var urlBuilder: UrlBuilder
@Before
fun setup() {
MockitoAnnotations.openMocks(this)
storeOfRelations = DefaultStoreOfRelations()
storeOfObjectTypes = DefaultStoreOfObjectTypes()
urlBuilder = UrlBuilder(gateway)
fieldParser =
FieldParserImpl(
dateProvider = dateProvider,
logger = logger,
getDateObjectByTimestamp = getDateObjectByTimestamp,
stringResourceProvider = stringResourceProvider
)
}
@Test
fun `should not filter featured fields by hidden`() = runTest {
storeOfRelations.apply {
merge(allSpaceRelations)
}
storeOfObjectTypes.apply {
merge(listOf(testObjectType, fieldAssigneeObjType2, fieldAssigneeObjType1))
}
val parsedFields = fieldParser.getObjectTypeParsedFields(
objectType = testObjectType,
storeOfRelations = storeOfRelations,
objectTypeConflictingFieldsIds = listOf()
)
assertEquals(
expected = listOf(field1, field2, field5),
actual = parsedFields.header
)
}
@Test
fun `should not filter sidebar fields by hidden`() = runTest {
storeOfRelations.apply {
merge(allSpaceRelations)
}
storeOfObjectTypes.apply {
merge(listOf(testObjectType, fieldAssigneeObjType2, fieldAssigneeObjType1))
}
val parsedFields = fieldParser.getObjectTypeParsedFields(
objectType = testObjectType,
storeOfRelations = storeOfRelations,
objectTypeConflictingFieldsIds = listOf()
)
assertEquals(
expected = listOf(field3, field4, field5),
actual = parsedFields.sidebar
)
}
@Test
fun `should map hidden fields`() = runTest {
storeOfRelations.apply {
merge(allSpaceRelations)
}
storeOfObjectTypes.apply {
merge(listOf(testObjectType, fieldAssigneeObjType2, fieldAssigneeObjType1))
}
val parsedFields = fieldParser.getObjectTypeParsedFields(
objectType = testObjectType,
storeOfRelations = storeOfRelations,
objectTypeConflictingFieldsIds = listOf()
)
assertEquals(
expected = listOf(field5),
actual = parsedFields.hidden
)
}
}