From f5b1d01bd5da4ae069f3b5ed8923954031477086 Mon Sep 17 00:00:00 2001 From: Jordan Marshall Date: Wed, 3 Apr 2019 10:38:58 -0600 Subject: [PATCH 1/3] [Student][MBL-11324] Submission Details Skeleton View --- apps/student/build.gradle | 3 + .../student/espresso/StudentRenderTest.kt | 2 + .../student/ui/pages/SubmissionDetailsPage.kt | 23 + .../SubmissionDetailsRenderPage.kt | 96 ++++ .../SubmissionDetailsRenderTest.kt | 272 +++++++++ .../student/fragment/AssignmentFragment.kt | 13 +- ...java => OldSubmissionDetailsFragment.java} | 10 +- .../SubmissionDetailsModels.kt | 42 +- .../SubmissionDetailsPresenter.kt | 97 ++++ .../SubmissionDetailsUpdate.kt | 28 +- .../ui/SubmissionDetailsDrawerPagerAdapter.kt | 63 +++ .../ui/SubmissionDetailsFragment.kt | 81 +++ .../ui/SubmissionDetailsView.kt | 183 +++++- .../ui/SubmissionDetailsViewState.kt | 32 +- .../ui/AssignmentDetailsView.kt | 4 +- .../mobius/common/MobiusKotlinUtils.kt | 12 +- .../student/router/RouteResolver.kt | 2 + .../res/drawable/handle_bar_drag_view.xml | 22 + .../layout/fragment_assignment_details.xml | 1 - ...ml => fragment_old_submission_details.xml} | 0 .../layout/fragment_submissiont_details.xml | 193 +++++++ .../layout/spinner_submission_versions.xml | 27 + .../spinner_submission_versions_dropdown.xml | 26 + .../SubmissionDetailsPresenterTest.kt | 271 +++++++++ .../SubmissionDetailsUpdateTest.kt | 524 ++++++++++++------ .../canvasapi2/utils/DataResult.kt | 2 + libs/pandares/src/main/res/values/strings.xml | 7 + .../views/ProgressiveCanvasLoadingView.kt | 29 +- 28 files changed, 1827 insertions(+), 238 deletions(-) create mode 100644 apps/student/src/androidTest/java/com/instructure/student/ui/pages/SubmissionDetailsPage.kt create mode 100644 apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionDetailsRenderPage.kt create mode 100644 apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionDetailsRenderTest.kt rename apps/student/src/main/java/com/instructure/student/fragment/{SubmissionDetailsFragment.java => OldSubmissionDetailsFragment.java} (99%) create mode 100644 apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsPresenter.kt create mode 100644 apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/ui/SubmissionDetailsDrawerPagerAdapter.kt create mode 100644 apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/ui/SubmissionDetailsFragment.kt create mode 100644 apps/student/src/main/res/drawable/handle_bar_drag_view.xml rename apps/student/src/main/res/layout/{fragment_submission_details.xml => fragment_old_submission_details.xml} (100%) create mode 100644 apps/student/src/main/res/layout/fragment_submissiont_details.xml create mode 100644 apps/student/src/main/res/layout/spinner_submission_versions.xml create mode 100644 apps/student/src/main/res/layout/spinner_submission_versions_dropdown.xml create mode 100644 apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsPresenterTest.kt diff --git a/apps/student/build.gradle b/apps/student/build.gradle index 1679badb80..0da1899b65 100644 --- a/apps/student/build.gradle +++ b/apps/student/build.gradle @@ -239,6 +239,9 @@ dependencies { implementation 'com.makeramen:roundedimageview:2.3.0' implementation Libs.PHOTO_VIEW + /* Sliding Panel */ + implementation 'com.sothree.slidinguppanel:library:3.4.0' + /* Apache Commons */ implementation 'org.apache.commons:commons-text:1.6' diff --git a/apps/student/src/androidTest/java/com/instructure/student/espresso/StudentRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/espresso/StudentRenderTest.kt index dc3b525cf6..07a0d5a5ba 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/espresso/StudentRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/espresso/StudentRenderTest.kt @@ -19,6 +19,7 @@ package com.instructure.student.espresso import androidx.test.ext.junit.runners.AndroidJUnit4 import com.instructure.student.SingleFragmentTestActivity import com.instructure.student.ui.pages.renderPages.AssignmentDetailsRenderPage +import com.instructure.student.ui.pages.renderPages.SubmissionDetailsRenderPage import com.instructure.student.ui.utils.StudentActivityTestRule import com.instructure.student.ui.utils.StudentTest import org.junit.runner.RunWith @@ -34,5 +35,6 @@ abstract class StudentRenderTest : StudentTest() { } val assignmentDetailsRenderPage = AssignmentDetailsRenderPage() + val submissionDetailsRenderPage = SubmissionDetailsRenderPage() } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SubmissionDetailsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SubmissionDetailsPage.kt new file mode 100644 index 0000000000..a74f3f0bc6 --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SubmissionDetailsPage.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2019 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.instructure.student.ui.pages + +import com.instructure.espresso.page.BasePage +import com.instructure.student.R + +open class SubmissionDetailsPage : BasePage(R.id.submissionDetails) { +} diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionDetailsRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionDetailsRenderPage.kt new file mode 100644 index 0000000000..2d533533c5 --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionDetailsRenderPage.kt @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2019 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.instructure.student.ui.pages.renderPages + +import android.view.View +import androidx.test.espresso.Espresso.onData +import androidx.test.espresso.action.* +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.withSpinnerText +import com.instructure.espresso.* +import com.instructure.espresso.page.onView +import com.instructure.espresso.page.onViewWithText +import com.instructure.espresso.page.withAncestor +import com.instructure.espresso.page.withText +import com.instructure.student.R +import com.instructure.student.ui.pages.SubmissionDetailsPage +import org.hamcrest.CoreMatchers.* + +class SubmissionDetailsRenderPage : SubmissionDetailsPage() { + + val toolbar by OnViewWithId(R.id.toolbar) + val loadingView by OnViewWithId(R.id.loadingView) + val errorView by OnViewWithId(R.id.errorContainer) + val mainContent by OnViewWithId(R.id.contentWrapper) + val drawerContent by OnViewWithId(R.id.drawerViewPager) + val slidingPanel by OnViewWithId(R.id.slidingUpPanelLayout) + val versionSpinner by OnViewWithId(R.id.submissionVersionsSpinner) + + /* Grabs the current coordinates of the center of drawerTabLayout */ + private val tabLayoutCoordinates = object : CoordinatesProvider { + override fun calculateCoordinates(view: View): FloatArray { + val tabs = view.findViewById(R.id.drawerTabLayout) + val xy = IntArray(2).apply { tabs.getLocationOnScreen(this) } + val x = xy[0] + (tabs.width / 2f) + val y = xy[1] + (tabs.height / 2f) + return floatArrayOf(x, y) + } + } + + fun assertDisplaysToolbarTitle(text: String) { + onViewWithText(text).assertDisplayed() + } + + fun assertDisplaysLoadingView() { + loadingView.assertDisplayed() + errorView.assertGone() + mainContent.assertGone() + } + + fun assertDisplaysError() { + errorView.assertVisible() + loadingView.assertGone() + mainContent.assertGone() + } + + fun assertDisplaysContent() { + mainContent.assertDisplayed() + loadingView.assertGone() + errorView.assertGone() + } + + fun assertDisplaysDrawerContent() { + drawerContent.assertDisplayed() + } + + fun clickTab(name: String) { + onView(allOf(withAncestor(R.id.drawerTabLayout), withText(name))).click() + } + + fun swipeDrawerTo(location: GeneralLocation) { + slidingPanel.perform(GeneralSwipeAction(Swipe.FAST, tabLayoutCoordinates, location, Press.FINGER)) + } + + fun assertSpinnerMatchesText(text: String) { + versionSpinner.check(matches(withSpinnerText(containsString(text)))) + } + + fun assertSpinnerDropdownItemHasText(position: Int, text: String) { + onData(anything()).atPosition(position).check(matches(ViewMatchers.withText(text))) + } +} diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionDetailsRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionDetailsRenderTest.kt new file mode 100644 index 0000000000..28e984679e --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionDetailsRenderTest.kt @@ -0,0 +1,272 @@ +/* + * Copyright (C) 2019 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.instructure.student.ui.renderTests + +import android.content.pm.ActivityInfo +import androidx.test.espresso.action.GeneralLocation +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.Submission +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.espresso.assertGone +import com.instructure.espresso.assertVisible +import com.instructure.espresso.click +import com.instructure.student.espresso.StudentRenderTest +import com.instructure.student.mobius.assignmentDetails.submissionDetails.SubmissionDetailsModel +import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionDetailsFragment +import com.spotify.mobius.runners.WorkRunner +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.util.* + +@RunWith(AndroidJUnit4::class) +class SubmissionDetailsRenderTest : StudentRenderTest() { + + private lateinit var baseModel: SubmissionDetailsModel + + @Before + fun setup() { + baseModel = SubmissionDetailsModel( + assignmentId = 0, + isLoading = false, + canvasContext = Course(name = "Test Course"), + assignment = DataResult.Fail(), + rootSubmission = DataResult.Fail() + ) + } + + @Test + fun displaysToolbarTitle() { + loadPageWithModel(baseModel) + submissionDetailsRenderPage.assertDisplaysToolbarTitle("Submission") + } + + @Test + fun displaysErrorState() { + loadPageWithModel(baseModel) + submissionDetailsRenderPage.assertDisplaysError() + } + + @Test + fun displaysLoadingState() { + loadPageWithModel(baseModel.copy(isLoading = true)) + submissionDetailsRenderPage.assertDisplaysLoadingView() + } + + @Test + fun displaysLoadedState() { + loadPageWithModel( + baseModel.copy( + selectedSubmissionAttemptId = 1, + assignment = DataResult.Success(Assignment()), + rootSubmission = DataResult.Success( + Submission(submissionHistory = listOf(Submission(attempt = 1))) + ) + ) + ) + submissionDetailsRenderPage.assertDisplaysContent() + } + + @Test + fun hidesVersionSpinnerForSingleSubmission() { + loadPageWithModel( + baseModel.copy( + selectedSubmissionAttemptId = 1, + assignment = DataResult.Success(Assignment()), + rootSubmission = DataResult.Success( + Submission(submissionHistory = listOf(Submission(attempt = 1))) + ) + ) + ) + submissionDetailsRenderPage.versionSpinner.assertGone() + } + + @Test + fun showsVersionSpinnerForMultipleSubmission() { + val firstSubmission = Submission(attempt = 1) + val secondSubmission = Submission(attempt = 2) + loadPageWithModel( + baseModel.copy( + selectedSubmissionAttemptId = 1, + assignment = DataResult.Success(Assignment()), + rootSubmission = DataResult.Success( + Submission(submissionHistory = listOf(firstSubmission, secondSubmission)) + ) + ) + ) + submissionDetailsRenderPage.versionSpinner.assertVisible() + } + + @Test + fun spinnerShowsCorrectSelectedSubmission() { + val firstSubmission = Submission( + attempt = 1, + submittedAt = Calendar.getInstance().apply { set(2050, 0, 30, 23, 59, 0) }.time + ) + val secondSubmission = Submission( + attempt = 2, + submittedAt = Calendar.getInstance().apply { set(2050, 0, 31, 23, 59, 0) }.time + ) + loadPageWithModel( + baseModel.copy( + selectedSubmissionAttemptId = 2, + assignment = DataResult.Success(Assignment()), + rootSubmission = DataResult.Success( + Submission(submissionHistory = listOf(firstSubmission, secondSubmission)) + ) + ) + ) + submissionDetailsRenderPage.assertSpinnerMatchesText("Jan 31 at 11:59 PM") + } + + @Test + fun clickingSpinnerShowsSubmissionVersions() { + val firstSubmission = Submission( + attempt = 1, + submittedAt = Calendar.getInstance().apply { set(2050, 0, 30, 23, 59, 0) }.time + ) + val secondSubmission = Submission( + attempt = 2, + submittedAt = Calendar.getInstance().apply { set(2050, 0, 31, 23, 59, 0) }.time + ) + loadPageWithModel( + baseModel.copy( + selectedSubmissionAttemptId = 2, + assignment = DataResult.Success(Assignment()), + rootSubmission = DataResult.Success( + Submission(submissionHistory = listOf(firstSubmission, secondSubmission)) + ) + ) + ) + submissionDetailsRenderPage.versionSpinner.click() + submissionDetailsRenderPage.assertSpinnerDropdownItemHasText(0, "Jan 31 at 11:59 PM") + submissionDetailsRenderPage.assertSpinnerDropdownItemHasText(1, "Jan 30 at 11:59 PM") + } + + @Test + fun tappingSelectedTabOpensDrawer() { + loadPageWithModel( + baseModel.copy( + selectedSubmissionAttemptId = 1, + assignment = DataResult.Success(Assignment()), + rootSubmission = DataResult.Success( + Submission(submissionHistory = listOf(Submission(attempt = 1))) + ) + ) + ) + submissionDetailsRenderPage.clickTab("COMMENTS") + submissionDetailsRenderPage.assertDisplaysDrawerContent() + } + + @Test + fun tappingUnselectedTabOpensDrawer() { + loadPageWithModel( + baseModel.copy( + selectedSubmissionAttemptId = 1, + assignment = DataResult.Success(Assignment()), + rootSubmission = DataResult.Success( + Submission(submissionHistory = listOf(Submission(attempt = 1))) + ) + ) + ) + submissionDetailsRenderPage.clickTab("FILES") + submissionDetailsRenderPage.assertDisplaysDrawerContent() + } + + @Test + fun swipingOnTabLayoutOpensDrawer() { + loadPageWithModel( + baseModel.copy( + selectedSubmissionAttemptId = 1, + assignment = DataResult.Success(Assignment()), + rootSubmission = DataResult.Success( + Submission(submissionHistory = listOf(Submission(attempt = 1))) + ) + ) + ) + submissionDetailsRenderPage.swipeDrawerTo(GeneralLocation.CENTER) + submissionDetailsRenderPage.assertDisplaysDrawerContent() + } + + @Test + fun swipingGauntlet() { + loadPageWithModel( + baseModel.copy( + selectedSubmissionAttemptId = 1, + assignment = DataResult.Success(Assignment()), + rootSubmission = DataResult.Success( + Submission(submissionHistory = listOf(Submission(attempt = 1))) + ) + ) + ) + submissionDetailsRenderPage.swipeDrawerTo(GeneralLocation.CENTER) + submissionDetailsRenderPage.swipeDrawerTo(GeneralLocation.TOP_CENTER) + submissionDetailsRenderPage.swipeDrawerTo(GeneralLocation.CENTER) + submissionDetailsRenderPage.swipeDrawerTo(GeneralLocation.BOTTOM_CENTER) + submissionDetailsRenderPage.swipeDrawerTo(GeneralLocation.TOP_CENTER) + submissionDetailsRenderPage.swipeDrawerTo(GeneralLocation.BOTTOM_CENTER) + submissionDetailsRenderPage.swipeDrawerTo(GeneralLocation.CENTER) + submissionDetailsRenderPage.assertDisplaysDrawerContent() + } + + @Test + fun updatesDrawerHeightOnOrientationChangeToLandscape() { + loadPageWithModel( + baseModel.copy( + selectedSubmissionAttemptId = 1, + assignment = DataResult.Success(Assignment()), + rootSubmission = DataResult.Success( + Submission(submissionHistory = listOf(Submission(attempt = 1))) + ) + ) + ) + submissionDetailsRenderPage.swipeDrawerTo(GeneralLocation.CENTER) + activityRule.activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + submissionDetailsRenderPage.assertDisplaysDrawerContent() + } + + @Test + fun updatesDrawerHeightOnOrientationChangeToPortrait() { + loadPageWithModel( + baseModel.copy( + selectedSubmissionAttemptId = 1, + assignment = DataResult.Success(Assignment()), + rootSubmission = DataResult.Success( + Submission(submissionHistory = listOf(Submission(attempt = 1))) + ) + ) + ) + submissionDetailsRenderPage.swipeDrawerTo(GeneralLocation.CENTER) + activityRule.activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + submissionDetailsRenderPage.assertDisplaysDrawerContent() + } + + private fun loadPageWithModel(model: SubmissionDetailsModel) { + val emptyEffectRunner = object : WorkRunner { + override fun dispose() = Unit + override fun post(runnable: Runnable) = Unit + } + val route = SubmissionDetailsFragment.makeRoute(model.canvasContext, model.assignmentId) + val fragment = SubmissionDetailsFragment.newInstance(route)!!.apply { + overrideInitModel = model + loopMod = { it.effectRunner { emptyEffectRunner } } + } + activityRule.activity.loadFragment(fragment) + } + +} diff --git a/apps/student/src/main/java/com/instructure/student/fragment/AssignmentFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/AssignmentFragment.kt index fe360dcaf5..c30ad25430 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/AssignmentFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/AssignmentFragment.kt @@ -57,7 +57,7 @@ import java.lang.ref.WeakReference import java.text.DateFormat import java.util.* -class AssignmentFragment : ParentFragment(), SubmissionDetailsFragment.SubmissionDetailsFragmentCallback, Bookmarkable { +class AssignmentFragment : ParentFragment(), OldSubmissionDetailsFragment.SubmissionDetailsFragmentCallback, Bookmarkable { // Bundle Args private var canvasContext: CanvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT) @@ -79,10 +79,10 @@ class AssignmentFragment : ParentFragment(), SubmissionDetailsFragment.Submissio null } else fragmentPagerAdapter!!.getRegisteredFragment(ASSIGNMENT_TAB_DETAILS) as OldAssignmentDetailsFragment? - private val submissionDetailsFragment: SubmissionDetailsFragment? + private val submissionDetailsFragment: OldSubmissionDetailsFragment? get() = if (fragmentPagerAdapter == null) { null - } else fragmentPagerAdapter!!.getRegisteredFragment(ASSIGNMENT_TAB_SUBMISSION) as SubmissionDetailsFragment? + } else fragmentPagerAdapter!!.getRegisteredFragment(ASSIGNMENT_TAB_SUBMISSION) as OldSubmissionDetailsFragment? private val assignmentCallback = object : StatusCallback() { override fun onResponse(response: Response, linkHeaders: LinkHeaders, type: ApiType) { @@ -224,7 +224,7 @@ class AssignmentFragment : ParentFragment(), SubmissionDetailsFragment.Submissio .forEach { when (it) { is OldAssignmentDetailsFragment -> it.setupAssignment(assignment) - is SubmissionDetailsFragment -> { + is OldSubmissionDetailsFragment -> { it.setAssignmentFragment(WeakReference(this)) it.setAssignment(assignment, isWithinAnotherCallback, isCached) it.setSubmissionDetailsFragmentCallback(this) @@ -321,7 +321,8 @@ class AssignmentFragment : ParentFragment(), SubmissionDetailsFragment.Submissio override fun getItem(position: Int): Fragment? { return when (position) { ASSIGNMENT_TAB_DETAILS -> OldAssignmentDetailsFragment.newInstance(OldAssignmentDetailsFragment.makeRoute(canvasContext)) - ASSIGNMENT_TAB_SUBMISSION -> SubmissionDetailsFragment.newInstance(SubmissionDetailsFragment.makeRoute(canvasContext)) + ASSIGNMENT_TAB_SUBMISSION -> OldSubmissionDetailsFragment.newInstance( + OldSubmissionDetailsFragment.makeRoute(canvasContext)) ASSIGNMENT_TAB_GRADE -> RubricFragment.newInstance(RubricFragment.makeRoute(canvasContext)) else -> OldAssignmentDetailsFragment.newInstance(OldAssignmentDetailsFragment.makeRoute(canvasContext)) } @@ -332,7 +333,7 @@ class AssignmentFragment : ParentFragment(), SubmissionDetailsFragment.Submissio override fun getPageTitle(position: Int): CharSequence { return when (position) { ASSIGNMENT_TAB_DETAILS -> if (isLocked) getString(R.string.assignmentLocked) else getString(OldAssignmentDetailsFragment.tabTitle) - ASSIGNMENT_TAB_SUBMISSION -> getString(SubmissionDetailsFragment.getTabTitle()) + ASSIGNMENT_TAB_SUBMISSION -> getString(OldSubmissionDetailsFragment.getTabTitle()) ASSIGNMENT_TAB_GRADE -> getString(RubricFragment.tabTitle) else -> getString(OldAssignmentDetailsFragment.tabTitle) } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/SubmissionDetailsFragment.java b/apps/student/src/main/java/com/instructure/student/fragment/OldSubmissionDetailsFragment.java similarity index 99% rename from apps/student/src/main/java/com/instructure/student/fragment/SubmissionDetailsFragment.java rename to apps/student/src/main/java/com/instructure/student/fragment/OldSubmissionDetailsFragment.java index ac83389057..ce8bc98ad5 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/SubmissionDetailsFragment.java +++ b/apps/student/src/main/java/com/instructure/student/fragment/OldSubmissionDetailsFragment.java @@ -134,7 +134,7 @@ import retrofit2.Response; @PageView(url = "{canvasContext}/assignments/{assignmentId}/submissions") -public class SubmissionDetailsFragment extends ParentFragment { +public class OldSubmissionDetailsFragment extends ParentFragment { public interface SubmissionDetailsFragmentCallback { void updateSubmissionDate(Date date); @@ -278,7 +278,7 @@ public void onStop() { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View rootView = getLayoutInflater().inflate(R.layout.fragment_submission_details, container, false); + View rootView = getLayoutInflater().inflate(R.layout.fragment_old_submission_details, container, false); setupViews(rootView); return rootView; } @@ -1615,10 +1615,10 @@ public static boolean validRoute(Route route){ } @Nullable - public static SubmissionDetailsFragment newInstance(Route route) { + public static OldSubmissionDetailsFragment newInstance(Route route) { if(!validRoute(route)) return null; - SubmissionDetailsFragment fragment = new SubmissionDetailsFragment(); + OldSubmissionDetailsFragment fragment = new OldSubmissionDetailsFragment(); fragment.canvasContext = route.getArguments().getParcelable(Const.CANVAS_CONTEXT); fragment.setArguments(route.getArguments()); return fragment; @@ -1627,6 +1627,6 @@ public static SubmissionDetailsFragment newInstance(Route route) { public static Route makeRoute(CanvasContext canvasContext) { Bundle args = new Bundle(); args.putParcelable(Const.CANVAS_CONTEXT, canvasContext); - return new Route(null, SubmissionDetailsFragment.class, canvasContext, args); + return new Route(null, OldSubmissionDetailsFragment.class, canvasContext, args); } } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsModels.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsModels.kt index 07be888f4f..24994ae2ce 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsModels.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsModels.kt @@ -25,38 +25,42 @@ import com.instructure.canvasapi2.models.Submission import com.instructure.canvasapi2.utils.DataResult sealed class SubmissionDetailsEvent { - data class SubmissionClicked(val submissionId: Long) : SubmissionDetailsEvent() - data class DataLoaded(val assignment: DataResult, val rootSubmission: DataResult) : SubmissionDetailsEvent() + object RefreshRequested : SubmissionDetailsEvent() + data class SubmissionClicked(val submissionAttemptId: Long, val attachmentId: Int = 0) : SubmissionDetailsEvent() + data class DataLoaded(val assignment: DataResult, val rootSubmission: DataResult) : + SubmissionDetailsEvent() } sealed class SubmissionDetailsEffect { data class LoadData(val courseId: Long, val assignmentId: Long) : SubmissionDetailsEffect() - data class ShowSubmissionContentType(val submissionContentType: SubmissionDetailsContentType) : SubmissionDetailsEffect() + data class ShowSubmissionContentType(val submissionContentType: SubmissionDetailsContentType) : + SubmissionDetailsEffect() } data class SubmissionDetailsModel( - val isLoading: Boolean = false, - val canvasContext: CanvasContext, - val assignmentId: Long, - val submissionContentType: SubmissionDetailsContentType = SubmissionDetailsContentType.NoneContent, - val selectedSubmissionId: Long? = null, - val assignment: DataResult? = null, - val rootSubmission: DataResult? = null + val isLoading: Boolean = false, + val canvasContext: CanvasContext, + val assignmentId: Long, + val selectedSubmissionAttemptId: Long? = null, + val selectedAttachmentId: Long? = null, + val assignment: DataResult? = null, + val rootSubmission: DataResult? = null ) sealed class SubmissionDetailsContentType { data class QuizContent( - val courseId: Long, - val assignmentId: Long, - val studentId: Long, - val url: String, - val pendingReview: Boolean) : SubmissionDetailsContentType() + val courseId: Long, + val assignmentId: Long, + val studentId: Long, + val url: String, + val pendingReview: Boolean + ) : SubmissionDetailsContentType() data class MediaContent( - val uri: Uri, - val contentType: String?, - val thumbnailUrl: String?, - val displayName: String? + val uri: Uri, + val contentType: String?, + val thumbnailUrl: String?, + val displayName: String? ) : SubmissionDetailsContentType() object NoSubmissionContent : SubmissionDetailsContentType() diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsPresenter.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsPresenter.kt new file mode 100644 index 0000000000..2c8256425e --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsPresenter.kt @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2019 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.mobius.assignmentDetails.submissionDetails + +import android.content.Context +import com.instructure.canvasapi2.utils.DateHelper +import com.instructure.student.R +import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionDetailsTabData +import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionDetailsViewState +import com.instructure.student.mobius.common.ui.Presenter + + +object SubmissionDetailsPresenter : Presenter { + override fun present(model: SubmissionDetailsModel, context: Context): SubmissionDetailsViewState { + if (model.isLoading) return SubmissionDetailsViewState.Loading + if (model.assignment?.isSuccess != true || model.rootSubmission?.isSuccess != true) return SubmissionDetailsViewState.Error + + val rootSubmission = model.rootSubmission.dataOrThrow + val assignment = model.assignment.dataOrThrow + + val atSeparator = context.getString(R.string.at) + + val validSubmissions = rootSubmission.submissionHistory + .filterNotNull() + .sortedByDescending { it.submittedAt } + + val selectedSubmission = validSubmissions.first { it.attempt == model.selectedSubmissionAttemptId } + + val submissionVersions: List> = validSubmissions + .map { + val formattedDate = DateHelper.getMonthDayAtTime(context, it.submittedAt, atSeparator) ?: "" + it.attempt to formattedDate + } + + + val selectedVersionIdx = submissionVersions + .indexOfFirst { it.first == model.selectedSubmissionAttemptId } + .coerceAtLeast(0) + + val tabData = mutableListOf() + + // Comments tab + tabData += SubmissionDetailsTabData.CommentData( + name = context.getString(R.string.comments), + assignmentId = model.assignmentId + ) + + // Files tab + with (selectedSubmission.attachments) { + val name = if (isEmpty()) { + context.getString(R.string.files) + } else { + context.getString(R.string.submissionDetailsFileTabName, size) + } + tabData += SubmissionDetailsTabData.FileData( + name = name, + files = this, + selectedFileId = model.selectedAttachmentId ?: 0 + ) + } + + // Grade/Rubric tab + if (rootSubmission.isGraded || rootSubmission.rubricAssessment.isNotEmpty()) { + val name = if (rootSubmission.rubricAssessment.isNotEmpty()) { + context.getString(R.string.rubric) + } else { + context.getString(R.string.grade) + } + tabData += SubmissionDetailsTabData.GradeData( + name = name, + assignment = assignment, + submission = rootSubmission + ) + } + + return SubmissionDetailsViewState.Loaded( + showVersionsSpinner = submissionVersions.size > 1, + selectedVersionSpinnerIndex = selectedVersionIdx, + submissionVersions = submissionVersions, + tabData = tabData + ) + } +} diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsUpdate.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsUpdate.kt index f04356e87d..f84fd4cc39 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsUpdate.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsUpdate.kt @@ -39,18 +39,27 @@ class SubmissionDetailsUpdate : UpdateInit { return when (event) { + SubmissionDetailsEvent.RefreshRequested -> { + Next.next( + model.copy(isLoading = true), + setOf(SubmissionDetailsEffect.LoadData(model.canvasContext.id, model.assignmentId))) + } is SubmissionDetailsEvent.SubmissionClicked -> { - val submissionType = getSubmissionContentType( - model.rootSubmission?.dataOrNull?.submissionHistory?.find { it?.id == event.submissionId }, + if (event.submissionAttemptId == model.selectedSubmissionAttemptId) { + Next.noChange() + } else { + val submissionType = getSubmissionContentType( + model.rootSubmission?.dataOrNull?.submissionHistory?.find { it?.id == event.submissionAttemptId }, model.assignment?.dataOrNull, model.canvasContext, - model.assignmentId) - Next.next( + model.assignmentId + ) + Next.next( model.copy( - selectedSubmissionId = event.submissionId, - submissionContentType = submissionType + selectedSubmissionAttemptId = event.submissionAttemptId ), setOf(SubmissionDetailsEffect.ShowSubmissionContentType(submissionType)) - ) + ) + } } is SubmissionDetailsEvent.DataLoaded -> { val submissionType = getSubmissionContentType( @@ -63,8 +72,7 @@ class SubmissionDetailsUpdate : UpdateInit SubmissionDetailsContentType.OtherAttachmentContent(attachment) } } -} \ No newline at end of file +} diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/ui/SubmissionDetailsDrawerPagerAdapter.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/ui/SubmissionDetailsDrawerPagerAdapter.kt new file mode 100644 index 0000000000..533d104ed2 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/ui/SubmissionDetailsDrawerPagerAdapter.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2019 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.mobius.assignmentDetails.submissionDetails.ui + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentStatePagerAdapter +import androidx.viewpager.widget.PagerAdapter + +class SubmissionDetailsDrawerPagerAdapter(fragmentManager: FragmentManager) : FragmentStatePagerAdapter(fragmentManager) { + + var tabData: List = emptyList() + set(value) { + if (field.size != value.size) { + cachedFragments = Array(value.size) { null } + } else { + for (idx in cachedFragments.indices) { + if (field[idx] != value[idx]) cachedFragments[idx] = null + } + } + field = value + } + + private var cachedFragments: Array = emptyArray() + + override fun getItem(position: Int): Fragment { + cachedFragments[position] + var cachedFragment = cachedFragments[position] + if (cachedFragment == null) { + val tab = tabData[position] + cachedFragment = PlaceholderFragment().apply { + typeName = tab::class.java.simpleName.substringAfterLast('.') + typeContents = tab.toString() + } + cachedFragments[position] = cachedFragment + } + return cachedFragment + } + + override fun getItemPosition(`object`: Any): Int { + val fragment: Fragment = `object` as? Fragment ?: return PagerAdapter.POSITION_UNCHANGED + val idx = cachedFragments.indexOf(fragment) + return idx.takeIf { it >= 0 } ?: PagerAdapter.POSITION_NONE + } + + override fun getPageTitle(position: Int) = tabData[position].tabName + + override fun getCount() = tabData.size +} diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/ui/SubmissionDetailsFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/ui/SubmissionDetailsFragment.kt new file mode 100644 index 0000000000..598e25ef57 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/ui/SubmissionDetailsFragment.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2019 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.mobius.assignmentDetails.submissionDetails.ui + +import android.view.LayoutInflater +import android.view.ViewGroup +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.utils.pageview.PageView +import com.instructure.canvasapi2.utils.pageview.PageViewUrlParam +import com.instructure.interactions.router.Route +import com.instructure.interactions.router.RouterParams +import com.instructure.pandautils.utils.* +import com.instructure.student.mobius.assignmentDetails.submissionDetails.* +import com.instructure.student.mobius.common.ui.MobiusFragment + +@PageView(url = "{canvasContext}/assignments/{assignmentId}/submissions") +class SubmissionDetailsFragment : + MobiusFragment() { + + val canvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT) + + @get:PageViewUrlParam(name = "assignmentId") + val assignmentId by LongArg(key = Const.ASSIGNMENT_ID) + + override fun makeEffectHandler() = SubmissionDetailsEffectHandler() + + override fun makeUpdate() = SubmissionDetailsUpdate() + + override fun makeView(inflater: LayoutInflater, parent: ViewGroup) = + SubmissionDetailsView(inflater, parent, canvasContext, childFragmentManager) + + override fun makePresenter() = SubmissionDetailsPresenter + + override fun makeInitModel() = SubmissionDetailsModel(canvasContext = canvasContext, assignmentId = assignmentId) + + companion object { + + @JvmStatic + fun makeRoute(course: CanvasContext, assignmentId: Long): Route { + val bundle = course.makeBundle { putLong(Const.ASSIGNMENT_ID, assignmentId) } + return Route(null, SubmissionDetailsFragment::class.java, course, bundle) + } + + @JvmStatic + fun validRoute(route: Route): Boolean { + return route.canvasContext is Course && + (route.arguments.containsKey(Const.ASSIGNMENT_ID) || + route.paramsHash.containsKey(RouterParams.ASSIGNMENT_ID)) + } + + @JvmStatic + fun newInstance(route: Route): SubmissionDetailsFragment? { + if (!validRoute(route)) return null + + // If routed from a URL, set the bundle's assignment ID from the url value + if (route.paramsHash.containsKey(RouterParams.ASSIGNMENT_ID)) { + val assignmentId = route.paramsHash[RouterParams.ASSIGNMENT_ID]?.toLong() ?: -1 + route.arguments.putLong(Const.ASSIGNMENT_ID, assignmentId) + } + + return SubmissionDetailsFragment().withArgs(route.arguments) + } + + } + +} diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/ui/SubmissionDetailsView.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/ui/SubmissionDetailsView.kt index 27b6370d82..331edb77af 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/ui/SubmissionDetailsView.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/ui/SubmissionDetailsView.kt @@ -17,34 +17,197 @@ package com.instructure.student.mobius.assignmentDetails.submissionDetails.ui +import android.annotation.SuppressLint +import android.app.Activity +import android.os.Bundle +import android.view.Gravity import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.TextView +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import com.google.android.material.tabs.TabLayout +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.pandautils.utils.* +import com.instructure.student.R import com.instructure.student.mobius.assignmentDetails.submissionDetails.SubmissionDetailsContentType import com.instructure.student.mobius.assignmentDetails.submissionDetails.SubmissionDetailsEvent import com.instructure.student.mobius.common.ui.MobiusView +import com.sothree.slidinguppanel.SlidingUpPanelLayout import com.spotify.mobius.functions.Consumer +import kotlinx.android.synthetic.main.fragment_submissiont_details.* -class SubmissionDetailsView(layoutInflater: LayoutInflater, parent: ViewGroup) : - MobiusView(0, layoutInflater, parent) { +class SubmissionDetailsView( + layoutInflater: LayoutInflater, + parent: ViewGroup, + private val canvasContext: CanvasContext, + private val fragmentManager: FragmentManager +) : MobiusView( + R.layout.fragment_submissiont_details, + layoutInflater, + parent +) { + private var consumer: Consumer? = null + private var drawerPagerAdapter = SubmissionDetailsDrawerPagerAdapter(fragmentManager) - override fun applyTheme() { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + /* Tab selection listener for the drawer ViewPager */ + private val drawerTabLayoutListener = object : TabLayout.OnTabSelectedListener { + override fun onTabUnselected(tab: TabLayout.Tab?) = Unit + override fun onTabReselected(tab: TabLayout.Tab?) = onTabSelected(tab) + override fun onTabSelected(tab: TabLayout.Tab?) { + if (slidingUpPanelLayout?.panelState == SlidingUpPanelLayout.PanelState.COLLAPSED) { + slidingUpPanelLayout?.panelState = SlidingUpPanelLayout.PanelState.ANCHORED + } + } } - override fun onDispose() { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + init { + toolbar.setupAsBackButton { (context as? Activity)?.onBackPressed() } + retryButton.onClick { consumer?.accept(SubmissionDetailsEvent.RefreshRequested) } + drawerViewPager.offscreenPageLimit = 3 + drawerViewPager.adapter = drawerPagerAdapter + drawerTabLayout.setupWithViewPager(drawerViewPager) + configureSlidingPanelHeight() + } + + private fun configureSlidingPanelHeight() { + /* Adjusts the panel content height based on the position of the sliding portion of the view, but only if + * it is at (or has passed) the anchor point. */ + slidingUpPanelLayout.addPanelSlideListener(object : SlidingUpPanelLayout.PanelSlideListener { + override fun onPanelStateChanged( + panel: View?, + previousState: SlidingUpPanelLayout.PanelState?, + newState: SlidingUpPanelLayout.PanelState? + ) = Unit + + override fun onPanelSlide(panel: View?, offset: Float) { + val maxHeight = contentWrapper.height + if (offset < ANCHOR_POINT || maxHeight == 0) return + val adjustedHeight = Math.abs(maxHeight * offset) + drawerViewPager.layoutParams?.height = adjustedHeight.toInt() + drawerViewPager.requestLayout() + } + }) + + /* Listens for layout changes on the content and adjusts the panel content height accordingly. This ensures we + * use the correct height for initial layout and after orientation changes. */ + contentWrapper.addOnLayoutChangeListener { _, _, top, _, bottom, _, oldTop, _, oldBottom -> + val oldHeight = oldBottom - oldTop + val newHeight = bottom - top + if (oldHeight != newHeight) { + contentWrapper.post { + val slideOffset = when (slidingUpPanelLayout.panelState) { + SlidingUpPanelLayout.PanelState.EXPANDED -> 1f + else -> ANCHOR_POINT + } + drawerViewPager.layoutParams?.height = (newHeight * slideOffset).toInt() + drawerViewPager.requestLayout() + } + } + } + } + + override fun applyTheme() { + ViewStyler.themeToolbar(context as Activity, toolbar, canvasContext) } override fun onConnect(output: Consumer) { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + consumer = output + } + + override fun onDispose() { + consumer = null } override fun render(state: SubmissionDetailsViewState) { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + // Reset visibilities + errorContainer.setGone() + slidingUpPanelLayout.setGone() + loadingView.setGone() + + when (state) { + SubmissionDetailsViewState.Error -> errorContainer.setVisible() + SubmissionDetailsViewState.Loading -> loadingView.setVisible() + is SubmissionDetailsViewState.Loaded -> renderLoadedState(state) + } + } + + private fun renderLoadedState(state: SubmissionDetailsViewState.Loaded) { + slidingUpPanelLayout.setVisible() + submissionVersionsSpinner.setVisible(state.showVersionsSpinner) + setupSubmissionVersionSpinner(state.submissionVersions, state.selectedVersionSpinnerIndex) + updateDrawerPager(state.tabData) + } + + private fun updateDrawerPager(tabData: List) { + /* Updating the pager adapter's data can cause the current tab to be reselected, erroneously causing the drawer + to open up to the anchor point if it was previously closed. As a workaround we remove the tab selection + listener temporarily, and then restore it after updating the adapter */ + drawerTabLayout.removeOnTabSelectedListener(drawerTabLayoutListener) + + // Update adapter data + drawerPagerAdapter.tabData = tabData + drawerPagerAdapter.notifyDataSetChanged() + + // Restore tab selection listener + drawerTabLayout.addOnTabSelectedListener(drawerTabLayoutListener) + } + + + private fun setupSubmissionVersionSpinner(submissions: List>, selectedIdx: Int) { + submissionVersionsSpinner.adapter = + ArrayAdapter(context, R.layout.spinner_submission_versions, submissions.map { it.second }).apply { + setDropDownViewResource(R.layout.spinner_submission_versions_dropdown) + } + submissionVersionsSpinner.setSelection(selectedIdx, false) + submissionVersionsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onNothingSelected(parent: AdapterView<*>?) = Unit + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + val submissionId = submissions[position].first + consumer?.accept(SubmissionDetailsEvent.SubmissionClicked(submissionId)) + } + } } fun showSubmissionContent(type: SubmissionDetailsContentType) { - TODO() + fragmentManager.beginTransaction().apply { + replace(R.id.submissionContent, getFragmentForContent(type)) + commitAllowingStateLoss() + } + } + + private fun getFragmentForContent(type: SubmissionDetailsContentType): Fragment { + // TODO + return PlaceholderFragment().apply { + typeName = type::class.java.simpleName + typeContents = type.toString() + } } -} \ No newline at end of file + + companion object { + private const val ANCHOR_POINT = 0.5f + } +} + + +class PlaceholderFragment : Fragment() { + + var typeName: String = "" + var typeContents: String = "" + + @SuppressLint("SetTextI18n") + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val view = TextView(context).apply { + text = "PLACEHOLDER\n$typeName\n\n$typeContents" + gravity = Gravity.CENTER + } + view.layoutParams = + ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) + return view + } + +} diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/ui/SubmissionDetailsViewState.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/ui/SubmissionDetailsViewState.kt index b0d29362ff..bead0555e5 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/ui/SubmissionDetailsViewState.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/ui/SubmissionDetailsViewState.kt @@ -17,4 +17,34 @@ package com.instructure.student.mobius.assignmentDetails.submissionDetails.ui -sealed class SubmissionDetailsViewState +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.Attachment +import com.instructure.canvasapi2.models.Submission + +sealed class SubmissionDetailsViewState { + object Error : SubmissionDetailsViewState() + object Loading : SubmissionDetailsViewState() + data class Loaded( + val showVersionsSpinner: Boolean, + val selectedVersionSpinnerIndex: Int, + val submissionVersions: List>, + val tabData : List + ): SubmissionDetailsViewState() +} + +sealed class SubmissionDetailsTabData(val tabName: String) { + data class CommentData( + val name: String, + val assignmentId: Long + ) : SubmissionDetailsTabData(name) + data class FileData( + val name: String, + val files: List, + val selectedFileId: Long + ) : SubmissionDetailsTabData(name) + data class GradeData( + val name: String, + val assignment: Assignment, + val submission: Submission + ) : SubmissionDetailsTabData(name) +} diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/AssignmentDetailsView.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/AssignmentDetailsView.kt index 980d6c8995..bb0ca1244b 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/AssignmentDetailsView.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/AssignmentDetailsView.kt @@ -33,6 +33,7 @@ import com.instructure.student.R import com.instructure.student.activity.InternalWebViewActivity import com.instructure.student.fragment.InternalWebviewFragment import com.instructure.student.mobius.assignmentDetails.AssignmentDetailsEvent +import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionDetailsFragment import com.instructure.student.mobius.common.ui.MobiusView import com.instructure.student.router.RouteMatcher import com.spotify.mobius.functions.Consumer @@ -156,8 +157,7 @@ class AssignmentDetailsView( } fun showSubmissionView(assignmentId: Long, course: Course) { - // TODO - context.toast("Route to submission page") + RouteMatcher.route(context, SubmissionDetailsFragment.makeRoute(course, assignmentId)) } fun showUploadStatusView(assignmentId: Long, course: Course) { diff --git a/apps/student/src/main/java/com/instructure/student/mobius/common/MobiusKotlinUtils.kt b/apps/student/src/main/java/com/instructure/student/mobius/common/MobiusKotlinUtils.kt index fa956eb95c..06630efadb 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/common/MobiusKotlinUtils.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/common/MobiusKotlinUtils.kt @@ -28,7 +28,17 @@ fun Connectable.contraMap( return Connectable { output -> val delegateConnection = connect(output) object : Connection { - override fun accept(value: J) = delegateConnection.accept(mapper(value, context)) + var lastValue: I? = null + + override fun accept(value: J) { + val mappedValue: I = mapper(value, context) + // Only push value if it has changed (prevents duplicate renders) + if (mappedValue != lastValue) { + lastValue = mappedValue + delegateConnection.accept(mappedValue) + } + } + override fun dispose() = delegateConnection.dispose() } } diff --git a/apps/student/src/main/java/com/instructure/student/router/RouteResolver.kt b/apps/student/src/main/java/com/instructure/student/router/RouteResolver.kt index 0b61035d84..4f8ed2bec3 100644 --- a/apps/student/src/main/java/com/instructure/student/router/RouteResolver.kt +++ b/apps/student/src/main/java/com/instructure/student/router/RouteResolver.kt @@ -6,6 +6,7 @@ import com.instructure.interactions.router.Route import com.instructure.pandautils.utils.Const import com.instructure.student.features.files.search.FileSearchFragment import com.instructure.student.fragment.* +import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionDetailsFragment import com.instructure.student.mobius.assignmentDetails.ui.AssignmentDetailsFragment object RouteResolver { @@ -94,6 +95,7 @@ object RouteResolver { cls.isA() -> CourseModuleProgressionFragment.newInstance(route) cls.isA() -> AssignmentFragment.newInstance(route) cls.isA() -> AssignmentDetailsFragment.newInstance(route) + cls.isA() -> SubmissionDetailsFragment.newInstance(route) cls.isA() -> DiscussionListFragment.newInstance(route) cls.isA() -> DiscussionDetailsFragment.newInstance(route) cls.isA() -> DiscussionsReplyFragment.newInstance(route) diff --git a/apps/student/src/main/res/drawable/handle_bar_drag_view.xml b/apps/student/src/main/res/drawable/handle_bar_drag_view.xml new file mode 100644 index 0000000000..c43fa04ae2 --- /dev/null +++ b/apps/student/src/main/res/drawable/handle_bar_drag_view.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/apps/student/src/main/res/layout/fragment_assignment_details.xml b/apps/student/src/main/res/layout/fragment_assignment_details.xml index 3311ab98dd..e5dd005ead 100644 --- a/apps/student/src/main/res/layout/fragment_assignment_details.xml +++ b/apps/student/src/main/res/layout/fragment_assignment_details.xml @@ -463,7 +463,6 @@ android:id="@+id/submitButton" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="16dp" android:background="@color/canvasDefaultAccent" android:text="@string/submitAssignment" android:textAllCaps="false" diff --git a/apps/student/src/main/res/layout/fragment_submission_details.xml b/apps/student/src/main/res/layout/fragment_old_submission_details.xml similarity index 100% rename from apps/student/src/main/res/layout/fragment_submission_details.xml rename to apps/student/src/main/res/layout/fragment_old_submission_details.xml diff --git a/apps/student/src/main/res/layout/fragment_submissiont_details.xml b/apps/student/src/main/res/layout/fragment_submissiont_details.xml new file mode 100644 index 0000000000..83e22f2de3 --- /dev/null +++ b/apps/student/src/main/res/layout/fragment_submissiont_details.xml @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + + + +