MBL-3085: Handle schema change for RichTextHeader (#2479)

In addition, refactor Rich Text transformers into extension functions
and move them to a new file `RichTextTransformers.kt` within a new
package at `../transformers/extensions`.

Co-authored-by: Tony Teate <4317686+tonyteate@users.noreply.github.com>
This commit is contained in:
Tony Teate
2026-02-26 13:49:10 -05:00
committed by GitHub
parent 7c8de34808
commit db843a0559
5 changed files with 2848 additions and 981 deletions

View File

@@ -31,34 +31,7 @@ fragment storyRichTextComponentFragment on RichTextComponent {
...richTextPhotoFragment
}
}
... on RichTextHeader1 {
__typename
text
link
styles
children {
...richTextChildParagraphFragment
}
}
... on RichTextHeader2 {
__typename
text
link
styles
children {
...richTextChildParagraphFragment
}
}
... on RichTextHeader3 {
__typename
text
link
styles
children {
...richTextChildParagraphFragment
}
}
... on RichTextHeader4 {
... on RichTextHeader {
__typename
text
link

View File

File diff suppressed because it is too large Load Diff

View File

@@ -6,10 +6,10 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.kickstarter.features.projectstory.data.StoriedProject
import com.kickstarter.features.projectstory.data.transform
import com.kickstarter.libs.Environment
import com.kickstarter.libs.utils.extensions.isProjectUri
import com.kickstarter.models.Project
import com.kickstarter.services.transformers.extensions.toStoriedProject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
@@ -70,7 +70,7 @@ class ProjectStoryViewModel(
_projectStoryUiState.value = ProjectStoryUiState(
isLoading = false,
error = null,
storiedProject = transform(it?.projectStoryFragment)
storiedProject = it?.projectStoryFragment.toStoriedProject()
)
Timber.d("storiedProject: ${projectStoryUiState.value.storiedProject}")
}

View File

@@ -1,15 +1,6 @@
package com.kickstarter.features.projectstory.data
import com.kickstarter.fragment.ProjectStoryFragment
import com.kickstarter.fragment.RichTextChildParagraphFragment
import com.kickstarter.fragment.RichTextPhotoFragment
import com.kickstarter.fragment.StoryRichTextComponentFragment
import com.kickstarter.libs.utils.extensions.isTrue
import com.kickstarter.libs.utils.extensions.negate
import com.kickstarter.models.Photo
import com.kickstarter.models.Project
import com.kickstarter.services.transformers.decodeRelayId
import timber.log.Timber
data class StoriedProject(
val project: Project,
@@ -82,168 +73,3 @@ sealed interface RichTextItem {
val _present: Boolean,
) : RichTextItem
}
fun transform(projectStoryFragment: ProjectStoryFragment?): StoriedProject {
Timber.d("transform()")
val id = decodeRelayId(projectStoryFragment?.id) ?: -1
Timber.d("-- id: $id")
Timber.d("-- _?.pid: ${projectStoryFragment?.pid}")
val name = projectStoryFragment?.name
val slug = projectStoryFragment?.slug
val displayPrelaunch = (projectStoryFragment?.isLaunched ?: false).negate()
/* from GraphQLTransformers.getPhoto() */
val photo = projectStoryFragment?.imageUrl.let { photoUrl ->
Photo.builder()
.ed(photoUrl)
.full(photoUrl)
.little(photoUrl)
.med(photoUrl)
.small(photoUrl)
.thumb(photoUrl)
.altText(null)
.build()
}
val project = Project.builder()
.displayPrelaunch(displayPrelaunch)
.id(id)
.name(name)
.photo(photo) // - now we get the full size for field from GraphQL, but V1 provided several image sizes
.slug(slug)
.build()
val story =
if (project.displayPrelaunch().isTrue())
projectStoryFragment?.prelaunchStoryRichText?.storyRichTextComponentFragment?.let {
transform(it)
}
else
projectStoryFragment?.storyRichText?.storyRichTextComponentFragment?.let {
transform(it)
}
return StoriedProject(project, story)
}
/*
* Note the use of `mapNotNull` to filter out `null` items everywhere. This may change.
*/
fun transform(storyRichTextComponentFragment: StoryRichTextComponentFragment): RichTextComponent {
val items: List<RichTextItem> = storyRichTextComponentFragment.items.mapNotNull { fragmentItem ->
when {
fragmentItem.onRichText != null -> {
RichTextItem.Text.Paragraph(
fragmentItem.onRichText.__typename,
fragmentItem.onRichText.text,
fragmentItem.onRichText.link,
fragmentItem.onRichText.styles,
children = fragmentItem.onRichText.children?.mapNotNull { fragmentItemChild ->
when {
fragmentItemChild.richTextChildParagraphFragment != null ->
transform(fragmentItemChild.richTextChildParagraphFragment)
fragmentItemChild.richTextPhotoFragment != null -> transform(fragmentItemChild.richTextPhotoFragment)
else -> null
}
}
)
}
fragmentItem.onRichTextHeader1 != null -> {
RichTextItem.Text.Header(
RichTextItem.Text.Header.Level.H1,
fragmentItem.onRichTextHeader1.__typename,
fragmentItem.onRichTextHeader1.text,
fragmentItem.onRichTextHeader1.link,
fragmentItem.onRichTextHeader1.styles,
children = fragmentItem.onRichTextHeader1.children?.mapNotNull { fragmentItemChild ->
fragmentItemChild.richTextChildParagraphFragment?.let(::transform)
},
)
}
fragmentItem.onRichTextHeader2 != null -> {
RichTextItem.Text.Header(
RichTextItem.Text.Header.Level.H2,
fragmentItem.onRichTextHeader2.__typename,
fragmentItem.onRichTextHeader2.text,
fragmentItem.onRichTextHeader2.link,
fragmentItem.onRichTextHeader2.styles,
children = fragmentItem.onRichTextHeader2.children?.mapNotNull { fragmentItemChild ->
fragmentItemChild.richTextChildParagraphFragment?.let(::transform)
},
)
}
fragmentItem.onRichTextHeader3 != null -> {
RichTextItem.Text.Header(
RichTextItem.Text.Header.Level.H3,
fragmentItem.onRichTextHeader3.__typename,
fragmentItem.onRichTextHeader3.text,
fragmentItem.onRichTextHeader3.link,
fragmentItem.onRichTextHeader3.styles,
children = fragmentItem.onRichTextHeader3.children?.mapNotNull { fragmentItemChild ->
fragmentItemChild.richTextChildParagraphFragment?.let(::transform)
},
)
}
fragmentItem.onRichTextHeader4 != null -> {
RichTextItem.Text.Header(
RichTextItem.Text.Header.Level.H4,
fragmentItem.onRichTextHeader4.__typename,
fragmentItem.onRichTextHeader4.text,
fragmentItem.onRichTextHeader4.link,
fragmentItem.onRichTextHeader4.styles,
children = fragmentItem.onRichTextHeader4.children?.mapNotNull { fragmentItemChild ->
fragmentItemChild.richTextChildParagraphFragment?.let(::transform)
},
)
}
fragmentItem.onRichTextListItem != null -> {
RichTextItem.Text.ListItem(
fragmentItem.onRichTextListItem.__typename,
fragmentItem.onRichTextListItem.text,
fragmentItem.onRichTextListItem.link,
fragmentItem.onRichTextListItem.styles,
children = fragmentItem.onRichTextListItem.children?.mapNotNull { fragmentItemChild ->
fragmentItemChild.richTextChildParagraphFragment?.let(::transform)
},
)
}
fragmentItem.onRichTextPhoto != null -> {
transform(fragmentItem.onRichTextPhoto.richTextPhotoFragment)
}
fragmentItem.onRichTextOembed != null -> {
RichTextItem.Oembed(
fragmentItem.onRichTextOembed.__typename,
fragmentItem.onRichTextOembed.type,
fragmentItem.onRichTextOembed.iframeUrl
)
}
else -> null
}
}
return RichTextComponent(items)
}
fun transform(richTextChildParagraphFragment: RichTextChildParagraphFragment): RichTextItem.Text.ChildParagraph {
return RichTextItem.Text.ChildParagraph(
richTextChildParagraphFragment.__typename,
richTextChildParagraphFragment.text,
richTextChildParagraphFragment.link,
richTextChildParagraphFragment.styles
)
}
fun transform(richTextPhotoFragment: RichTextPhotoFragment): RichTextItem.Photo {
return RichTextItem.Photo(
richTextPhotoFragment.__typename,
richTextPhotoFragment.url,
richTextPhotoFragment.altText,
richTextPhotoFragment.caption,
richTextPhotoFragment.asset?.let {
RichTextItem.Photo.Asset(
it.url,
it.altText
)
}
)
}

View File

@@ -0,0 +1,147 @@
package com.kickstarter.services.transformers.extensions
import com.kickstarter.features.projectstory.data.RichTextComponent
import com.kickstarter.features.projectstory.data.RichTextItem
import com.kickstarter.features.projectstory.data.StoriedProject
import com.kickstarter.fragment.ProjectStoryFragment
import com.kickstarter.fragment.RichTextChildParagraphFragment
import com.kickstarter.fragment.RichTextPhotoFragment
import com.kickstarter.fragment.StoryRichTextComponentFragment
import com.kickstarter.libs.utils.extensions.isTrue
import com.kickstarter.libs.utils.extensions.negate
import com.kickstarter.models.Photo
import com.kickstarter.models.Project
import com.kickstarter.services.transformers.decodeRelayId
import timber.log.Timber
fun ProjectStoryFragment?.toStoriedProject(): StoriedProject {
Timber.d("transform()")
val id = decodeRelayId(this?.id) ?: -1
Timber.d("-- id: $id")
Timber.d("-- _?.pid: ${this?.pid}")
val name = this?.name
val slug = this?.slug
val displayPrelaunch = (this?.isLaunched ?: false).negate()
/* from GraphQLTransformers.getPhoto() */
val photo = this?.imageUrl.let { photoUrl ->
Photo.builder()
.ed(photoUrl)
.full(photoUrl)
.little(photoUrl)
.med(photoUrl)
.small(photoUrl)
.thumb(photoUrl)
.altText(null)
.build()
}
val project = Project.builder()
.displayPrelaunch(displayPrelaunch)
.id(id)
.name(name)
.photo(photo) // - now we get the full size for field from GraphQL, but V1 provided several image sizes
.slug(slug)
.build()
val story =
if (project.displayPrelaunch().isTrue())
this?.prelaunchStoryRichText?.storyRichTextComponentFragment?.toRichTextComponent()
else
this?.storyRichText?.storyRichTextComponentFragment?.toRichTextComponent()
return StoriedProject(project, story)
}
/*
* Note the use of `mapNotNull` to filter out `null` items everywhere. This may change.
*/
fun StoryRichTextComponentFragment.toRichTextComponent(): RichTextComponent {
val items: List<RichTextItem> = this.items.mapNotNull { fragmentItem ->
when {
fragmentItem.onRichText != null -> {
RichTextItem.Text.Paragraph(
fragmentItem.onRichText.__typename,
fragmentItem.onRichText.text,
fragmentItem.onRichText.link,
fragmentItem.onRichText.styles,
children = fragmentItem.onRichText.children?.mapNotNull { fragmentItemChild ->
when {
fragmentItemChild.richTextChildParagraphFragment != null ->
fragmentItemChild.richTextChildParagraphFragment.toChildParagraph()
fragmentItemChild.richTextPhotoFragment != null ->
fragmentItemChild.richTextPhotoFragment.toPhoto()
else -> null
}
}
)
}
fragmentItem.onRichTextHeader != null -> {
val styles = fragmentItem.onRichTextHeader.styles.orEmpty()
val level = when {
"HEADING_1" in styles -> RichTextItem.Text.Header.Level.H1
"HEADING_2" in styles -> RichTextItem.Text.Header.Level.H2
"HEADING_3" in styles -> RichTextItem.Text.Header.Level.H3
else -> RichTextItem.Text.Header.Level.H4 /* Least disruptive style */
}
RichTextItem.Text.Header(
level,
fragmentItem.onRichTextHeader.__typename,
fragmentItem.onRichTextHeader.text,
fragmentItem.onRichTextHeader.link,
fragmentItem.onRichTextHeader.styles,
children = fragmentItem.onRichTextHeader.children?.mapNotNull { fragmentItemChild ->
fragmentItemChild.richTextChildParagraphFragment?.toChildParagraph()
},
)
}
fragmentItem.onRichTextListItem != null -> {
RichTextItem.Text.ListItem(
fragmentItem.onRichTextListItem.__typename,
fragmentItem.onRichTextListItem.text,
fragmentItem.onRichTextListItem.link,
fragmentItem.onRichTextListItem.styles,
children = fragmentItem.onRichTextListItem.children?.mapNotNull { fragmentItemChild ->
fragmentItemChild.richTextChildParagraphFragment?.toChildParagraph()
},
)
}
fragmentItem.onRichTextPhoto != null -> {
fragmentItem.onRichTextPhoto.richTextPhotoFragment.toPhoto()
}
fragmentItem.onRichTextOembed != null -> {
RichTextItem.Oembed(
fragmentItem.onRichTextOembed.__typename,
fragmentItem.onRichTextOembed.type,
fragmentItem.onRichTextOembed.iframeUrl
)
}
else -> null
}
}
return RichTextComponent(items)
}
private fun RichTextChildParagraphFragment.toChildParagraph(): RichTextItem.Text.ChildParagraph {
return RichTextItem.Text.ChildParagraph(
__typename,
text,
link,
styles
)
}
private fun RichTextPhotoFragment.toPhoto(): RichTextItem.Photo {
return RichTextItem.Photo(
__typename,
url,
altText,
caption,
asset?.let {
RichTextItem.Photo.Asset(
it.url,
it.altText
)
}
)
}