Skip to content

Task detail screen #129

Description

@MessiasLima

Description

A read-only detail screen that shows all fields of a task. Users navigate to it by tapping a task row from the Today or Upcoming screens.

The screen displays the task title, description, due date, priority, URL (as a tappable link), creation date, and completion date (if completed). Tapping the URL opens it in the platform browser.

A status indicator is shown: red when the task is overdue, grey when the task is completed, and hidden when it is a regular unfinished (on-track) task.

The screen includes a "Mark as completed" button, a "Delete" button, and a "Share" button that shares the task content as plain text via the platform share sheet.

The visual design of this screen must be reviewed as part of the ticket to ensure layout, spacing, and visual hierarchy are correct before implementation is considered complete.

Scope

In scope: TaskDetailScreen composable, TaskDetailViewModel, TaskDetailNavigation route, displaying all task fields, status indicator (red overdue, grey completed, hidden on-track), tappable URL that opens in the platform browser, "Mark as completed" button, "Delete" button, "Share" button that exports task content as plain text via platform share sheet, back navigation, design review, and string resources.

Out of scope: edit task (separate ticket), location map (separate ticket), delete confirmation dialog (separate ticket), and statistics.

Acceptance criteria

  • GIVEN the user taps a task row on the Today or Upcoming screen
    WHEN the navigation occurs
    THEN the Task Detail screen is displayed for that task
  • GIVEN the Task Detail screen is displayed
    WHEN the screen renders
    THEN it shows the task title, description, due date, priority, creation date, and completion date
  • GIVEN the Task Detail screen is displayed and the task has a URL
    WHEN the user taps the URL
    THEN the URL opens in the platform browser
  • GIVEN the Task Detail screen is displayed and the task has no URL
    WHEN the screen renders
    THEN the URL field is not shown
  • GIVEN the Task Detail screen is displayed and the task is overdue
    WHEN the screen renders
    THEN a red status indicator is shown
  • GIVEN the Task Detail screen is displayed and the task is completed
    WHEN the screen renders
    THEN a grey status indicator is shown
  • GIVEN the Task Detail screen is displayed and the task is unfinished and not overdue
    WHEN the screen renders
    THEN no status indicator is shown
  • GIVEN the Task Detail screen is displayed
    WHEN the screen renders
    THEN a "Mark as completed" button is visible
  • GIVEN the Task Detail screen is displayed
    WHEN the screen renders
    THEN a "Delete" button is visible
  • GIVEN the Task Detail screen is displayed
    WHEN the user taps the "Share" button
    THEN the platform share sheet opens with the task content formatted as plain text
  • GIVEN the user is on the Task Detail screen
    WHEN they press the back button or back arrow
    THEN they navigate back to the previous screen
  • GIVEN the Task Detail screen UI has been implemented
    WHEN the ticket is reviewed
    THEN the design is explicitly reviewed for layout, spacing, visual hierarchy, status indicator placement, and button placement
  • GIVEN user-visible text appears on the Task Detail screen
    WHEN the app renders the screen
    THEN all visible text comes from string resources

Activity

  1. added this to the next milestone on Jul 15, 2026
  2. MessiasLima commented on Jul 26, 2026

    @MessiasLima
    OwnerAuthor

    /oc

    Task Detail Screen Implementation Plan

    Issue target

    Implement issue #129 as a new read-only Task Detail screen in the KMP app.

    The screen must:

    1. Open when the user taps a task row.
    2. Show task detail fields.
    3. Show overdue/completed status.
    4. Open task URLs in the platform browser.
    5. Let the user mark the task as completed.
    6. Let the user delete the task.
    7. Let the user share the task as plain text.
    8. Support back navigation.
    9. Use string resources for all user-visible text.
    10. Receive an explicit design review before the ticket is considered done.

    Current codebase status

    1. Today is the only task-list screen currently implemented. There is no Upcoming screen in the repository yet.
    2. TaskRepository only exposes findTodayTasks() and save(task).
    3. TaskDao only exposes save, findAll, and findTodayTasks.
    4. There is no existing task detail route, screen, or ViewModel.
    5. There is no existing outbound share abstraction.
    6. There is no existing outbound browser abstraction, but LocalUriHandler can likely cover URL opening in common Compose UI.
    7. Task rows are rendered from TodayScreen.kt using FoliaryTaskCard, and row taps are not wired today.

    Important scope notes

    1. The ticket mentions navigation from Today or Upcoming, but only Today exists right now.
    2. The ticket description says "all fields of a task", but location map is explicitly out of scope and the current entity has a nullable location field.
    3. The app targets android, ios, and jvm. Android and iOS have native share sheets. JVM desktop does not have a standard OS share sheet equivalent, so desktop behavior needs an explicit implementation decision.

    Recommended implementation shape

    Keep the implementation minimal and consistent with the existing screen pattern:

    1. Add a dedicated feature/taskdetail/ package.
    2. Add the standard three feature files:
      TaskDetailViewModel.kt, TaskDetailScreen.kt, and TaskDetailNavigation.kt.
    3. Extend the existing task data layer only where the detail screen needs it.
    4. Reuse the existing save(task) API to mark a task as completed instead of adding a separate complete endpoint.
    5. Use LocalUriHandler for URL opening.
    6. Add a small platform share abstraction for plain-text sharing.

    Files to add

    1. foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/taskdetail/TaskDetailViewModel.kt
    2. foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/taskdetail/TaskDetailScreen.kt
    3. foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/taskdetail/TaskDetailNavigation.kt
    4. foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/share/TaskShareLauncher.kt
    5. foliary/src/androidMain/kotlin/dev/appoutlet/foliary/core/share/TaskShareLauncher.android.kt
    6. foliary/src/iosMain/kotlin/dev/appoutlet/foliary/core/share/TaskShareLauncher.ios.kt
    7. foliary/src/jvmMain/kotlin/dev/appoutlet/foliary/core/share/TaskShareLauncher.jvm.kt
    8. foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/taskdetail/TaskDetailViewModelTest.kt
    9. foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/taskdetail/TaskDetailScreenTest.kt

    Files to update

    1. foliary/src/commonMain/kotlin/dev/appoutlet/foliary/data/task/TaskRepository.kt
    2. foliary/src/commonMain/kotlin/dev/appoutlet/foliary/data/task/TaskRepositoryImpl.kt
    3. foliary/src/commonMain/kotlin/dev/appoutlet/foliary/data/task/database/TaskDao.kt
    4. foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/today/TodayViewModel.kt
    5. foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/today/TodayScreen.kt
    6. foliary/src/commonMain/composeResources/values/strings.xml
    7. foliary/src/commonMain/composeResources/values-en/strings.xml
    8. foliary/src/commonTest/kotlin/dev/appoutlet/foliary/data/task/TaskRepositoryImplTest.kt
    9. foliary/src/commonTest/kotlin/dev/appoutlet/foliary/data/task/database/TaskDaoTest.kt
    10. foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/today/TodayViewModelTest.kt
    11. foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/today/TodayScreenTest.kt

    Data-layer changes

    TaskRepository

    Add only the methods the detail flow actually needs:

    interface TaskRepository {
        fun findTodayTasks(): Flow<List<Task>>
        fun findById(id: Uuid): Flow<Task?>
        suspend fun save(task: Task)
        suspend fun delete(id: Uuid)
    }

    Rationale:

    1. findById(id) lets the detail screen react to edits and deletion.
    2. save(task) already supports replace/upsert and is enough for mark-as-completed.
    3. delete(id) is the only missing write operation needed for this ticket.

    TaskDao

    Add:

    @Query("SELECT * FROM Task WHERE id = :id")
    fun findById(id: Uuid): Flow<Task?>
    
    @Query("DELETE FROM Task WHERE id = :id")
    suspend fun delete(id: Uuid)

    Keep save(vararg task) unchanged and reuse it for completion.

    TaskRepositoryImpl

    Implement the new methods as thin DAO pass-throughs.

    No new business logic is needed here beyond delegating to the DAO.

    Navigation changes

    New nav key

    Add a dedicated key in TaskDetailNavigation.kt:

    @Serializable
    data class TaskDetailNavKey(val taskId: String) : NavKey

    Use String in the nav key because that matches the current FoliaryTaskCardViewData.id shape and avoids custom nav serialization work for Uuid.

    Route setup

    Follow the same pattern used by CreateTaskNavigation:

    1. Register scope.entry<TaskDetailNavKey> { ... }.
    2. Create the ViewModel with koinViewModel<TaskDetailViewModel>().
    3. Wrap the screen in Screen(screenName = "TaskDetailScreen", ...).
    4. Pass the taskId from the NavKey down into the composable.

    Why pass taskId into the screen instead of the ViewModel constructor

    The current codebase does not show a parameterized koinViewModel pattern. The least risky approach is:

    1. Create a normal TaskDetailViewModel with injected dependencies only.
    2. Pass taskId into TaskDetailScreen(taskId, viewData, onEvent).
    3. Trigger an initial LoadTask(taskId) event from a LaunchedEffect(taskId) inside the screen.

    This avoids adding a new dependency-injection pattern just for this feature.

    Today integration

    TodayViewModel.kt

    Add:

    sealed interface TodayEvent {
        data object OnAddTaskClick : TodayEvent
        data class OnTaskClick(val taskId: String) : TodayEvent
    }
    
    sealed interface TodayAction : Action {
        data object NavigateToCreateTask : TodayAction
        data object NavigateToSignIn : TodayAction
        data class NavigateToTaskDetail(val taskId: String) : TodayAction
    }

    Handle OnTaskClick by posting NavigateToTaskDetail(taskId).

    TodayScreen.kt

    Wire row taps in the items(viewData.tasks) block.

    Recommended minimal approach:

    1. Wrap the existing FoliaryTaskCard call in a clickable container.
    2. Emit TodayEvent.OnTaskClick(task.id) when that container is tapped.
    3. Handle the new action in onAction with navigator.navigate(TaskDetailNavKey(taskId)).

    This is smaller than changing FoliaryTaskCard globally.

    Upcoming

    Do not invent an Upcoming screen as part of this ticket unless the user asks for it.

    Document this in the PR or implementation notes:

    "Task Detail navigation is wired from Today now. Upcoming can use the same TaskDetailNavKey once that feature exists."

    ViewModel design

    Dependencies

    TaskDetailViewModel should depend on:

    1. TaskRepository
    2. TimeProvider

    No browser or share dependency is needed in the ViewModel if URL opening is handled in the composable and sharing is emitted as a side effect.

    Suggested state model

    sealed interface TaskDetailViewData {
        data object Idle : TaskDetailViewData
        data object Loading : TaskDetailViewData
        data class Loaded(
            val task: Task,
            val isOverdue: Boolean,
        ) : TaskDetailViewData
    }

    This keeps the state minimal and lets the screen render directly from the entity plus one derived flag.

    Suggested events

    sealed interface TaskDetailEvent {
        data class LoadTask(val taskId: String) : TaskDetailEvent
        data object BackClicked : TaskDetailEvent
        data object MarkCompletedClicked : TaskDetailEvent
        data object DeleteClicked : TaskDetailEvent
        data object ShareClicked : TaskDetailEvent
    }

    Suggested actions

    sealed interface TaskDetailAction : Action {
        data object NavigateBack : TaskDetailAction
        data class ShareTask(val text: String) : TaskDetailAction
    }

    OpenUrl does not need to be a ViewModel action if the URL is opened directly from the composable with LocalUriHandler.

    ViewModel behavior

    Load flow

    1. On first LoadTask(taskId), parse the Uuid.
    2. Start collecting taskRepository.findById(id).
    3. Reduce to Loading before collection starts.
    4. When a task is emitted, reduce to Loaded(task, isOverdue).
    5. If the DAO emits null before any task is loaded, surface an error state or navigate back.

    Recommended behavior for null:

    1. If the screen never successfully loaded a task, emit an ErrorState with explicit task-detail strings.
    2. If the task was already loaded and later becomes null because of deletion, navigate back.

    Overdue calculation

    Use the same logic already present in FoliaryTaskCardViewDataMapper:

    1. Completed tasks are never overdue.
    2. Null dueDate is not overdue.
    3. Otherwise dueDate < timeProvider.now() is overdue.

    This logic can stay private in the ViewModel to avoid introducing a new shared mapper unless reuse becomes necessary.

    Mark as completed

    Implementation:

    val loaded = state as? TaskDetailViewData.Loaded ?: return
    val updatedTask = loaded.task.copy(completionDate = loaded.task.completionDate ?: timeProvider.now())
    taskRepository.save(updatedTask)

    Recommendations:

    1. Keep the button visible to satisfy the ticket.
    2. Disable it when completionDate != null to avoid duplicate writes.
    3. Let the repository flow refresh the UI after save.

    Delete

    Implementation:

    1. Read the loaded task id.
    2. Call taskRepository.delete(id).
    3. Post NavigateBack immediately after delete succeeds.

    This is simpler than waiting for the observed task flow to emit null.

    Share

    Implementation:

    1. Build a plain-text payload from the currently loaded task.
    2. Post TaskDetailAction.ShareTask(text).
    3. Let the route-level action handler call the platform share launcher.

    Screen UI design

    Layout structure

    Use CreateTaskScreen as the closest full-screen layout reference.

    Recommended structure:

    1. Scaffold
    2. TopAppBar with FoliaryBackIconButton
    3. Scrollable detail content constrained with Modifier.widthInCompact()
    4. Bottom action area with the primary and secondary actions

    Top bar

    Use:

    1. Back arrow on the left
    2. Title from string resources, for example Task details

    Content sections

    Render the task content as stacked labeled sections in this order:

    1. Status indicator, if needed
    2. Title
    3. Description
    4. Due date
    5. Priority
    6. URL
    7. Creation date
    8. Completion date, only when completed

    Optional-field rules

    Use these rules to keep behavior consistent with the ticket and current nullable model:

    1. URL: hide the entire field when url == null or blank. This is explicitly required by the issue.
    2. Completion date: hide when completionDate == null.
    3. Description: show a fallback string such as No description when null or blank.
    4. Due date: show a fallback string such as No due date when null.
    5. Priority: show a fallback string such as No priority when null.
    6. Location: do not render in this ticket.

    The fallback approach is recommended because tasks created today default to null description, dueDate, priority, and url.

    Status indicator

    Rules:

    1. Overdue and not completed: show red indicator.
    2. Completed: show grey indicator.
    3. Unfinished and on-track: render nothing.

    Recommended implementation:

    1. A small inline status row near the top of the content.
    2. Use color plus label so the meaning is not color-only.
    3. Add test tags for the overdue and completed indicators.

    Possible resource-backed labels:

    1. Overdue
    2. Completed

    URL behavior

    If url is present:

    1. Render it as visually interactive text.
    2. Use LocalUriHandler.current.openUri(url) on tap.
    3. Style it distinctly from plain body text.

    This avoids adding a dedicated browser abstraction.

    Buttons

    Render three buttons:

    1. Mark as completed
    2. Delete
    3. Share

    Recommended arrangement:

    1. Mark as completed as the primary button.
    2. Share as secondary or outlined.
    3. Delete as outlined or low-emphasis destructive action.

    Because delete confirmation is explicitly out of scope, the delete button can act immediately.

    Date formatting

    No shared date formatter currently exists in common code.

    Recommended minimal implementation:

    1. Add small private formatter helpers inside TaskDetailScreen.kt or TaskDetailViewModel.kt.
    2. Convert Instant to local date/time with TimeZone.currentSystemDefault().
    3. Format consistently for due date, creation date, and completion date.

    Keep the formatter local unless a second screen needs the same output.

    Priority formatting

    Map the enum values to resource-backed labels:

    1. LOWEST
    2. LOW
    3. MEDIUM
    4. HIGH
    5. HIGHEST
    6. BLOCKER

    Do not display raw enum names in the UI.

    Share implementation

    Common API

    Add a small interface in common code:

    interface TaskShareLauncher {
        fun share(text: String)
    }

    Inject it in TaskDetailNavigation.kt and call it from the onAction handler when ShareTask(text) is emitted.

    Android

    Use Intent.ACTION_SEND with MIME type text/plain and launch a chooser.

    iOS

    Use UIActivityViewController with the plain-text payload.

    JVM desktop

    This requires a product/engineering decision before implementation is complete.

    Recommended options, in order of honesty:

    1. Hide the share button on JVM if true share-sheet parity is required.
    2. Provide a JVM-specific fallback such as copy-to-clipboard, but only if the ticket is explicitly relaxed for desktop.
    3. Attempt a best-effort desktop share integration if a supported OS-level API is available, but do not claim parity without verification.

    Because the issue explicitly says "platform share sheet", mobile support is straightforward, but JVM needs explicit agreement.

    Share text format

    Recommended payload:

    <Task title>
    
    Description: <description or fallback>
    Due date: <formatted due date or fallback>
    Priority: <localized priority or fallback>
    URL: <url, only when present>
    Created: <formatted creation date>
    Completed: <formatted completion date, only when present>
    

    Recommendations:

    1. Keep it plain text only.
    2. Reuse the same labels and fallback strings as the UI where practical.
    3. Omit URL and completion lines when those values are absent.

    String resources to add

    At minimum add new entries to values/strings.xml and values-en/strings.xml.

    Suggested names:

    1. task_detail_title
    2. task_detail_task_title_label
    3. task_detail_description_label
    4. task_detail_due_date_label
    5. task_detail_priority_label
    6. task_detail_url_label
    7. task_detail_creation_date_label
    8. task_detail_completion_date_label
    9. task_detail_mark_completed
    10. task_detail_delete
    11. task_detail_share
    12. task_detail_status_overdue
    13. task_detail_status_completed
    14. task_detail_no_description
    15. task_detail_no_due_date
    16. task_detail_no_priority
    17. task_detail_not_found_title
    18. task_detail_not_found_message

    If the share button or URL row needs accessibility text beyond visible labels, add those too.

    Testing plan

    Before implementing tests, load the testing skill as required by AGENTS.md.

    DAO tests

    Update TaskDaoTest.kt to cover:

    1. findById returns the saved task.
    2. delete(id) removes the task.
    3. findById emits null after deletion if the test shape supports flow observation.

    Repository tests

    Update TaskRepositoryImplTest.kt to cover:

    1. findById delegates to the DAO.
    2. delete(id) delegates to the DAO.

    Today tests

    Update TodayViewModelTest.kt to cover:

    1. OnTaskClick(taskId) posts NavigateToTaskDetail(taskId).

    Update TodayScreenTest.kt to cover:

    1. Tapping a rendered task row emits OnTaskClick(taskId).

    This may require adding a stable test tag to the clickable row wrapper.

    Task Detail ViewModel tests

    Add TaskDetailViewModelTest.kt for:

    1. Loading a task by id.
    2. Reducing to Loading before the task arrives.
    3. Emitting Loaded with isOverdue = true when the task is overdue.
    4. Emitting Loaded with isOverdue = false when completed.
    5. Posting NavigateBack on back click.
    6. Saving a copied task with completionDate = now when mark-completed is clicked.
    7. Deleting the task and posting NavigateBack when delete is clicked.
    8. Posting ShareTask(text) when share is clicked.

    Task Detail screen tests

    Add TaskDetailScreenTest.kt for:

    1. Rendering title, description, due date, priority, creation date, and button labels.
    2. Hiding the URL row when URL is absent.
    3. Showing the URL row when URL is present.
    4. Showing the overdue indicator when overdue.
    5. Showing the completed indicator when completed.
    6. Hiding the status indicator when on-track and unfinished.
    7. Emitting BackClicked, MarkCompletedClicked, DeleteClicked, and ShareClicked.

    Avoid over-testing color values directly unless the Compose test API makes it straightforward. Presence and semantics checks are enough for most of this ticket.

    Verification commands

    Do not run a full build.

    Recommended test commands after implementation:

    1. ./gradlew foliary:jvmTest --tests "dev.appoutlet.foliary.feature.taskdetail.TaskDetailViewModelTest"
    2. ./gradlew foliary:jvmTest --tests "dev.appoutlet.foliary.feature.taskdetail.TaskDetailScreenTest"
    3. ./gradlew foliary:jvmTest --tests "dev.appoutlet.foliary.feature.today.TodayViewModelTest"
    4. ./gradlew foliary:jvmTest --tests "dev.appoutlet.foliary.feature.today.TodayScreenTest"
    5. ./gradlew foliary:jvmTest --tests "dev.appoutlet.foliary.data.task.TaskRepositoryImplTest"
    6. ./gradlew foliary:jvmTest --tests "dev.appoutlet.foliary.data.task.database.TaskDaoTest"

    Design review checklist

    The ticket explicitly requires a design review before completion. Review these items after the first implementation pass:

    1. Is the title visually dominant enough?
    2. Are section labels and values clearly separated?
    3. Is the status indicator visible without overpowering the screen?
    4. Are vertical spacing and content grouping consistent?
    5. Does the URL look interactive?
    6. Are the bottom actions ordered and weighted correctly?
    7. Does the layout still feel balanced on narrow mobile width and wide desktop/tablet width?
    8. Does widthInCompact() keep the content readable on large screens?

    Recommended order of implementation

    1. Extend TaskDao and TaskRepository.
    2. Add TaskDetailNavigation.kt and TaskDetailViewModel.kt.
    3. Wire Today row taps to the new route.
    4. Build TaskDetailScreen.kt with URL opening and local formatting.
    5. Add the share abstraction and hook it into route-side effects.
    6. Add string resources.
    7. Add or update tests.
    8. Run only the targeted JVM tests.
    9. Perform the design review pass.

    Open decisions to resolve before or during implementation

    1. Should JVM desktop hide the share button, or should the ticket accept a non-share-sheet fallback there?
    2. Should nullable description, dueDate, and priority use fallback text, or should some of those rows be hidden? The fallback approach is recommended because it fits the current nullable model better.
    3. When findById(id) returns null for an invalid task id, should the app show an error state or immediately navigate back? The recommended behavior is error before first successful load, back navigation after delete.
    4. Should the completed button remain enabled after completion, or visible-but-disabled? Visible-but-disabled is the safest interpretation of the ticket.
  3. opencode-agent commented on Jul 26, 2026

    @opencode-agent
    Contributor

    Created PR #199

    New%20session%20-%202026-07-26T14%3A45%3A14.628Z
    opencode session  |  github run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions