Skip to content

[MBL-19040][Teacher] Edit File page state sets back to default after orientation change#3684

Closed
kristofnemere wants to merge 1 commit into
masterfrom
MBL-19040-edit-file-orientation-state
Closed

[MBL-19040][Teacher] Edit File page state sets back to default after orientation change#3684
kristofnemere wants to merge 1 commit into
masterfrom
MBL-19040-edit-file-orientation-state

Conversation

@kristofnemere
Copy link
Copy Markdown
Contributor

@kristofnemere kristofnemere commented May 11, 2026

Test plan:

  1. Open the Teacher app and navigate to any course's Files
  2. Open a file's Edit screen
  3. Change the title and access (e.g. switch to Restricted Access / Schedule Availability, set lock/unlock dates)
  4. Rotate the device
  5. Verify the title and all access fields (status, schedule, dates/times) are preserved
  6. Repeat for usage rights (justification, license, copyright holder) and confirm those persist as well
  7. Save and verify the update applies the values shown on screen

refs: MBL-19040
affects: Teacher
release note: Fixed an issue where the Edit File screen lost its in-progress changes after rotating the device.

Checklist

  • Follow-up e2e test ticket created or not needed
  • Tested in dark mode
  • Tested in light mode
  • Test in landscape mode and/or tablet
  • A11y checked
  • Approve from product

…orientation change

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Nice refactor — moving the edit state (accessStatus, lockDate/unlockDate, usageType, licenseType, editedName, editedCopyright) out of the fragment and into the presenter is a clear win for surviving configuration changes (e.g., rotation), and it also makes the derivation logic far more testable. The diff is well-scoped and the call sites read cleanly.

A few things I especially liked:

  • Fixing the missing else in setupAccess so that RestrictedScheduleStatus() is actually returned when lock/unlock dates exist (previously the expression was constructed and silently discarded).
  • Replacing licenseList.filter { ... }[0] with .firstOrNull() to avoid a potential IndexOutOfBoundsException on an empty CC list.
  • Consolidating spinner initial-position logic into clean when (presenter.accessStatus) blocks driven by sealed-class subtypes rather than re-reading currentFileOrFolder fields in multiple places.
  • Adding the copyrightTextWatcher so the copyright field's state persists in the presenter alongside the title.

Issues to consider

  • Behavior change: lockDate/unlockDate are now seeded from the file unconditionally (presenter lines 45–46), including for isLocked files — previously they were only seeded for the hidden/scheduled branch. Combined with the setupAccess else fix, this changes the saved payload when a user switches a previously-locked file with dates back to "Restricted Access". (inline)
  • setSelection(-1) edge case in setupLicenses when both presenter.licenseType?.name is null and the filtered CC list is empty — getPosition(null) returns -1. Consider .coerceAtLeast(0). (inline)
  • Redundant assignment in setupAccess's restrictedAccess branch — overwritten immediately by setupRestrictedAccess's listener. Not a defect, just a cleanup opportunity. (inline)
  • No unit tests added for the new presenter state. Now that initialization is a pure function of currentFileOrFolder, this is the perfect moment to add an EditFileFolderPresenterTest covering the access-status precedence and field defaulting. (inline)

Comment on lines +45 to +46
var lockDate: Date? = currentFileOrFolder.lockDate
var unlockDate: Date? = currentFileOrFolder.unlockDate
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Subtle behavior change worth verifying: previously the fragment's lockDate/unlockDate were only seeded from the file when it was hidden/scheduled (the isHidden || lockDate != null || unlockDate != null branch in the old setupAccess). Now they're seeded from the file unconditionally, including for isLocked (unpublished) files.

Combined with the bug fix in setupAccess (the missing else between RestrictedScheduleStatus() and RestrictedStatus()), this means: if a user opens a locked file that previously had lock/unlock dates and switches the access spinner to "Restricted Access", the status will now resolve to RestrictedScheduleStatus() and the prior dates will be sent on save. Previously, switching always resolved to RestrictedStatus() (due to the missing else) and the dates were not used.

This is probably the intended improvement, but it does change the saved payload for that flow — please confirm it matches the desired UX.

Comment on lines +424 to +425
val initialPosition = spinnerAdapter.getPosition(presenter.licenseType?.name
?: presenter.licenseList.filter { it.name.contains("CC") }.firstOrNull()?.name)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice safety improvement over the original [0] (which would throw IndexOutOfBoundsException on an empty CC license list). One edge case to consider though: if presenter.licenseType?.name is null and the filtered CC list is empty, firstOrNull()?.name returns null, and ArrayAdapter.getPosition(null) returns -1. licenseSpinner.setSelection(-1) will leave the spinner in an unselected state.

If licenseList is guaranteed to contain at least one CC entry, this is fine. Otherwise, consider defaulting to 0:

val initialPosition = spinnerAdapter.getPosition(presenter.licenseType?.name
    ?: presenter.licenseList.firstOrNull { it.name.contains("CC") }?.name)
    .coerceAtLeast(0)

Comment on lines 277 to 280
getString(R.string.restrictedAccess) -> {
if (lockDate != null || unlockDate != null)
RestrictedScheduleStatus()
RestrictedStatus()
if (presenter.lockDate != null || presenter.unlockDate != null) RestrictedScheduleStatus()
else RestrictedStatus()
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch fixing the missing else here — the original if (lockDate != null || unlockDate != null) RestrictedScheduleStatus() followed by RestrictedStatus() always returned RestrictedStatus() regardless of the dates because the RestrictedScheduleStatus() expression was constructed and discarded.

Note that this assignment is somewhat redundant: setupRestrictedAccess runs immediately after setupAccess and its onItemSelected listener also writes to presenter.accessStatus whenever the restricted access section becomes visible — so this value ends up being overwritten. Not a defect, just worth being aware of if you later refactor further.

Comment on lines +37 to +42
var accessStatus: FileAccessStatus = when {
currentFileOrFolder.isLocked -> UnpublishStatus()
currentFileOrFolder.isHidden -> RestrictedStatus()
currentFileOrFolder.unlockDate != null || currentFileOrFolder.lockDate != null -> RestrictedScheduleStatus()
else -> PublishStatus()
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that the access-status derivation lives in the presenter (a pure function of currentFileOrFolder) it would be a great candidate for a small unit test. I don't see an EditFileFolderPresenterTest in apps/teacher/src/test/. Suggested cases:

  • isLocked = trueUnpublishStatus
  • isHidden = trueRestrictedStatus
  • lockDate != null (only) → RestrictedScheduleStatus
  • unlockDate != null (only) → RestrictedScheduleStatus
  • neither → PublishStatus
  • isLocked && isHidden (precedence) → UnpublishStatus
  • isHidden && lockDate != null (precedence) → RestrictedStatus

Also worth covering editedName fallback to displayName when name is null, and editedCopyright defaulting to empty when usageRights is null — these previously lived in the fragment and were untestable, so this is a nice opportunity to lock the behavior in.

@github-actions
Copy link
Copy Markdown
Contributor

🧪 Unit Test Results

✅ 📱 Teacher App

  • Tests: 364 total, 0 failed, 0 skipped
  • Duration: 28.770s
  • Success Rate: 100%

✅ 📦 Submodules

  • Tests: 3477 total, 0 failed, 0 skipped
  • Duration: 54.174s
  • Success Rate: 100%

📊 Summary

  • Total Tests: 3841
  • Failed: 0
  • Skipped: 0
  • Status: ✅ All tests passed!

Last updated: Mon, 11 May 2026 13:43:26 GMT

@github-actions
Copy link
Copy Markdown
Contributor

📊 Code Coverage Report

✅ Student

  • PR Coverage: 42.48%
  • Master Coverage: 42.48%
  • Delta: +0.00%

⚠️ Teacher

  • PR Coverage: 25.15%
  • Master Coverage: 25.23%
  • Delta: -0.08%

✅ Pandautils

  • PR Coverage: 24.15%
  • Master Coverage: 24.15%
  • Delta: +0.00%

📈 Overall Average

  • PR Coverage: 30.59%
  • Master Coverage: 30.62%
  • Delta: -0.03%

@github-actions
Copy link
Copy Markdown
Contributor

Teacher Install Page

Copy link
Copy Markdown
Contributor

@kdeakinstructure kdeakinstructure left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QA 👍

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants