diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 3b312839bf2e..000000000000 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index c5b8a0faeacc..df0cd9e5b9da 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -3,6 +3,7 @@ XX.X * Add Remote Preview support for posts and pages. * Post List: Trashed post must now be restored before edit or preview. * All changes to posts and pages will be automatically synced with the server. +* Post List: Unhandled conflict with auto saves are now detected and visible. On post opening, the app will let you choose which version you prefer. * Clicking on "Publish" on a private post sometimes published the post as public 13.2 diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostActionHandler.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostActionHandler.kt index bf6fffeae85e..506dd926fc5c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostActionHandler.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostActionHandler.kt @@ -57,6 +57,7 @@ class PostActionHandler( private val postStore: PostStore, private val postListDialogHelper: PostListDialogHelper, private val doesPostHaveUnhandledConflict: (PostModel) -> Boolean, + private val hasUnhandledAutoSave: (PostModel) -> Boolean, private val triggerPostListAction: (PostListAction) -> Unit, private val triggerPostUploadAction: (PostUploadAction) -> Unit, private val invalidateList: () -> Unit, @@ -170,12 +171,18 @@ class PostActionHandler( } private fun editPostButtonAction(site: SiteModel, post: PostModel) { - // first of all, check whether this post is in Conflicted state. + // first of all, check whether this post is in Conflicted state with a more recent remote version if (doesPostHaveUnhandledConflict.invoke(post)) { postListDialogHelper.showConflictedPostResolutionDialog(post) return } + // Then check if it's in conflicted state with a remote auto-save + if (hasUnhandledAutoSave.invoke(post)) { + postListDialogHelper.showAutoSaveConflictedPostResolutionDialog(post) + return + } + editPost(site, post) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostConflictResolver.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostConflictResolver.kt index d2ab4b136d69..3372fc4ecd47 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostConflictResolver.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostConflictResolver.kt @@ -84,6 +84,10 @@ class PostConflictResolver( return !isFetchingConflictedPost && PostUtils.isPostInConflictWithRemote(post) } + fun hasUnhandledAutoSave(post: PostModel): Boolean { + return PostUtils.isPostInConflictWithAutoSave(post) + } + fun onPostSuccessfullyUpdated() { originalPostCopyForConflictUndo?.id?.let { val updatedPost = getPostByLocalPostId.invoke(it) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListDialogHelper.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListDialogHelper.kt index 2b097c058de9..acc5384ec3ae 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListDialogHelper.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListDialogHelper.kt @@ -10,6 +10,7 @@ private const val CONFIRM_DELETE_POST_DIALOG_TAG = "CONFIRM_DELETE_POST_DIALOG_T private const val CONFIRM_PUBLISH_POST_DIALOG_TAG = "CONFIRM_PUBLISH_POST_DIALOG_TAG" private const val CONFIRM_TRASH_POST_WITH_LOCAL_CHANGES_DIALOG_TAG = "CONFIRM_TRASH_POST_WITH_LOCAL_CHANGES_DIALOG_TAG" private const val CONFIRM_ON_CONFLICT_LOAD_REMOTE_POST_DIALOG_TAG = "CONFIRM_ON_CONFLICT_LOAD_REMOTE_POST_DIALOG_TAG" +private const val CONFIRM_ON_AUTOSAVE_CONFLICT_DIALOG_TAG = "CONFIRM_ON_AUTOSAVE_CONFLICT_DIALOG_TAG" /** * This is a temporary class to make the PostListViewModel more manageable. Please feel free to refactor it any way @@ -24,6 +25,7 @@ class PostListDialogHelper( private var localPostIdForPublishDialog: Int? = null private var localPostIdForTrashPostWithLocalChangesDialog: Int? = null private var localPostIdForConflictResolutionDialog: Int? = null + private var localPostIdForAutoSaveConflictResolutionDialog: Int? = null fun showDeletePostConfirmationDialog(post: PostModel) { // We need network connection to delete a remote post, but not a local draft @@ -84,12 +86,25 @@ class PostListDialogHelper( showDialog.invoke(dialogHolder) } + fun showAutoSaveConflictedPostResolutionDialog(post: PostModel) { + val dialogHolder = DialogHolder( + tag = CONFIRM_ON_AUTOSAVE_CONFLICT_DIALOG_TAG, + title = UiStringRes(R.string.dialog_confirm_autosave_title), + message = UiStringRes(R.string.dialog_confirm_autosave_body), + positiveButton = UiStringRes(R.string.dialog_confirm_autosave_restore_button), + negativeButton = UiStringRes(R.string.dialog_confirm_autosave_dont_restore_button) + ) + localPostIdForAutoSaveConflictResolutionDialog = post.id + showDialog.invoke(dialogHolder) + } + fun onPositiveClickedForBasicDialog( instanceTag: String, trashPostWithLocalChanges: (Int) -> Unit, deletePost: (Int) -> Unit, publishPost: (Int) -> Unit, - updateConflictedPostWithRemoteVersion: (Int) -> Unit + updateConflictedPostWithRemoteVersion: (Int) -> Unit, + editRestoredAutoSavePost: (Int) -> Unit ) { when (instanceTag) { CONFIRM_DELETE_POST_DIALOG_TAG -> localPostIdForDeleteDialog?.let { @@ -109,13 +124,19 @@ class PostListDialogHelper( localPostIdForTrashPostWithLocalChangesDialog = null trashPostWithLocalChanges(it) } + CONFIRM_ON_AUTOSAVE_CONFLICT_DIALOG_TAG -> localPostIdForAutoSaveConflictResolutionDialog?.let { + // open the editor with the restored auto save + localPostIdForAutoSaveConflictResolutionDialog = null + editRestoredAutoSavePost(it) + } else -> throw IllegalArgumentException("Dialog's positive button click is not handled: $instanceTag") } } fun onNegativeClickedForBasicDialog( instanceTag: String, - updateConflictedPostWithLocalVersion: (Int) -> Unit + updateConflictedPostWithLocalVersion: (Int) -> Unit, + editLocalPost: (Int) -> Unit ) { when (instanceTag) { CONFIRM_DELETE_POST_DIALOG_TAG -> localPostIdForDeleteDialog = null @@ -124,20 +145,27 @@ class PostListDialogHelper( CONFIRM_ON_CONFLICT_LOAD_REMOTE_POST_DIALOG_TAG -> localPostIdForConflictResolutionDialog?.let { updateConflictedPostWithLocalVersion(it) } + CONFIRM_ON_AUTOSAVE_CONFLICT_DIALOG_TAG -> localPostIdForAutoSaveConflictResolutionDialog?.let { + // open the editor with the local post (don't use the auto save version) + editLocalPost(it) + } else -> throw IllegalArgumentException("Dialog's negative button click is not handled: $instanceTag") } } fun onDismissByOutsideTouchForBasicDialog( instanceTag: String, - updateConflictedPostWithLocalVersion: (Int) -> Unit + updateConflictedPostWithLocalVersion: (Int) -> Unit, + editLocalPost: (Int) -> Unit ) { - // Cancel and outside touch dismiss works the same way for all, except for conflict resolution dialog, + // Cancel and outside touch dismiss works the same way for all, except for conflict resolution dialogs, // for which tapping outside and actively tapping the "edit local" have different meanings - if (instanceTag != CONFIRM_ON_CONFLICT_LOAD_REMOTE_POST_DIALOG_TAG) { + if (instanceTag != CONFIRM_ON_CONFLICT_LOAD_REMOTE_POST_DIALOG_TAG && + instanceTag != CONFIRM_ON_AUTOSAVE_CONFLICT_DIALOG_TAG) { onNegativeClickedForBasicDialog( instanceTag = instanceTag, - updateConflictedPostWithLocalVersion = updateConflictedPostWithLocalVersion + updateConflictedPostWithLocalVersion = updateConflictedPostWithLocalVersion, + editLocalPost = editLocalPost ) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListMainViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListMainViewModel.kt index 7ecdd459eb02..2c9ec1a51106 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListMainViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListMainViewModel.kt @@ -19,6 +19,7 @@ import org.wordpress.android.analytics.AnalyticsTracker.Stat.POST_LIST_SEARCH_AC import org.wordpress.android.analytics.AnalyticsTracker.Stat.POST_LIST_TAB_CHANGED import org.wordpress.android.fluxc.Dispatcher import org.wordpress.android.fluxc.generated.ListActionBuilder +import org.wordpress.android.fluxc.generated.PostActionBuilder import org.wordpress.android.fluxc.model.LocalOrRemoteId.LocalId import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.model.list.PostListDescriptor @@ -168,6 +169,7 @@ class PostListMainViewModel @Inject constructor( postStore = postStore, postListDialogHelper = postListDialogHelper, doesPostHaveUnhandledConflict = postConflictResolver::doesPostHaveUnhandledConflict, + hasUnhandledAutoSave = postConflictResolver::hasUnhandledAutoSave, triggerPostListAction = { _postListAction.postValue(it) }, triggerPostUploadAction = { _postUploadAction.postValue(it) }, invalidateList = this::invalidateAllLists, @@ -269,6 +271,7 @@ class PostListMainViewModel @Inject constructor( postActionHandler = postActionHandler, getUploadStatus = uploadStatusTracker::getUploadStatus, doesPostHaveUnhandledConflict = postConflictResolver::doesPostHaveUnhandledConflict, + hasAutoSave = postConflictResolver::hasUnhandledAutoSave, postFetcher = postFetcher, getFeaturedImageUrl = featuredImageTracker::getFeaturedImageUrl ) @@ -358,6 +361,28 @@ class PostListMainViewModel @Inject constructor( postActionHandler.handleEditPostResult(data) } + private fun editRestoredAutoSavePost(localPostId: Int) { + val post = postStore.getPostByLocalPostId(localPostId) + if (post != null) { + post.title = post.autoSaveTitle ?: post.title + post.content = post.autoSaveContent ?: post.content + post.excerpt = post.autoSaveExcerpt ?: post.excerpt + dispatcher.dispatch(PostActionBuilder.newUpdatePostAction(post)) + _postListAction.postValue(PostListAction.EditPost(site, post)) + } else { + _snackBarMessage.value = SnackbarMessageHolder(R.string.error_post_does_not_exist) + } + } + + private fun editLocalPost(localPostId: Int) { + val post = postStore.getPostByLocalPostId(localPostId) + if (post != null) { + _postListAction.postValue(PostListAction.EditPost(site, post)) + } else { + _snackBarMessage.value = SnackbarMessageHolder(R.string.error_post_does_not_exist) + } + } + // BasicFragmentDialog Events fun onPositiveClickedForBasicDialog(instanceTag: String) { @@ -366,21 +391,24 @@ class PostListMainViewModel @Inject constructor( trashPostWithLocalChanges = postActionHandler::trashPostWithLocalChanges, deletePost = postActionHandler::deletePost, publishPost = postActionHandler::publishPost, - updateConflictedPostWithRemoteVersion = postConflictResolver::updateConflictedPostWithRemoteVersion + updateConflictedPostWithRemoteVersion = postConflictResolver::updateConflictedPostWithRemoteVersion, + editRestoredAutoSavePost = this::editRestoredAutoSavePost ) } fun onNegativeClickedForBasicDialog(instanceTag: String) { postListDialogHelper.onNegativeClickedForBasicDialog( instanceTag = instanceTag, - updateConflictedPostWithLocalVersion = postConflictResolver::updateConflictedPostWithLocalVersion + updateConflictedPostWithLocalVersion = postConflictResolver::updateConflictedPostWithLocalVersion, + editLocalPost = this::editLocalPost ) } fun onDismissByOutsideTouchForBasicDialog(instanceTag: String) { postListDialogHelper.onDismissByOutsideTouchForBasicDialog( instanceTag = instanceTag, - updateConflictedPostWithLocalVersion = postConflictResolver::updateConflictedPostWithLocalVersion + updateConflictedPostWithLocalVersion = postConflictResolver::updateConflictedPostWithLocalVersion, + editLocalPost = this::editLocalPost ) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUtils.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUtils.java index 884c49d6b18a..b93236bda983 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUtils.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUtils.java @@ -453,6 +453,17 @@ public static boolean isPostInConflictWithRemote(PostModel post) { return !post.getLastModified().equals(post.getRemoteLastModified()) && post.isLocallyChanged(); } + public static boolean isPostInConflictWithAutoSave(PostModel post) { + // TODO: would be great to check if title, content and excerpt are different, + // but we currently don't have them when we fetch the post list + + // Ignore auto-saves in case the post is locally changed. + // This might be changed in the future to show a better conflict UX. + return !post.isLocallyChanged() + // has auto-save + && post.hasUnpublishedRevision(); + } + public static String getConflictedPostCustomStringForDialog(PostModel post) { Context context = WordPress.getContext(); String firstPart = context.getString(R.string.dialog_confirm_load_remote_post_body); diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/posts/PostListItemUiStateHelper.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/posts/PostListItemUiStateHelper.kt index f943c001fc7e..0ffc8839b8df 100644 --- a/WordPress/src/main/java/org/wordpress/android/viewmodel/posts/PostListItemUiStateHelper.kt +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/posts/PostListItemUiStateHelper.kt @@ -69,6 +69,7 @@ class PostListItemUiStateHelper @Inject constructor(private val appPrefsWrapper: post: PostModel, uploadStatus: PostListItemUploadStatus, unhandledConflicts: Boolean, + hasAutoSave: Boolean, capabilitiesToPublish: Boolean, statsSupported: Boolean, featuredImageUrl: String?, @@ -102,14 +103,16 @@ class PostListItemUiStateHelper @Inject constructor(private val appPrefsWrapper: isLocalDraft = post.isLocalDraft, isLocallyChanged = post.isLocallyChanged, uploadUiState = uploadUiState, - hasUnhandledConflicts = unhandledConflicts + hasUnhandledConflicts = unhandledConflicts, + hasAutoSave = hasAutoSave ) val statusesColor = getStatusesColor( postStatus = postStatus, isLocalDraft = post.isLocalDraft, isLocallyChanged = post.isLocallyChanged, uploadUiState = uploadUiState, - hasUnhandledConflicts = unhandledConflicts + hasUnhandledConflicts = unhandledConflicts, + hasAutoSave = hasAutoSave ) val statusesDelimeter = UiStringRes(R.string.multiple_status_label_delimiter) val onSelected = { @@ -204,7 +207,8 @@ class PostListItemUiStateHelper @Inject constructor(private val appPrefsWrapper: isLocalDraft: Boolean, isLocallyChanged: Boolean, uploadUiState: PostUploadUiState, - hasUnhandledConflicts: Boolean + hasUnhandledConflicts: Boolean, + hasAutoSave: Boolean ): List { val labels: MutableList = ArrayList() when { @@ -233,6 +237,7 @@ class PostListItemUiStateHelper @Inject constructor(private val appPrefsWrapper: } } hasUnhandledConflicts -> labels.add(UiStringRes(R.string.local_post_is_conflicted)) + hasAutoSave -> labels.add(UiStringRes(R.string.local_post_autosave_conflict)) } // we want to show either single error/progress label or 0-n info labels. @@ -282,9 +287,10 @@ class PostListItemUiStateHelper @Inject constructor(private val appPrefsWrapper: isLocalDraft: Boolean, isLocallyChanged: Boolean, uploadUiState: PostUploadUiState, - hasUnhandledConflicts: Boolean + hasUnhandledConflicts: Boolean, + hasAutoSave: Boolean ): Int? { - val isError = uploadUiState is PostUploadUiState.UploadFailed || hasUnhandledConflicts + val isError = uploadUiState is PostUploadUiState.UploadFailed || hasUnhandledConflicts || hasAutoSave val isProgressInfo = uploadUiState is UploadingPost || uploadUiState is UploadingMedia || uploadUiState is UploadQueued val isStateInfo = isLocalDraft || isLocallyChanged || postStatus == PRIVATE || postStatus == PENDING || diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/posts/PostListViewModel.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/posts/PostListViewModel.kt index 75ea4c9b906f..ddc1f0dc86b4 100644 --- a/WordPress/src/main/java/org/wordpress/android/viewmodel/posts/PostListViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/posts/PostListViewModel.kt @@ -360,6 +360,7 @@ class PostListViewModel @Inject constructor( post = post, uploadStatus = connector.getUploadStatus(post, connector.site), unhandledConflicts = connector.doesPostHaveUnhandledConflict(post), + hasAutoSave = connector.hasAutoSave(post), capabilitiesToPublish = uploadUtilsWrapper.userCanPublish(connector.site), statsSupported = isStatsSupported, featuredImageUrl = diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/posts/PostListViewModelConnector.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/posts/PostListViewModelConnector.kt index 18d68dc62ccd..682d6207ecf7 100644 --- a/WordPress/src/main/java/org/wordpress/android/viewmodel/posts/PostListViewModelConnector.kt +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/posts/PostListViewModelConnector.kt @@ -11,6 +11,7 @@ class PostListViewModelConnector( val postActionHandler: PostActionHandler, val getUploadStatus: (PostModel, SiteModel) -> PostListItemUploadStatus, val doesPostHaveUnhandledConflict: (PostModel) -> Boolean, + val hasAutoSave: (PostModel) -> Boolean, val postFetcher: PostFetcher, private val getFeaturedImageUrl: (site: SiteModel, featuredImageId: Long) -> String? ) { diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index 2929143e09a2..e5f2246193b4 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -353,6 +353,12 @@ Web version discarded Undo + + More Recent Version Conflict + A more recent version exists. Edit current version or most recent version?\n\n + More recent version + Current version + Local changes Trashing this post will discard local changes, are you sure you want to continue? @@ -1412,6 +1418,7 @@ Version conflict + Unhandled Auto Save Saving… @@ -1421,7 +1428,7 @@ Can\'t preview an empty post Can\'t preview an empty page Can\'t preview an empty draft - + Links are disabled on the preview screen diff --git a/WordPress/src/test/java/org/wordpress/android/viewmodel/posts/PostListItemUiStateHelperTest.kt b/WordPress/src/test/java/org/wordpress/android/viewmodel/posts/PostListItemUiStateHelperTest.kt index fb449d105ffa..f1792e5ff02a 100644 --- a/WordPress/src/test/java/org/wordpress/android/viewmodel/posts/PostListItemUiStateHelperTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/viewmodel/posts/PostListItemUiStateHelperTest.kt @@ -22,8 +22,8 @@ import org.wordpress.android.ui.posts.AuthorFilterSelection.EVERYONE import org.wordpress.android.ui.posts.AuthorFilterSelection.ME import org.wordpress.android.ui.prefs.AppPrefsWrapper import org.wordpress.android.ui.utils.UiString.UiStringRes -import org.wordpress.android.viewmodel.posts.PostListItemAction.MoreItem import org.wordpress.android.ui.utils.UiString.UiStringText +import org.wordpress.android.viewmodel.posts.PostListItemAction.MoreItem import org.wordpress.android.viewmodel.posts.PostListItemType.PostListItemUiState import org.wordpress.android.widgets.PostListButtonType @@ -405,11 +405,18 @@ class PostListItemUiStateHelperTest { assertThat(state.data.statusesColor).isEqualTo(PROGRESS_INFO_COLOR) } + @Test fun `label has error color on version conflict`() { val state = createPostListItemUiState(unhandledConflicts = true) assertThat(state.data.statusesColor).isEqualTo(ERROR_COLOR) } + @Test + fun `label has error color on auto-save conflict`() { + val state = createPostListItemUiState(hasAutoSave = true) + assertThat(state.data.statusesColor).isEqualTo(ERROR_COLOR) + } + @Test fun `private label shown for private posts`() { val state = createPostListItemUiState(post = createPostModel(status = POST_STATE_PRIVATE)) @@ -440,6 +447,12 @@ class PostListItemUiStateHelperTest { assertThat(state.data.statuses).contains(UiStringRes(R.string.local_post_is_conflicted)) } + @Test + fun `unhandled auto-save label shown for posts with existing auto-save`() { + val state = createPostListItemUiState(hasAutoSave = true) + assertThat(state.data.statuses).contains(UiStringRes(R.string.local_post_autosave_conflict)) + } + @Test fun `uploading post label shown when the post is being uploaded`() { val state = createPostListItemUiState(uploadStatus = createUploadStatus(isUploading = true)) @@ -750,6 +763,7 @@ class PostListItemUiStateHelperTest { post: PostModel = PostModel(), uploadStatus: PostListItemUploadStatus = createUploadStatus(), unhandledConflicts: Boolean = false, + hasAutoSave: Boolean = false, capabilitiesToPublish: Boolean = true, statsSupported: Boolean = true, featuredImageUrl: String? = null, @@ -761,6 +775,7 @@ class PostListItemUiStateHelperTest { post = post, uploadStatus = uploadStatus, unhandledConflicts = unhandledConflicts, + hasAutoSave = hasAutoSave, capabilitiesToPublish = capabilitiesToPublish, statsSupported = statsSupported, featuredImageUrl = featuredImageUrl, diff --git a/WordPress/src/test/java/org/wordpress/android/viewmodel/posts/PostListViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/viewmodel/posts/PostListViewModelTest.kt index 353491d4605a..ad3574ad42f3 100644 --- a/WordPress/src/test/java/org/wordpress/android/viewmodel/posts/PostListViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/viewmodel/posts/PostListViewModelTest.kt @@ -109,6 +109,7 @@ class PostListViewModelTest : BaseUnitTest() { postActionHandler = mock(), getUploadStatus = mock(), doesPostHaveUnhandledConflict = mock(), + hasAutoSave = mock(), getFeaturedImageUrl = mock(), postFetcher = mock() ) diff --git a/build.gradle b/build.gradle index 87c294351a70..38f6dfeddbba 100644 --- a/build.gradle +++ b/build.gradle @@ -106,5 +106,5 @@ buildScan { ext { daggerVersion = '2.22.1' - fluxCVersion = 'c91d9539fcdac830382a582e795ed55bcf72a0af' + fluxCVersion = '3b03a7c1dc4f54e6f8ac490af8d8efea0b9ec2f4' }