Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
package com.woocommerce.android.ui.products.details

import com.woocommerce.android.R
import com.woocommerce.android.ui.products.details.ProductDetailViewModel.ProductDetailViewState.AuxiliaryState
import com.woocommerce.android.ui.products.details.ProductDetailViewModel.ProductDetailViewState.AuxiliaryState.Error
import com.woocommerce.android.ui.products.details.ProductDetailViewModel.ProductDetailViewState.AuxiliaryState.Loading
import com.woocommerce.android.ui.products.details.ProductDetailViewModel.ProductDetailViewState.AuxiliaryState.None
import com.woocommerce.android.ui.products.models.ProductProperty
import com.woocommerce.android.ui.products.models.ProductPropertyCard

class ProductDetailUiMapper {
fun mapScreenState(
auxiliaryState: AuxiliaryState,
hasProduct: Boolean,
cards: List<ProductDetailCardUiModel>,
showAddMore: Boolean,
showLinkedProductPromo: Boolean,
): ProductDetailScreenState = when (auxiliaryState) {
Loading -> ProductDetailScreenState.Loading
is Error -> if (auxiliaryState.message == R.string.product_detail_product_not_selected) {
ProductDetailScreenState.Empty(auxiliaryState.message)
} else {
ProductDetailScreenState.Error(auxiliaryState.message)
}
None -> if (hasProduct) {
ProductDetailScreenState.Content(
cards = cards,
showAddMore = showAddMore,
showLinkedProductPromo = showLinkedProductPromo,
)
} else {
ProductDetailScreenState.Empty()
}
}

fun map(cards: List<ProductPropertyCard>): List<ProductDetailCardUiModel> {
val cardKeyOccurrences = mutableMapOf<String, Int>()
return cards.map { card ->
val rows = mapRows(card.properties)
val cardBaseKey = when (card.type) {
ProductPropertyCard.Type.PRIMARY -> PRIMARY_CARD_KEY
ProductPropertyCard.Type.SECONDARY -> if (card.properties.any { it.isBlazeProperty() }) {
BLAZE_CARD_KEY
} else {
SECONDARY_CARD_KEY
}
}
val cardKey = cardBaseKey.withOccurrence(cardKeyOccurrences)

ProductDetailCardUiModel(
key = cardKey,
style = when (card.type) {
ProductPropertyCard.Type.PRIMARY -> ProductDetailCardStyle.PRIMARY
ProductPropertyCard.Type.SECONDARY -> ProductDetailCardStyle.SECONDARY
},
caption = card.caption,
rows = rows,
)
}
}

private fun mapRows(properties: List<ProductProperty>): List<ProductDetailRowUiModel> {
val keyOccurrences = mutableMapOf<String, Int>()
return properties.mapIndexed { index, property ->
val key = property.semanticKey().withOccurrence(keyOccurrences)
property.toUiModel(key).let { row ->
if (row is ProductDetailRowUiModel.Rating) {
row.copy(showDivider = index != properties.lastIndex)
} else {
row
}
}
}
}

private fun ProductProperty.toUiModel(key: String): ProductDetailRowUiModel = when (this) {
ProductProperty.Divider -> ProductDetailRowUiModel.Divider(key)
is ProductProperty.Property -> toPropertyUiModel(key)
is ProductProperty.ComplexProperty -> toComplexPropertyUiModel(key)
is ProductProperty.RatingBar -> toRatingUiModel(key)
is ProductProperty.Editable -> toEditableUiModel(key)
is ProductProperty.PropertyGroup -> toPropertyGroupUiModel(key)
is ProductProperty.Link -> toLinkUiModel(key)
is ProductProperty.Button -> toButtonUiModel(key)
is ProductProperty.Switch -> toSwitchUiModel(key)
is ProductProperty.Warning -> ProductDetailRowUiModel.Warning(key, content)
}

private fun ProductProperty.Property.toPropertyUiModel(key: String) =
ProductDetailRowUiModel.Property(
key = key,
title = title,
value = value,
showDivider = isDividerVisible,
)

private fun ProductProperty.ComplexProperty.toComplexPropertyUiModel(key: String) =
ProductDetailRowUiModel.ComplexProperty(
key = key,
title = title,
value = value,
icon = icon,
showTitle = showTitle,
maxLines = maxLines,
showDivider = isDividerVisible,
onClick = onClick,
)

private fun ProductProperty.RatingBar.toRatingUiModel(key: String) =
ProductDetailRowUiModel.Rating(
key = key,
title = title,
value = value,
rating = rating,
icon = icon,
showDivider = false,
onClick = onClick,
)

private fun ProductProperty.Editable.toEditableUiModel(key: String) =
ProductDetailRowUiModel.Editable(
key = key,
hint = hint,
text = text,
shouldFocus = shouldFocus,
isReadOnly = isReadOnly,
badgeText = badgeText,
badgeTone = badgeColor?.let {
if (it == R.color.product_status_badge_pending) {
ProductDetailBadgeTone.WARNING
} else {
ProductDetailBadgeTone.NEUTRAL
}
},
onTextChanged = onTextChanged,
)

private fun ProductProperty.PropertyGroup.toPropertyGroupUiModel(key: String) =
ProductDetailRowUiModel.PropertyGroup(
key = key,
title = title,
properties = properties.entries.map { ProductDetailPropertyValueUiModel(it.key, it.value) },
icon = icon,
showTitle = showTitle,
showDivider = isDividerVisible,
isHighlighted = isHighlighted,
propertyFormat = propertyFormat,
onClick = onClick,
)

private fun ProductProperty.Link.toLinkUiModel(key: String) =
ProductDetailRowUiModel.Link(
key = key,
title = title,
icon = icon,
showDivider = isDividerVisible,
onClick = onClick,
)

private fun ProductProperty.Button.toButtonUiModel(key: String) =
ProductDetailRowUiModel.Button(
key = key,
text = text,
icon = icon,
showDivider = isDividerVisible,
tooltip = tooltip?.let {
ProductDetailTooltipUiModel(
title = it.title,
text = it.text,
dismissButtonText = it.dismissButtonText,
onDismiss = it.onDismiss,
)
},
link = link?.let { ProductDetailButtonLinkUiModel(it.text, it.onClick) },
onClick = onClick,
)

private fun ProductProperty.Switch.toSwitchUiModel(key: String) =
ProductDetailRowUiModel.Switch(
key = key,
title = title,
isOn = isOn,
icon = icon,
onStateChanged = onStateChanged,
)

private fun ProductProperty.semanticKey(): String = when (this) {
ProductProperty.Divider -> "divider"
is ProductProperty.Property -> "property_$title"
is ProductProperty.ComplexProperty -> "complex_${title ?: NO_RESOURCE}_${icon ?: NO_RESOURCE}"
is ProductProperty.RatingBar -> "rating_$title"
is ProductProperty.Editable -> "editable_$hint"
is ProductProperty.PropertyGroup -> "group_$title"
is ProductProperty.Link -> "link_$title"
is ProductProperty.Button -> "button_$text"
is ProductProperty.Switch -> "switch_$title"
is ProductProperty.Warning -> "warning"
}

private fun ProductProperty.isBlazeProperty() =
this is ProductProperty.Link && title == R.string.product_details_blaze_card

private fun String.withOccurrence(occurrences: MutableMap<String, Int>): String {
val occurrence = occurrences.getOrDefault(this, 0)
occurrences[this] = occurrence + 1
return if (occurrence == 0) this else "${this}_$occurrence"
}

private companion object {
const val PRIMARY_CARD_KEY = "primary"
const val SECONDARY_CARD_KEY = "secondary"
const val BLAZE_CARD_KEY = "blaze"
const val NO_RESOURCE = 0
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package com.woocommerce.android.ui.products.details

import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.runtime.Immutable

@Immutable
sealed interface ProductDetailScreenState {
data object Loading : ProductDetailScreenState

data class Empty(@StringRes val message: Int? = null) : ProductDetailScreenState

data class Error(@StringRes val message: Int) : ProductDetailScreenState

data class Content(
val cards: List<ProductDetailCardUiModel>,
val showAddMore: Boolean,
val showLinkedProductPromo: Boolean,
) : ProductDetailScreenState
}

@Immutable
sealed interface ProductDetailImageUiState {
data object Loading : ProductDetailImageUiState
data object Gallery : ProductDetailImageUiState
data object AddImage : ProductDetailImageUiState
data object Unavailable : ProductDetailImageUiState
data object Hidden : ProductDetailImageUiState
}

@Immutable
data class ProductDetailCardUiModel(
val key: String,
val style: ProductDetailCardStyle,
val caption: String,
val rows: List<ProductDetailRowUiModel>,
)

enum class ProductDetailCardStyle {
PRIMARY,
SECONDARY,
}

enum class ProductDetailBadgeTone {
NEUTRAL,
WARNING,
}

@Immutable
sealed interface ProductDetailRowUiModel {
val key: String

data class Divider(
override val key: String,
) : ProductDetailRowUiModel

data class Property(
override val key: String,
@StringRes val title: Int,
val value: String,
val showDivider: Boolean,
) : ProductDetailRowUiModel

data class ComplexProperty(
override val key: String,
@StringRes val title: Int?,
val value: String,
@DrawableRes val icon: Int?,
val showTitle: Boolean,
val maxLines: Int,
val showDivider: Boolean,
val onClick: (() -> Unit)?,
) : ProductDetailRowUiModel

data class Rating(
override val key: String,
@StringRes val title: Int,
val value: String,
val rating: Float,
@DrawableRes val icon: Int,
val showDivider: Boolean,
val onClick: (() -> Unit)?,
) : ProductDetailRowUiModel

data class Editable(
override val key: String,
@StringRes val hint: Int,
val text: String,
val shouldFocus: Boolean,
val isReadOnly: Boolean,
@StringRes val badgeText: Int?,
val badgeTone: ProductDetailBadgeTone?,
val onTextChanged: ((String) -> Unit)?,
) : ProductDetailRowUiModel

data class PropertyGroup(
override val key: String,
@StringRes val title: Int,
val properties: List<ProductDetailPropertyValueUiModel>,
@DrawableRes val icon: Int?,
val showTitle: Boolean,
val showDivider: Boolean,
val isHighlighted: Boolean,
@StringRes val propertyFormat: Int,
val onClick: (() -> Unit)?,
) : ProductDetailRowUiModel

data class Link(
override val key: String,
@StringRes val title: Int,
@DrawableRes val icon: Int?,
val showDivider: Boolean,
val onClick: (() -> Unit)?,
) : ProductDetailRowUiModel

data class Button(
override val key: String,
@StringRes val text: Int,
@DrawableRes val icon: Int?,
val showDivider: Boolean,
val tooltip: ProductDetailTooltipUiModel?,
val link: ProductDetailButtonLinkUiModel?,
val onClick: () -> Unit,
) : ProductDetailRowUiModel

data class Switch(
override val key: String,
@StringRes val title: Int,
val isOn: Boolean,
@DrawableRes val icon: Int?,
val onStateChanged: ((Boolean) -> Unit)?,
) : ProductDetailRowUiModel

data class Warning(
override val key: String,
val content: String,
) : ProductDetailRowUiModel
}

@Immutable
data class ProductDetailPropertyValueUiModel(
val label: String,
val value: String,
)

@Immutable
data class ProductDetailTooltipUiModel(
@StringRes val title: Int,
@StringRes val text: Int,
@StringRes val dismissButtonText: Int,
val onDismiss: () -> Unit,
)

@Immutable
data class ProductDetailButtonLinkUiModel(
@StringRes val text: Int,
val onClick: () -> Unit,
)
Loading
Loading