mirror of
https://github.com/anyproto/anytype-kotlin.git
synced 2025-06-08 05:47:05 +09:00
DROID-3429 Primitives | Edit type properties, part 1 (#2138)
This commit is contained in:
parent
b566ba30e0
commit
3932c2fe87
42 changed files with 750 additions and 1142 deletions
|
@ -28,6 +28,7 @@ dependencies {
|
|||
implementation project(':localization')
|
||||
implementation project(':presentation')
|
||||
implementation project(':library-emojifier')
|
||||
implementation project(':feature-properties')
|
||||
|
||||
compileOnly libs.javaxInject
|
||||
|
||||
|
|
|
@ -1,35 +1,13 @@
|
|||
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 object OnEditPropertyScreenDismiss : 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()
|
||||
}
|
||||
|
||||
|
@ -38,7 +16,6 @@ sealed class FieldEvent {
|
|||
}
|
||||
|
||||
sealed class Section : FieldEvent() {
|
||||
data object OnAddToHeaderIconClick : Section()
|
||||
data object OnAddToSidebarIconClick : Section()
|
||||
data object OnLocalInfoClick : Section()
|
||||
}
|
||||
|
@ -47,6 +24,4 @@ sealed class FieldEvent {
|
|||
data class OnMove(val fromKey: String, val toKey: String) : DragEvent()
|
||||
data object OnDragEnd : DragEvent()
|
||||
}
|
||||
|
||||
data class OnAddFieldSearchQueryChanged(val query: String) : FieldEvent()
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ 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.feature_properties.edit.UiPropertyLimitTypeItem
|
||||
import com.anytypeio.anytype.presentation.objects.ObjectIcon
|
||||
|
||||
//region Top bar
|
||||
|
@ -34,6 +35,7 @@ data class UiFieldsListState(val items: List<UiFieldsListItem>) {
|
|||
}
|
||||
}
|
||||
|
||||
//todo rename to UiPropertiesListItem
|
||||
sealed class UiFieldsListItem {
|
||||
abstract val id: Id
|
||||
|
||||
|
@ -41,7 +43,7 @@ sealed class UiFieldsListItem {
|
|||
abstract val fieldKey: Key
|
||||
abstract val fieldTitle: String
|
||||
abstract val format: RelationFormat
|
||||
abstract val limitObjectTypes: List<UiFieldObjectItem>
|
||||
abstract val limitObjectTypes: List<UiPropertyLimitTypeItem>
|
||||
abstract val canDelete: Boolean
|
||||
abstract val isEditableField: Boolean
|
||||
|
||||
|
@ -50,7 +52,7 @@ sealed class UiFieldsListItem {
|
|||
override val fieldKey: Key,
|
||||
override val fieldTitle: String,
|
||||
override val format: RelationFormat,
|
||||
override val limitObjectTypes: List<UiFieldObjectItem> = emptyList(),
|
||||
override val limitObjectTypes: List<UiPropertyLimitTypeItem> = emptyList(),
|
||||
override val canDelete: Boolean,
|
||||
override val isEditableField: Boolean
|
||||
) : Item()
|
||||
|
@ -60,7 +62,7 @@ sealed class UiFieldsListItem {
|
|||
override val fieldKey: Key,
|
||||
override val fieldTitle: String,
|
||||
override val format: RelationFormat,
|
||||
override val limitObjectTypes: List<UiFieldObjectItem> = emptyList(),
|
||||
override val limitObjectTypes: List<UiPropertyLimitTypeItem> = emptyList(),
|
||||
override val canDelete: Boolean = false,
|
||||
override val isEditableField: Boolean
|
||||
) : Item()
|
||||
|
@ -186,18 +188,4 @@ sealed class 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
|
||||
|
||||
|
||||
|
|
|
@ -1,170 +0,0 @@
|
|||
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 = {}
|
||||
)
|
||||
}
|
|
@ -1,386 +0,0 @@
|
|||
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 = {}
|
||||
)
|
||||
}
|
|
@ -35,24 +35,19 @@ 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.bottomBorder
|
||||
import com.anytypeio.anytype.core_ui.common.rememberReorderHapticFeedback
|
||||
import com.anytypeio.anytype.core_ui.extensions.simpleIcon
|
||||
import com.anytypeio.anytype.core_ui.foundation.Dragger
|
||||
|
@ -68,14 +63,14 @@ 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.feature_properties.edit.UiEditPropertyState
|
||||
import com.anytypeio.anytype.feature_properties.edit.ui.PropertyScreen
|
||||
import com.anytypeio.anytype.presentation.objects.ObjectIcon
|
||||
import kotlinx.coroutines.delay
|
||||
import sh.calvin.reorderable.ReorderableItem
|
||||
|
@ -88,9 +83,8 @@ fun FieldsMainScreen(
|
|||
uiFieldsListState: UiFieldsListState,
|
||||
uiTitleState: UiTitleState,
|
||||
uiIconState: UiIconState,
|
||||
uiFieldEditOrNewState: UiFieldEditOrNewState,
|
||||
uiFieldLocalInfoState: UiLocalsFieldsInfoState,
|
||||
uiAddFieldsScreenState: UiAddFieldsScreenState,
|
||||
uiEditPropertyState: UiEditPropertyState,
|
||||
fieldEvent: (FieldEvent) -> Unit
|
||||
) {
|
||||
|
||||
|
@ -198,10 +192,7 @@ fun FieldsMainScreen(
|
|||
item = item,
|
||||
reorderingState = reorderableLazyColumnState,
|
||||
fieldEvent = fieldEvent,
|
||||
isReorderable = false,
|
||||
onAddIconClick = {
|
||||
fieldEvent(FieldEvent.Section.OnAddToHeaderIconClick)
|
||||
}
|
||||
isReorderable = false
|
||||
)
|
||||
}
|
||||
is Section.Local,
|
||||
|
@ -226,11 +217,16 @@ fun FieldsMainScreen(
|
|||
}
|
||||
)
|
||||
|
||||
if (uiFieldEditOrNewState is UiFieldEditOrNewState.Visible) {
|
||||
EditFieldScreen(
|
||||
if (uiEditPropertyState is UiEditPropertyState.Visible) {
|
||||
PropertyScreen(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
uiFieldEditOrNewState = uiFieldEditOrNewState,
|
||||
fieldEvent = fieldEvent
|
||||
uiState = uiEditPropertyState,
|
||||
onDismissRequest = { fieldEvent(OnEditPropertyScreenDismiss) },
|
||||
onFormatClick = {},
|
||||
onLimitTypesClick = {},
|
||||
onSaveButtonClicked = {},
|
||||
onCreateNewButtonClicked = {},
|
||||
onPropertyNameUpdate = { }
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -241,13 +237,6 @@ fun FieldsMainScreen(
|
|||
fieldEvent = fieldEvent
|
||||
)
|
||||
}
|
||||
|
||||
if (uiAddFieldsScreenState is UiAddFieldsScreenState.Visible) {
|
||||
AddFieldScreen(
|
||||
state = uiAddFieldsScreenState,
|
||||
fieldEvent = fieldEvent
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns a content type string based on the item type. **/
|
||||
|
@ -599,29 +588,6 @@ private fun LazyItemScope.FieldItemDraggable(
|
|||
}
|
||||
}
|
||||
|
||||
@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,
|
||||
|
@ -757,9 +723,8 @@ fun PreviewTypeFieldsMainScreen() {
|
|||
)
|
||||
),
|
||||
fieldEvent = {},
|
||||
uiFieldEditOrNewState = UiFieldEditOrNewState.Hidden,
|
||||
uiFieldLocalInfoState = UiLocalsFieldsInfoState.Hidden,
|
||||
uiAddFieldsScreenState = UiAddFieldsScreenState.Hidden
|
||||
uiEditPropertyState = UiEditPropertyState.Hidden,
|
||||
uiFieldLocalInfoState = UiLocalsFieldsInfoState.Hidden
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
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
|
||||
)
|
||||
}
|
|
@ -33,8 +33,7 @@ sealed class ObjectTypeCommand {
|
|||
|
||||
data object OpenFieldsScreen : ObjectTypeCommand()
|
||||
|
||||
data class OpenAddFieldScreen(val typeId: Id, val space: Id, val isSet: Boolean = false) :
|
||||
ObjectTypeCommand()
|
||||
data class OpenEditTypePropertiesScreen(val typeId: Id, val space: Id) : ObjectTypeCommand()
|
||||
}
|
||||
|
||||
//region OBJECT TYPE HEADER (title + icon)
|
||||
|
|
|
@ -13,11 +13,10 @@ 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.feature_properties.edit.UiPropertyLimitTypeItem
|
||||
import com.anytypeio.anytype.presentation.editor.cover.CoverImageHashProvider
|
||||
import com.anytypeio.anytype.presentation.mapper.objectIcon
|
||||
import com.anytypeio.anytype.presentation.relations.BasicObjectCoverWrapper
|
||||
|
@ -56,7 +55,7 @@ fun ObjectWrapper.Basic.toTemplateView(
|
|||
* 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 =
|
||||
fun ObjectWrapper.Relation.getName(stringResourceProvider: StringResourceProvider): String =
|
||||
if (name.isNullOrBlank()) {
|
||||
stringResourceProvider.getUntitledObjectTitle()
|
||||
} else {
|
||||
|
@ -140,7 +139,7 @@ suspend fun buildUiFieldsList(
|
|||
}
|
||||
|
||||
return buildList {
|
||||
add(Section.Header(canAdd = true))
|
||||
add(Section.Header(canAdd = false))
|
||||
addAll(headerItems)
|
||||
|
||||
add(Section.SideBar(canAdd = true))
|
||||
|
@ -173,11 +172,11 @@ private suspend fun mapLimitObjectTypes(
|
|||
storeOfObjectTypes: StoreOfObjectTypes,
|
||||
fieldParser: FieldParser,
|
||||
urlBuilder: UrlBuilder
|
||||
): List<UiFieldObjectItem> {
|
||||
): List<UiPropertyLimitTypeItem> {
|
||||
return if (relation.format == RelationFormat.OBJECT && relation.relationFormatObjectTypes.isNotEmpty()) {
|
||||
relation.relationFormatObjectTypes.mapNotNull { key ->
|
||||
storeOfObjectTypes.getByKey(key)?.let { objType ->
|
||||
UiFieldObjectItem(
|
||||
UiPropertyLimitTypeItem(
|
||||
id = objType.id,
|
||||
key = objType.uniqueKey,
|
||||
title = fieldParser.getObjectName(objType),
|
||||
|
@ -239,16 +238,3 @@ private suspend fun mapToUiFieldsLocalListItem(
|
|||
)
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -4,14 +4,13 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import com.anytypeio.anytype.analytics.base.Analytics
|
||||
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.ObjectWrapper
|
||||
import com.anytypeio.anytype.core_models.Relations
|
||||
import com.anytypeio.anytype.core_models.permissions.ObjectPermissions
|
||||
import com.anytypeio.anytype.core_models.permissions.toObjectPermissionsForTypes
|
||||
import com.anytypeio.anytype.core_ui.extensions.simpleIcon
|
||||
import com.anytypeio.anytype.domain.base.fold
|
||||
import com.anytypeio.anytype.domain.block.interactor.sets.CreateObjectSet
|
||||
import com.anytypeio.anytype.domain.event.interactor.SpaceSyncAndP2PStatusProvider
|
||||
import com.anytypeio.anytype.domain.library.StoreSearchParams
|
||||
import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer
|
||||
|
@ -22,17 +21,12 @@ 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.fields.FieldEvent
|
||||
import com.anytypeio.anytype.feature_object_type.fields.UiAddFieldItem
|
||||
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.UiFieldEditOrNewState.Visible.*
|
||||
import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListItem
|
||||
import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListState
|
||||
|
@ -50,19 +44,15 @@ import com.anytypeio.anytype.feature_object_type.ui.UiLayoutButtonState
|
|||
import com.anytypeio.anytype.feature_object_type.ui.UiLayoutTypeState
|
||||
import com.anytypeio.anytype.feature_object_type.ui.UiLayoutTypeState.*
|
||||
import com.anytypeio.anytype.feature_object_type.ui.UiSyncStatusBadgeState
|
||||
import com.anytypeio.anytype.feature_object_type.ui.UiTemplatesAddIconState
|
||||
import com.anytypeio.anytype.feature_object_type.ui.UiTemplatesButtonState
|
||||
import com.anytypeio.anytype.feature_object_type.ui.UiTemplatesModalListState
|
||||
import com.anytypeio.anytype.feature_object_type.ui.UiTitleState
|
||||
import com.anytypeio.anytype.feature_object_type.ui.buildUiFieldsList
|
||||
import com.anytypeio.anytype.feature_object_type.ui.mapToUiAddFieldListItem
|
||||
import com.anytypeio.anytype.feature_object_type.ui.toTemplateView
|
||||
import com.anytypeio.anytype.feature_properties.edit.UiEditPropertyState
|
||||
import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate
|
||||
import com.anytypeio.anytype.presentation.editor.cover.CoverImageHashProvider
|
||||
import com.anytypeio.anytype.presentation.editor.cover.UnsplashViewModel.Companion.DEBOUNCE_DURATION
|
||||
import com.anytypeio.anytype.presentation.extension.sendAnalyticsScreenObjectType
|
||||
import com.anytypeio.anytype.presentation.home.OpenObjectNavigation
|
||||
import com.anytypeio.anytype.presentation.home.navigation
|
||||
import com.anytypeio.anytype.presentation.mapper.objectIcon
|
||||
import com.anytypeio.anytype.presentation.search.ObjectSearchConstants.defaultKeys
|
||||
import com.anytypeio.anytype.presentation.sync.SyncStatusWidgetState
|
||||
|
@ -71,22 +61,14 @@ import com.anytypeio.anytype.presentation.sync.updateStatus
|
|||
import com.anytypeio.anytype.presentation.templates.TemplateView
|
||||
import kotlin.collections.map
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.take
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
|
@ -105,18 +87,15 @@ class ObjectTypeViewModel(
|
|||
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
|
||||
private val objectTypeSetRecommendedFields: SetObjectTypeRecommendedFields
|
||||
) : ViewModel(), AnalyticSpaceHelperDelegate by analyticSpaceHelperDelegate {
|
||||
|
||||
//region UI STATE
|
||||
|
@ -148,13 +127,11 @@ class ObjectTypeViewModel(
|
|||
val uiFieldLocalInfoState =
|
||||
MutableStateFlow<UiLocalsFieldsInfoState>(UiLocalsFieldsInfoState.Hidden)
|
||||
|
||||
//fields
|
||||
//properties list
|
||||
val uiFieldsListState = MutableStateFlow<UiFieldsListState>(UiFieldsListState.EMPTY)
|
||||
val uiFieldEditOrNewState =
|
||||
MutableStateFlow<UiFieldEditOrNewState>(UiFieldEditOrNewState.Hidden)
|
||||
|
||||
//add new field
|
||||
val uiAddFieldsState = MutableStateFlow<UiAddFieldsScreenState>(UiAddFieldsScreenState.Hidden)
|
||||
//edit property
|
||||
val uiEditPropertyScreen = MutableStateFlow<UiEditPropertyState>(UiEditPropertyState.Hidden)
|
||||
|
||||
//error
|
||||
val errorState = MutableStateFlow<UiErrorState>(UiErrorState.Hidden)
|
||||
|
@ -166,6 +143,8 @@ class ObjectTypeViewModel(
|
|||
private val _objectTypeConflictingFieldIds = MutableStateFlow<List<Id>>(emptyList())
|
||||
//endregion
|
||||
|
||||
val commands = MutableSharedFlow<ObjectTypeCommand>()
|
||||
|
||||
//region INIT AND LIFE CYCLE
|
||||
init {
|
||||
Timber.d("init, vmParams: $vmParams")
|
||||
|
@ -191,7 +170,6 @@ class ObjectTypeViewModel(
|
|||
//endregion
|
||||
|
||||
//region DATA
|
||||
|
||||
private fun proceedWithObservingObjectType() {
|
||||
viewModelScope.launch {
|
||||
combine(
|
||||
|
@ -387,6 +365,31 @@ class ObjectTypeViewModel(
|
|||
fun hideError() {
|
||||
errorState.value = UiErrorState.Hidden
|
||||
}
|
||||
|
||||
fun setupUiEditPropertyScreen(item: UiFieldsListItem.Item) {
|
||||
val permissions = _objectTypePermissionsState.value
|
||||
if (permissions?.participantCanEdit == true && item.isEditableField) {
|
||||
uiEditPropertyScreen.value = UiEditPropertyState.Visible.Edit(
|
||||
id = item.id,
|
||||
key = item.fieldKey,
|
||||
name = item.fieldTitle,
|
||||
formatName = stringResourceProvider.getPropertiesFormatPrettyString(item.format),
|
||||
formatIcon = item.format.simpleIcon(),
|
||||
format = item.format,
|
||||
limitObjectTypes = item.limitObjectTypes
|
||||
)
|
||||
} else {
|
||||
uiEditPropertyScreen.value = UiEditPropertyState.Visible.View(
|
||||
id = item.id,
|
||||
key = item.fieldKey,
|
||||
name = item.fieldTitle,
|
||||
formatName = stringResourceProvider.getPropertiesFormatPrettyString(item.format),
|
||||
formatIcon = item.format.simpleIcon(),
|
||||
format = item.format,
|
||||
limitObjectTypes = item.limitObjectTypes
|
||||
)
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Ui EVENTS - TYPES
|
||||
|
@ -623,33 +626,17 @@ class ObjectTypeViewModel(
|
|||
fun onFieldEvent(event: FieldEvent) {
|
||||
Timber.d("onFieldEvent: $event")
|
||||
when (event) {
|
||||
FieldEvent.OnChangeTypeClick -> TODO()
|
||||
FieldEvent.OnFieldEditScreenDismiss -> {
|
||||
uiFieldEditOrNewState.value = UiFieldEditOrNewState.Hidden
|
||||
FieldEvent.OnEditPropertyScreenDismiss -> {
|
||||
uiEditPropertyScreen.value = UiEditPropertyState.Hidden
|
||||
}
|
||||
|
||||
is FieldEvent.OnFieldItemClick -> {
|
||||
when (event.item) {
|
||||
is UiFieldsListItem.Item -> {
|
||||
val permissions = _objectTypePermissionsState.value
|
||||
if (permissions?.participantCanEdit == true && event.item.isEditableField) {
|
||||
uiFieldEditOrNewState.value = Edit(
|
||||
event.item
|
||||
)
|
||||
} else {
|
||||
uiFieldEditOrNewState.value = ViewOnly(
|
||||
event.item
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is UiFieldsListItem.Item -> setupUiEditPropertyScreen(item = event.item)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
FieldEvent.OnLimitTypesClick -> TODO()
|
||||
is FieldEvent.OnSaveButtonClicked -> TODO()
|
||||
|
||||
is FieldEvent.FieldItemMenu -> proceedWithFieldItemMenuClick(event)
|
||||
FieldEvent.FieldLocalInfo.OnDismiss -> {
|
||||
uiFieldLocalInfoState.value = UiLocalsFieldsInfoState.Hidden
|
||||
|
@ -659,12 +646,15 @@ class ObjectTypeViewModel(
|
|||
uiFieldLocalInfoState.value = UiLocalsFieldsInfoState.Visible
|
||||
}
|
||||
|
||||
FieldEvent.Section.OnAddToHeaderIconClick -> {
|
||||
proceedWithAddFieldToHeaderScreen()
|
||||
}
|
||||
|
||||
FieldEvent.Section.OnAddToSidebarIconClick -> {
|
||||
proceedWithAddFieldToSidebarScreen()
|
||||
viewModelScope.launch {
|
||||
commands.emit(
|
||||
ObjectTypeCommand.OpenEditTypePropertiesScreen(
|
||||
typeId = vmParams.objectId,
|
||||
space = vmParams.spaceId.id,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FieldEvent.DragEvent.OnDragEnd -> {
|
||||
|
@ -706,24 +696,6 @@ class ObjectTypeViewModel(
|
|||
currentList.add(toIndex, item)
|
||||
uiFieldsListState.value = UiFieldsListState(items = currentList)
|
||||
}
|
||||
|
||||
FieldEvent.OnAddFieldScreenDismiss -> {
|
||||
hideAddNewFieldScreen()
|
||||
}
|
||||
|
||||
is FieldEvent.OnAddToHeaderFieldClick -> {
|
||||
onAddToHeaderFieldClicked(item = event.item)
|
||||
hideAddNewFieldScreen()
|
||||
}
|
||||
|
||||
is FieldEvent.OnAddToSidebarFieldClick -> {
|
||||
onAddToSidebarFieldClicked(item = event.item)
|
||||
hideAddNewFieldScreen()
|
||||
}
|
||||
|
||||
is FieldEvent.OnAddFieldSearchQueryChanged -> {
|
||||
onQueryChanged(query = event.query)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -777,30 +749,6 @@ class ObjectTypeViewModel(
|
|||
proceedWithSetRecommendedFields(newRecommendedFields)
|
||||
}
|
||||
|
||||
is FieldEvent.FieldItemMenu.OnRemoveLocalClick -> TODO()
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region NAVIGATION
|
||||
val commands = MutableSharedFlow<ObjectTypeCommand>()
|
||||
val navigation = MutableSharedFlow<OpenObjectNavigation>()
|
||||
|
||||
private fun proceedWithNavigation(
|
||||
objectId: Id,
|
||||
objectLayout: ObjectType.Layout?
|
||||
) {
|
||||
Timber.d("proceedWithNavigation, objectId: $objectId, objectLayout: $objectLayout")
|
||||
val destination = objectLayout?.navigation(
|
||||
target = objectId,
|
||||
space = vmParams.spaceId.id
|
||||
)
|
||||
if (destination != null) {
|
||||
viewModelScope.launch {
|
||||
navigation.emit(destination)
|
||||
}
|
||||
} else {
|
||||
Timber.w("No navigation destination found for object $objectId with layout $objectLayout")
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
@ -960,124 +908,6 @@ class ObjectTypeViewModel(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun proceedWithSetHeaderRecommendedFields(fields: List<Id>) {
|
||||
val params = SetObjectTypeHeaderRecommendedFields.Params(
|
||||
objectTypeId = vmParams.objectId,
|
||||
fields = fields
|
||||
)
|
||||
viewModelScope.launch {
|
||||
objectTypeSetHeaderRecommendedFields.async(params).fold(
|
||||
onSuccess = {
|
||||
Timber.d("Header recommended fields set")
|
||||
},
|
||||
onFailure = {
|
||||
Timber.e(it, "Error while setting header recommended fields")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region ADD NEW FIELD
|
||||
private val input = MutableStateFlow("")
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
private val query = input.take(1).onCompletion {
|
||||
emitAll(
|
||||
input.drop(1).debounce(DEBOUNCE_DURATION).distinctUntilChanged()
|
||||
)
|
||||
}
|
||||
|
||||
private var addFieldSearchJob: Job? = null
|
||||
|
||||
/**
|
||||
* Loads the available fields from type, applies filtering based on a search query,
|
||||
* and then updates the UI state.
|
||||
*/
|
||||
private fun showAddFieldScreen(addToHeader: Boolean) {
|
||||
// Collect field keys that are already present in the type fields list.
|
||||
val typeFieldsKeys =
|
||||
uiFieldsListState.value.items.mapNotNull { (it as? UiFieldsListItem.Item)?.fieldKey }
|
||||
|
||||
addFieldSearchJob = viewModelScope.launch {
|
||||
// Combine the search query flow with the list of all fields.
|
||||
combine(
|
||||
query,
|
||||
storeOfRelations.trackChanges()
|
||||
) { queryText, _ ->
|
||||
// Filter out fields by query and that already exist and are not valid.
|
||||
filterFields(
|
||||
allFields = storeOfRelations.getAll(),
|
||||
typeKeys = typeFieldsKeys,
|
||||
queryText = queryText
|
||||
)
|
||||
}.collect { filteredFields ->
|
||||
val items = filteredFields.mapNotNull { field ->
|
||||
field.mapToUiAddFieldListItem(stringResourceProvider)
|
||||
}.sortedBy { it.fieldTitle }
|
||||
|
||||
uiAddFieldsState.value = UiAddFieldsScreenState.Visible(
|
||||
items = items,
|
||||
addToHeader = addToHeader
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun filterFields(
|
||||
allFields: List<ObjectWrapper.Relation>,
|
||||
typeKeys: List<Key>,
|
||||
queryText: String
|
||||
): List<ObjectWrapper.Relation> = allFields.filter { field ->
|
||||
field.key !in typeKeys &&
|
||||
field.isValidToUse &&
|
||||
(queryText.isBlank() || field.name?.contains(queryText, ignoreCase = true) == true)
|
||||
}
|
||||
|
||||
private fun onQueryChanged(query: String) {
|
||||
input.value = query
|
||||
}
|
||||
|
||||
fun hideAddNewFieldScreen() {
|
||||
input.value = ""
|
||||
addFieldSearchJob?.cancel()
|
||||
addFieldSearchJob = null
|
||||
uiAddFieldsState.value = UiAddFieldsScreenState.Hidden
|
||||
}
|
||||
|
||||
fun proceedWithAddFieldToHeaderScreen() {
|
||||
showAddFieldScreen(addToHeader = true)
|
||||
}
|
||||
|
||||
fun proceedWithAddFieldToSidebarScreen() {
|
||||
showAddFieldScreen(addToHeader = false)
|
||||
}
|
||||
|
||||
private fun updateFieldRecommendations(
|
||||
currentFields: List<String>?,
|
||||
item: UiAddFieldItem,
|
||||
updateAction: (List<String>) -> Unit
|
||||
) {
|
||||
val newFields = currentFields.orEmpty() + item.id
|
||||
updateAction(newFields)
|
||||
}
|
||||
|
||||
private fun onAddToSidebarFieldClicked(item: UiAddFieldItem) {
|
||||
updateFieldRecommendations(
|
||||
currentFields = _objTypeState.value?.recommendedRelations,
|
||||
item = item,
|
||||
updateAction = ::proceedWithSetRecommendedFields
|
||||
)
|
||||
}
|
||||
|
||||
private fun onAddToHeaderFieldClicked(item: UiAddFieldItem) {
|
||||
updateFieldRecommendations(
|
||||
currentFields = _objTypeState.value?.recommendedFeaturedRelations,
|
||||
item = item,
|
||||
updateAction = ::proceedWithSetHeaderRecommendedFields
|
||||
)
|
||||
}
|
||||
//endregion
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -3,7 +3,6 @@ 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
|
||||
|
@ -13,10 +12,8 @@ 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
|
||||
|
@ -35,18 +32,15 @@ class ObjectTypeVMFactory @Inject constructor(
|
|||
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")
|
||||
|
@ -61,17 +55,14 @@ class ObjectTypeVMFactory @Inject constructor(
|
|||
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
|
||||
}
|
|
@ -193,29 +193,6 @@ class TestFieldsMappping {
|
|||
)
|
||||
}
|
||||
|
||||
@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 {
|
||||
|
||||
|
@ -233,8 +210,54 @@ class TestFieldsMappping {
|
|||
objectTypeConflictingFieldsIds = listOf()
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
expected = listOf(),
|
||||
actual = parsedFields.hidden
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test filter duplicates`() = runTest {
|
||||
|
||||
storeOfRelations.apply {
|
||||
merge(allSpaceRelations)
|
||||
}
|
||||
|
||||
val testObjectType = StubObjectType(
|
||||
recommendedFeaturedRelations = listOf(field1, field1, field2, field3).map { it.id },
|
||||
recommendedRelations = listOf(field1, field4, field4).map { it.id },
|
||||
recommendedFileRelations = listOf(field1, field2, field3, field4, field5, field5).map { it.id },
|
||||
recommendedHiddenRelations = listOf(field1, field2, field3, field4, field5, fieldCreatedDate, fieldCreatedDate).map { it.id },
|
||||
space = space
|
||||
)
|
||||
|
||||
storeOfObjectTypes.apply {
|
||||
merge(listOf(testObjectType, fieldAssigneeObjType2, fieldAssigneeObjType1))
|
||||
}
|
||||
|
||||
val parsedFields = fieldParser.getObjectTypeParsedFields(
|
||||
objectType = testObjectType,
|
||||
storeOfRelations = storeOfRelations,
|
||||
objectTypeConflictingFieldsIds = listOf()
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
expected = listOf(field1, field2, field3),
|
||||
actual = parsedFields.header
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
expected = listOf(field4),
|
||||
actual = parsedFields.sidebar
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
expected = listOf(field5),
|
||||
actual = parsedFields.file
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
expected = listOf(fieldCreatedDate),
|
||||
actual = parsedFields.hidden
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue