mirror of
https://github.com/pawelorzech/Swoosh.git
synced 2026-03-31 11:55:47 +00:00
feat: add SENT status, QUEUED_EMAIL_ONLY queue status, and email_only field for email-only posts
Phase 4b.1: Add data model support for email-only posts. - PostStatus: add SENT enum value - QueueStatus: add QUEUED_EMAIL_ONLY enum value - PostFilter: add SENT filter with "status:sent" ghost filter - GhostPost: add email_only Boolean field - FeedPost: add emailOnly Boolean field - LocalPostDao: include QUEUED_EMAIL_ONLY in queued posts query - OverallStats: handle SENT status in stats calculation - FeedScreen: show "Pending email send" for QUEUED_EMAIL_ONLY queue status - Update tests for new enum values and fields
This commit is contained in:
parent
3b1061694d
commit
f9d060ed7d
6 changed files with 139 additions and 13 deletions
|
|
@ -26,13 +26,13 @@ interface LocalPostDao {
|
|||
|
||||
@Query("SELECT * FROM local_posts WHERE queueStatus IN (:statuses) ORDER BY createdAt ASC")
|
||||
suspend fun getQueuedPosts(
|
||||
statuses: List<QueueStatus> = listOf(QueueStatus.QUEUED_PUBLISH, QueueStatus.QUEUED_SCHEDULED)
|
||||
statuses: List<QueueStatus> = listOf(QueueStatus.QUEUED_PUBLISH, QueueStatus.QUEUED_SCHEDULED, QueueStatus.QUEUED_EMAIL_ONLY)
|
||||
): List<LocalPost>
|
||||
|
||||
@Query("SELECT * FROM local_posts WHERE accountId = :accountId AND queueStatus IN (:statuses) ORDER BY createdAt ASC")
|
||||
suspend fun getQueuedPostsByAccount(
|
||||
accountId: String,
|
||||
statuses: List<QueueStatus> = listOf(QueueStatus.QUEUED_PUBLISH, QueueStatus.QUEUED_SCHEDULED)
|
||||
statuses: List<QueueStatus> = listOf(QueueStatus.QUEUED_PUBLISH, QueueStatus.QUEUED_SCHEDULED, QueueStatus.QUEUED_EMAIL_ONLY)
|
||||
): List<LocalPost>
|
||||
|
||||
@Query("SELECT * FROM local_posts WHERE localId = :localId")
|
||||
|
|
|
|||
|
|
@ -48,7 +48,8 @@ data class GhostPost(
|
|||
val visibility: String? = "public",
|
||||
val authors: List<Author>? = null,
|
||||
val reading_time: Int? = null,
|
||||
val tags: List<GhostTag>? = null
|
||||
val tags: List<GhostTag>? = null,
|
||||
val email_only: Boolean? = null
|
||||
)
|
||||
|
||||
data class GhostTag(
|
||||
|
|
@ -107,13 +108,15 @@ data class LocalPost(
|
|||
enum class PostStatus {
|
||||
DRAFT,
|
||||
PUBLISHED,
|
||||
SCHEDULED
|
||||
SCHEDULED,
|
||||
SENT
|
||||
}
|
||||
|
||||
enum class QueueStatus {
|
||||
NONE,
|
||||
QUEUED_PUBLISH,
|
||||
QUEUED_SCHEDULED,
|
||||
QUEUED_EMAIL_ONLY,
|
||||
UPLOADING,
|
||||
FAILED
|
||||
}
|
||||
|
|
@ -145,7 +148,8 @@ data class FeedPost(
|
|||
val isLocal: Boolean = false,
|
||||
val queueStatus: QueueStatus = QueueStatus.NONE,
|
||||
val fileUrl: String? = null,
|
||||
val fileName: String? = null
|
||||
val fileName: String? = null,
|
||||
val emailOnly: Boolean = false
|
||||
)
|
||||
|
||||
@Stable
|
||||
|
|
@ -162,7 +166,8 @@ enum class PostFilter(val displayName: String, val ghostFilter: String?) {
|
|||
ALL("All", null),
|
||||
PUBLISHED("Published", "status:published"),
|
||||
DRAFT("Drafts", "status:draft"),
|
||||
SCHEDULED("Scheduled", "status:scheduled");
|
||||
SCHEDULED("Scheduled", "status:scheduled"),
|
||||
SENT("Sent", "status:sent");
|
||||
|
||||
/** Returns the matching [PostStatus] for local filtering, or null for ALL. */
|
||||
fun toPostStatus(): PostStatus? = when (this) {
|
||||
|
|
@ -170,6 +175,7 @@ enum class PostFilter(val displayName: String, val ghostFilter: String?) {
|
|||
PUBLISHED -> PostStatus.PUBLISHED
|
||||
DRAFT -> PostStatus.DRAFT
|
||||
SCHEDULED -> PostStatus.SCHEDULED
|
||||
SENT -> PostStatus.SENT
|
||||
}
|
||||
|
||||
/** Empty-state message shown when filter yields no results. */
|
||||
|
|
@ -178,6 +184,7 @@ enum class PostFilter(val displayName: String, val ghostFilter: String?) {
|
|||
PUBLISHED -> "No published posts yet"
|
||||
DRAFT -> "No drafts yet"
|
||||
SCHEDULED -> "No scheduled posts yet"
|
||||
SENT -> "No sent newsletters yet"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ data class OverallStats(
|
|||
PostStatus.PUBLISHED -> publishedCount++
|
||||
PostStatus.DRAFT -> draftCount++
|
||||
PostStatus.SCHEDULED -> scheduledCount++
|
||||
PostStatus.SENT -> publishedCount++ // sent counts as published
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -47,6 +48,7 @@ data class OverallStats(
|
|||
"published" -> publishedCount++
|
||||
"draft" -> draftCount++
|
||||
"scheduled" -> scheduledCount++
|
||||
"sent" -> publishedCount++
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1621,6 +1621,7 @@ fun PostCardContent(
|
|||
Spacer(modifier = Modifier.height(8.dp))
|
||||
val queueLabel = when (post.queueStatus) {
|
||||
QueueStatus.QUEUED_PUBLISH, QueueStatus.QUEUED_SCHEDULED -> "Pending upload"
|
||||
QueueStatus.QUEUED_EMAIL_ONLY -> "Pending email send"
|
||||
QueueStatus.UPLOADING -> "Uploading..."
|
||||
QueueStatus.FAILED -> "Upload failed"
|
||||
else -> ""
|
||||
|
|
@ -1631,7 +1632,7 @@ fun PostCardContent(
|
|||
label = queueLabel,
|
||||
isUploading = isUploading
|
||||
)
|
||||
if (post.queueStatus == QueueStatus.QUEUED_PUBLISH || post.queueStatus == QueueStatus.QUEUED_SCHEDULED) {
|
||||
if (post.queueStatus == QueueStatus.QUEUED_PUBLISH || post.queueStatus == QueueStatus.QUEUED_SCHEDULED || post.queueStatus == QueueStatus.QUEUED_EMAIL_ONLY) {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
TextButton(onClick = onCancelQueue) {
|
||||
Text("Cancel", style = MaterialTheme.typography.labelSmall)
|
||||
|
|
|
|||
|
|
@ -265,13 +265,13 @@ class GhostModelsTest {
|
|||
// --- Enum values ---
|
||||
|
||||
@Test
|
||||
fun `PostStatus has exactly 3 values`() {
|
||||
assertEquals(3, PostStatus.values().size)
|
||||
fun `PostStatus has exactly 4 values`() {
|
||||
assertEquals(4, PostStatus.values().size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `QueueStatus has exactly 5 values`() {
|
||||
assertEquals(5, QueueStatus.values().size)
|
||||
fun `QueueStatus has exactly 6 values`() {
|
||||
assertEquals(6, QueueStatus.values().size)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -279,6 +279,7 @@ class GhostModelsTest {
|
|||
assertEquals(PostStatus.DRAFT, PostStatus.valueOf("DRAFT"))
|
||||
assertEquals(PostStatus.PUBLISHED, PostStatus.valueOf("PUBLISHED"))
|
||||
assertEquals(PostStatus.SCHEDULED, PostStatus.valueOf("SCHEDULED"))
|
||||
assertEquals(PostStatus.SENT, PostStatus.valueOf("SENT"))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -286,6 +287,7 @@ class GhostModelsTest {
|
|||
assertEquals(QueueStatus.NONE, QueueStatus.valueOf("NONE"))
|
||||
assertEquals(QueueStatus.QUEUED_PUBLISH, QueueStatus.valueOf("QUEUED_PUBLISH"))
|
||||
assertEquals(QueueStatus.QUEUED_SCHEDULED, QueueStatus.valueOf("QUEUED_SCHEDULED"))
|
||||
assertEquals(QueueStatus.QUEUED_EMAIL_ONLY, QueueStatus.valueOf("QUEUED_EMAIL_ONLY"))
|
||||
assertEquals(QueueStatus.UPLOADING, QueueStatus.valueOf("UPLOADING"))
|
||||
assertEquals(QueueStatus.FAILED, QueueStatus.valueOf("FAILED"))
|
||||
}
|
||||
|
|
@ -422,4 +424,95 @@ class GhostModelsTest {
|
|||
val json = gson.toJson(wrapper)
|
||||
assertTrue(json.contains("\"feature_image_alt\":\"Photo description\""))
|
||||
}
|
||||
|
||||
// --- email_only field ---
|
||||
|
||||
@Test
|
||||
fun `GhostPost default email_only is null`() {
|
||||
val post = GhostPost()
|
||||
assertNull(post.email_only)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GhostPost stores email_only true`() {
|
||||
val post = GhostPost(email_only = true)
|
||||
assertEquals(true, post.email_only)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GhostPost serializes email_only to JSON`() {
|
||||
val post = GhostPost(title = "Email post", email_only = true)
|
||||
val json = gson.toJson(post)
|
||||
assertTrue(json.contains("\"email_only\":true"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GhostPost deserializes email_only from JSON`() {
|
||||
val json = """{"id":"1","email_only":true}"""
|
||||
val post = gson.fromJson(json, GhostPost::class.java)
|
||||
assertEquals(true, post.email_only)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GhostPost deserializes with missing email_only`() {
|
||||
val json = """{"id":"1","title":"Test"}"""
|
||||
val post = gson.fromJson(json, GhostPost::class.java)
|
||||
assertNull(post.email_only)
|
||||
}
|
||||
|
||||
// --- FeedPost emailOnly ---
|
||||
|
||||
@Test
|
||||
fun `FeedPost default emailOnly is false`() {
|
||||
val post = FeedPost(
|
||||
title = "Test",
|
||||
textContent = "Content",
|
||||
htmlContent = null,
|
||||
imageUrl = null,
|
||||
linkUrl = null,
|
||||
linkTitle = null,
|
||||
linkDescription = null,
|
||||
linkImageUrl = null,
|
||||
status = "published",
|
||||
publishedAt = null,
|
||||
createdAt = null,
|
||||
updatedAt = null
|
||||
)
|
||||
assertFalse(post.emailOnly)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `FeedPost stores emailOnly true`() {
|
||||
val post = FeedPost(
|
||||
title = "Test",
|
||||
textContent = "Content",
|
||||
htmlContent = null,
|
||||
imageUrl = null,
|
||||
linkUrl = null,
|
||||
linkTitle = null,
|
||||
linkDescription = null,
|
||||
linkImageUrl = null,
|
||||
status = "sent",
|
||||
publishedAt = null,
|
||||
createdAt = null,
|
||||
updatedAt = null,
|
||||
emailOnly = true
|
||||
)
|
||||
assertTrue(post.emailOnly)
|
||||
}
|
||||
|
||||
// --- LocalPost emailOnly ---
|
||||
|
||||
@Test
|
||||
fun `LocalPost default emailOnly is false`() {
|
||||
val post = LocalPost()
|
||||
assertFalse(post.emailOnly)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `LocalPost stores emailOnly true`() {
|
||||
val post = LocalPost(emailOnly = true, newsletterSlug = "default-newsletter")
|
||||
assertTrue(post.emailOnly)
|
||||
assertEquals("default-newsletter", post.newsletterSlug)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ class PostFilterTest {
|
|||
// --- Enum values ---
|
||||
|
||||
@Test
|
||||
fun `PostFilter has exactly 4 values`() {
|
||||
assertEquals(4, PostFilter.values().size)
|
||||
fun `PostFilter has exactly 5 values`() {
|
||||
assertEquals(5, PostFilter.values().size)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -18,6 +18,7 @@ class PostFilterTest {
|
|||
assertEquals(PostFilter.PUBLISHED, PostFilter.valueOf("PUBLISHED"))
|
||||
assertEquals(PostFilter.DRAFT, PostFilter.valueOf("DRAFT"))
|
||||
assertEquals(PostFilter.SCHEDULED, PostFilter.valueOf("SCHEDULED"))
|
||||
assertEquals(PostFilter.SENT, PostFilter.valueOf("SENT"))
|
||||
}
|
||||
|
||||
// --- Display names ---
|
||||
|
|
@ -107,4 +108,26 @@ class PostFilterTest {
|
|||
fun `SCHEDULED emptyMessage returns No scheduled posts yet`() {
|
||||
assertEquals("No scheduled posts yet", PostFilter.SCHEDULED.emptyMessage())
|
||||
}
|
||||
|
||||
// --- SENT filter ---
|
||||
|
||||
@Test
|
||||
fun `SENT displayName is Sent`() {
|
||||
assertEquals("Sent", PostFilter.SENT.displayName)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SENT ghostFilter is status_sent`() {
|
||||
assertEquals("status:sent", PostFilter.SENT.ghostFilter)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SENT toPostStatus returns PostStatus SENT`() {
|
||||
assertEquals(PostStatus.SENT, PostFilter.SENT.toPostStatus())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SENT emptyMessage returns No sent newsletters yet`() {
|
||||
assertEquals("No sent newsletters yet", PostFilter.SENT.emptyMessage())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue