diff --git a/actions/src/main/kotlin/com/alfresco/content/actions/ActionUploadFiles.kt b/actions/src/main/kotlin/com/alfresco/content/actions/ActionUploadFiles.kt index c18b68acb..512acc254 100644 --- a/actions/src/main/kotlin/com/alfresco/content/actions/ActionUploadFiles.kt +++ b/actions/src/main/kotlin/com/alfresco/content/actions/ActionUploadFiles.kt @@ -39,6 +39,7 @@ data class ActionUploadFiles( } } } + else -> {} } withContext(Dispatchers.IO) { @@ -48,6 +49,7 @@ data class ActionUploadFiles( it, getParentId(entry), uploadServerType = entry.uploadServer, + observerId = entry.observerID, ) } repository.setTotalTransferSize(result.size) diff --git a/actions/src/main/kotlin/com/alfresco/content/actions/ActionUploadMedia.kt b/actions/src/main/kotlin/com/alfresco/content/actions/ActionUploadMedia.kt index 6772275db..26827a175 100644 --- a/actions/src/main/kotlin/com/alfresco/content/actions/ActionUploadMedia.kt +++ b/actions/src/main/kotlin/com/alfresco/content/actions/ActionUploadMedia.kt @@ -36,6 +36,7 @@ data class ActionUploadMedia( } } } + else -> {} } withContext(Dispatchers.IO) { @@ -45,6 +46,7 @@ data class ActionUploadMedia( it, getParentId(entry), uploadServerType = entry.uploadServer, + observerId = entry.observerID, ) } repository.setTotalTransferSize(result.size) diff --git a/common/src/main/kotlin/com/alfresco/content/common/EntryListener.kt b/common/src/main/kotlin/com/alfresco/content/common/EntryListener.kt index d8e27edce..43b4c27a5 100644 --- a/common/src/main/kotlin/com/alfresco/content/common/EntryListener.kt +++ b/common/src/main/kotlin/com/alfresco/content/common/EntryListener.kt @@ -1,6 +1,7 @@ package com.alfresco.content.common import com.alfresco.content.data.ParentEntry +import com.alfresco.content.data.payloads.FieldsData /** * Mark as EntryListener interface @@ -18,4 +19,5 @@ interface EntryListener { fun onProcessStart(entries: List) {} fun onAttachFolder(entry: ParentEntry) {} + fun onAttachFiles(field: FieldsData) {} } diff --git a/data/objectbox-models/default.json b/data/objectbox-models/default.json index f6e247b07..68f720100 100644 --- a/data/objectbox-models/default.json +++ b/data/objectbox-models/default.json @@ -5,7 +5,7 @@ "entities": [ { "id": "1:3882697123829827748", - "lastPropertyId": "32:675229831295035009", + "lastPropertyId": "33:8784310188115192790", "name": "Entry", "properties": [ { @@ -153,6 +153,11 @@ "id": "32:675229831295035009", "name": "canDelete", "type": 1 + }, + { + "id": "33:8784310188115192790", + "name": "observerID", + "type": 9 } ], "relations": [] diff --git a/data/src/main/kotlin/com/alfresco/content/data/AttachFolderSearchData.kt b/data/src/main/kotlin/com/alfresco/content/data/AttachFolderSearchData.kt index 873f05549..026e638a8 100644 --- a/data/src/main/kotlin/com/alfresco/content/data/AttachFolderSearchData.kt +++ b/data/src/main/kotlin/com/alfresco/content/data/AttachFolderSearchData.kt @@ -1,3 +1,6 @@ package com.alfresco.content.data +import com.alfresco.content.data.payloads.FieldsData + data class AttachFolderSearchData(val entry: Entry? = null) +data class AttachFilesData(val field: FieldsData? = null) diff --git a/data/src/main/kotlin/com/alfresco/content/data/Entry.kt b/data/src/main/kotlin/com/alfresco/content/data/Entry.kt index c328d42a5..d7ad89fc3 100644 --- a/data/src/main/kotlin/com/alfresco/content/data/Entry.kt +++ b/data/src/main/kotlin/com/alfresco/content/data/Entry.kt @@ -90,6 +90,7 @@ data class Entry( val uploadServer: UploadServerType = UploadServerType.DEFAULT, val isReadOnly: Boolean = false, var isSelectedForMultiSelection: Boolean = false, + var observerID: String = "", ) : ParentEntry(), Parcelable { val isSynced: Boolean @@ -146,6 +147,7 @@ data class Entry( isFavorite = other.isFavorite, canDelete = other.canDelete, otherId = other.otherId, + observerID = other.observerID, ) enum class Type { @@ -395,7 +397,7 @@ data class Entry( /** * return the ContentEntry obj after converting the data from ContentDataEntry obj */ - fun with(data: ContentDataEntry, parentId: String? = null, uploadServer: UploadServerType): Entry { + fun with(data: ContentDataEntry, parentId: String? = null, uploadServer: UploadServerType, observerID: String = ""): Entry { return Entry( id = data.id?.toString() ?: "", parentId = parentId, @@ -412,6 +414,7 @@ data class Entry( previewStatus = data.previewStatus, thumbnailStatus = data.thumbnailStatus, uploadServer = uploadServer, + observerID = observerID, ) } @@ -432,8 +435,8 @@ data class Entry( /** * return the default Workflow content entry obj */ - fun defaultWorkflowEntry(id: String?): Entry { - return Entry(uploadServer = UploadServerType.UPLOAD_TO_PROCESS, parentId = id) + fun defaultWorkflowEntry(id: String?, fieldId: String = ""): Entry { + return Entry(uploadServer = UploadServerType.UPLOAD_TO_PROCESS, parentId = id, observerID = fieldId) } fun withSelectedEntries(entries: List): Entry { diff --git a/data/src/main/kotlin/com/alfresco/content/data/OfflineRepository.kt b/data/src/main/kotlin/com/alfresco/content/data/OfflineRepository.kt index 5239549e4..a521f9311 100644 --- a/data/src/main/kotlin/com/alfresco/content/data/OfflineRepository.kt +++ b/data/src/main/kotlin/com/alfresco/content/data/OfflineRepository.kt @@ -193,6 +193,7 @@ class OfflineRepository(otherSession: Session? = null) { parentId: String, isExtension: Boolean = false, uploadServerType: UploadServerType = UploadServerType.DEFAULT, + observerId: String? = null, ) { val resolver = context.contentResolver var name: String? = null @@ -217,9 +218,12 @@ class OfflineRepository(otherSession: Session? = null) { offlineStatus = OfflineStatus.PENDING, isExtension = isExtension, uploadServer = uploadServerType, + observerID = observerId ?: "", ) - clearData() + if (observerId == null) { + clearData() + } update(entry) @@ -275,6 +279,17 @@ class OfflineRepository(otherSession: Session? = null) { .build() .find() + fun fetchProcessEntries(parentId: String, observerId: String): MutableList { + val query = box.query() + .equal(Entry_.parentId, parentId, StringOrder.CASE_SENSITIVE) + .equal(Entry_.observerID, observerId, StringOrder.CASE_SENSITIVE) + .equal(Entry_.uploadServer, UploadServerType.UPLOAD_TO_PROCESS.value(), StringOrder.CASE_SENSITIVE) + .order(Entry_.name) + .build() + val list = query.find() + return list + } + /** * returns the list of uploads which is being uploaded on the server. */ @@ -291,6 +306,22 @@ class OfflineRepository(otherSession: Session? = null) { awaitClose { subscription.cancel() } } + /** + * returns the list of uploads which is being uploaded on the server. + */ + fun observeProcessUploads(parentId: String, uploadServerType: UploadServerType = UploadServerType.DEFAULT): Flow> = callbackFlow { + val query = box.query() + .equal(Entry_.parentId, parentId, StringOrder.CASE_SENSITIVE) + .equal(Entry_.isUpload, true) + .equal(Entry_.uploadServer, uploadServerType.value(), StringOrder.CASE_SENSITIVE) + .order(Entry_.name) + .build() + val subscription = query.subscribe().observer { + trySendBlocking(it) + } + awaitClose { subscription.cancel() } + } + /** * observer for transfer uploads */ diff --git a/data/src/main/kotlin/com/alfresco/content/data/TaskRepository.kt b/data/src/main/kotlin/com/alfresco/content/data/TaskRepository.kt index 6fd1c091f..39bdd86d7 100644 --- a/data/src/main/kotlin/com/alfresco/content/data/TaskRepository.kt +++ b/data/src/main/kotlin/com/alfresco/content/data/TaskRepository.kt @@ -328,6 +328,7 @@ class TaskRepository { ), local.parentId, uploadServer = uploadServerType, + observerID = local.observerID, ) else -> Entry() diff --git a/data/src/main/kotlin/com/alfresco/content/data/payloads/UploadData.kt b/data/src/main/kotlin/com/alfresco/content/data/payloads/UploadData.kt new file mode 100644 index 000000000..a023dbd74 --- /dev/null +++ b/data/src/main/kotlin/com/alfresco/content/data/payloads/UploadData.kt @@ -0,0 +1,11 @@ +package com.alfresco.content.data.payloads + +import android.os.Parcelable +import com.alfresco.content.data.ProcessEntry +import kotlinx.parcelize.Parcelize + +@Parcelize +data class UploadData( + val field: FieldsData = FieldsData(), + val process: ProcessEntry = ProcessEntry(), +) : Parcelable diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/AttachFilesField.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/AttachFilesField.kt index bb77846aa..c849fe494 100644 --- a/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/AttachFilesField.kt +++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/AttachFilesField.kt @@ -35,6 +35,7 @@ import com.alfresco.content.process.ui.theme.AlfrescoError fun AttachFilesField( contents: List = emptyList(), fieldsData: FieldsData = FieldsData(), + onUserTap: (Boolean) -> Unit = { }, navController: NavController, errorData: Pair = Pair(false, ""), ) { @@ -70,9 +71,7 @@ fun AttachFilesField( ) IconButton(onClick = { - navController.navigate( - R.id.action_nav_process_form_to_nav_attach_files, - ) + onUserTap(true) }) { Icon( imageVector = Icons.Default.Attachment, diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/FloatingActionButton.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/FloatingActionButton.kt index 3195f0ca1..2af752e9c 100644 --- a/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/FloatingActionButton.kt +++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/FloatingActionButton.kt @@ -13,7 +13,9 @@ import androidx.compose.ui.res.stringResource import com.airbnb.mvrx.compose.collectAsState import com.alfresco.content.component.ComponentBuilder import com.alfresco.content.component.ComponentData +import com.alfresco.content.data.Entry import com.alfresco.content.data.OptionsModel +import com.alfresco.content.data.payloads.FieldType import com.alfresco.content.process.R import com.alfresco.content.process.ui.fragments.FormViewModel import com.alfresco.content.process.ui.fragments.ProcessFragment @@ -33,7 +35,15 @@ fun FloatingActionButton(outcomes: List, fragment: ProcessFragment ) ComponentBuilder(context, componentData) .onApply { name, query, _ -> - val entry = state.listContents.find { it.isUpload } + val list = state.formFields.filter { it.type == FieldType.UPLOAD.value() } + .map { it.value as? List<*> ?: emptyList() }.flatten() + + val uploadList = state.formFields.filter { it.type == FieldType.UPLOAD.value() } + + val entry = uploadList.flatMap { fieldsData -> + (fieldsData.value as? List<*>)?.mapNotNull { it as? Entry } ?: emptyList() + }.find { !it.isUpload } + if (entry != null) { viewModel.optionsModel = OptionsModel(id = query, name = name) fragment.confirmContentQueuePrompt() diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/Outcomes.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/Outcomes.kt index 4a9042932..606533a83 100644 --- a/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/Outcomes.kt +++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/Outcomes.kt @@ -12,7 +12,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.airbnb.mvrx.compose.collectAsState +import com.alfresco.content.data.Entry import com.alfresco.content.data.OptionsModel +import com.alfresco.content.data.payloads.FieldType import com.alfresco.content.process.ui.fragments.FormViewModel import com.alfresco.content.process.ui.fragments.ProcessFragment @@ -25,7 +27,12 @@ fun Outcomes(outcomes: List, viewModel: FormViewModel, fragment: P .fillMaxWidth() .padding(horizontal = 12.dp, vertical = 4.dp), onClick = { - val entry = state.listContents.find { it.isUpload } + val uploadList = state.formFields.filter { it.type == FieldType.UPLOAD.value() } + + val entry = uploadList.flatMap { fieldsData -> + (fieldsData.value as? List<*>)?.mapNotNull { it as? Entry } ?: emptyList() + }.find { !it.isUpload } + if (entry != null) { viewModel.optionsModel = it fragment.confirmContentQueuePrompt() diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/composeviews/FormScrollContent.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/composeviews/FormScrollContent.kt index 1f1c7368b..e1ee6aa28 100644 --- a/process-app/src/main/kotlin/com/alfresco/content/process/ui/composeviews/FormScrollContent.kt +++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/composeviews/FormScrollContent.kt @@ -1,5 +1,6 @@ package com.alfresco.content.process.ui.composeviews +import android.os.Bundle import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -8,10 +9,13 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.navigation.NavController +import com.airbnb.mvrx.Mavericks +import com.alfresco.content.data.Entry import com.alfresco.content.data.ProcessEntry import com.alfresco.content.data.UserGroupDetails import com.alfresco.content.data.payloads.FieldType import com.alfresco.content.data.payloads.FieldsData +import com.alfresco.content.data.payloads.UploadData import com.alfresco.content.process.R import com.alfresco.content.process.ui.components.AmountInputField import com.alfresco.content.process.ui.components.AttachFilesField @@ -188,10 +192,31 @@ fun FormScrollContent(field: FieldsData, viewModel: FormViewModel, state: FormVi } FieldType.UPLOAD.value() -> { + val listContents = (field.value as? List<*>)?.mapNotNull { it as? Entry } ?: emptyList() + AttachFilesField( - contents = state.listContents, + contents = listContents, fieldsData = field, navController = navController, + onUserTap = { + if (it) { + viewModel.selectedField = field + + val bundle = Bundle().apply { + putParcelable( + Mavericks.KEY_ARG, + UploadData( + field = field, + process = state.parent, + ), + ) + } + navController.navigate( + R.id.action_nav_process_form_to_nav_attach_files, + bundle, + ) + } + }, ) } diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/FormViewModel.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/FormViewModel.kt index 97f7f5cdb..68893fc9b 100644 --- a/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/FormViewModel.kt +++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/FormViewModel.kt @@ -10,13 +10,14 @@ import com.airbnb.mvrx.ViewModelContext import com.alfresco.content.DATE_FORMAT_4 import com.alfresco.content.DATE_FORMAT_5 import com.alfresco.content.common.EntryListener +import com.alfresco.content.data.AttachFilesData import com.alfresco.content.data.AttachFolderSearchData import com.alfresco.content.data.DefaultOutcomesID +import com.alfresco.content.data.Entry import com.alfresco.content.data.OfflineRepository import com.alfresco.content.data.OptionsModel import com.alfresco.content.data.ProcessEntry import com.alfresco.content.data.TaskRepository -import com.alfresco.content.data.UploadServerType import com.alfresco.content.data.UserGroupDetails import com.alfresco.content.data.payloads.FieldType import com.alfresco.content.data.payloads.FieldsData @@ -25,10 +26,7 @@ import com.alfresco.content.getFormattedDate import com.alfresco.content.process.R import com.alfresco.coroutines.asFlow import com.alfresco.events.on -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel import kotlinx.coroutines.launch -import java.util.UUID class FormViewModel( val state: FormViewState, @@ -36,14 +34,13 @@ class FormViewModel( private val repository: TaskRepository, ) : MavericksViewModel(state) { - private var observeUploadsJob: Job? = null - var observerID: String = "" var selectedField: FieldsData? = null private var entryListener: EntryListener? = null var optionsModel: OptionsModel? = null init { - observerID = UUID.randomUUID().toString() + + OfflineRepository().removeCompletedUploads() if (state.parent.processInstanceId != null) { getTaskForms(state.parent) @@ -56,6 +53,12 @@ class FormViewModel( entryListener?.onAttachFolder(entry) } } + + viewModelScope.on { + it.field?.let { field -> + entryListener?.onAttachFiles(field) + } + } } /** @@ -63,32 +66,6 @@ class FormViewModel( */ fun getAPSUser() = repository.getAPSUser() - /** - * delete content locally - */ - fun deleteAttachment(contentId: String) = stateFlow.execute { - deleteUploads(contentId) - } - - private fun observeUploads(state: FormViewState) { - requireNotNull(state.parent) - - val repo = OfflineRepository() - - // On refresh clean completed uploads - repo.removeCompletedUploads() - - observeUploadsJob?.cancel() - observeUploadsJob = repo.observeUploads(observerID, UploadServerType.UPLOAD_TO_PROCESS) - .execute { - if (it is Success) { - updateUploads(it()) - } else { - this - } - } - } - private fun singleProcessDefinition(appDefinitionId: String) = withState { state -> viewModelScope.launch { repository::singleProcessDefinition.asFlow(appDefinitionId).execute { @@ -129,9 +106,6 @@ class FormViewModel( processOutcomes = it().outcomes, requestForm = Success(it()), ) - - observeUploads(updatedState) - val hasAllRequiredData = hasFieldValidData(fields) updateStateData(hasAllRequiredData, fields) @@ -207,6 +181,7 @@ class FormViewModel( updatedValue = null } } + updatedFieldList.add(FieldsData.withUpdateField(field, updatedValue, errorData)) } else { updatedFieldList.add(field) @@ -320,35 +295,36 @@ class FormViewModel( private fun convertFieldsToValues(fields: List): Map { val values = mutableMapOf() - fields.forEach { - when (it.type) { + fields.forEach { field -> + when (field.type) { FieldType.PEOPLE.value(), FieldType.FUNCTIONAL_GROUP.value() -> { when { - it.value != null -> { - values[it.id] = convertModelToMapValues(it.value as? UserGroupDetails) + field.value != null -> { + values[field.id] = convertModelToMapValues(field.value as? UserGroupDetails) } else -> { - values[it.id] = null + values[field.id] = null } } } FieldType.DATETIME.value(), FieldType.DATE.value() -> { - val convertedDate = (it.value as? String)?.getFormattedDate(DATE_FORMAT_4, DATE_FORMAT_5) - values[it.id] = convertedDate + val convertedDate = (field.value as? String)?.getFormattedDate(DATE_FORMAT_4, DATE_FORMAT_5) + values[field.id] = convertedDate } FieldType.RADIO_BUTTONS.value(), FieldType.DROPDOWN.value() -> { - values[it.id] = convertModelToMapValues(it) + values[field.id] = convertModelToMapValues(field) } FieldType.UPLOAD.value() -> { - values[it.id] = state.listContents.joinToString(separator = ",") { content -> content.id } + val listContents = (field.value as? List<*>)?.mapNotNull { it as? Entry } ?: emptyList() + values[field.id] = listContents.joinToString(separator = ",") { content -> content.id } } else -> { - values[it.id] = it.value + values[field.id] = field.value } } } @@ -370,6 +346,8 @@ class FormViewModel( entryListener = listener } + fun getContents(state: FormViewState, fieldId: String) = OfflineRepository().fetchProcessEntries(parentId = state.parent.id, observerId = fieldId) + fun emptyMessageArgs(state: FormViewState) = when { else -> diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/FormViewState.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/FormViewState.kt index 2ea1fd62b..8b24d8b5e 100644 --- a/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/FormViewState.kt +++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/FormViewState.kt @@ -3,8 +3,6 @@ package com.alfresco.content.process.ui.fragments import com.airbnb.mvrx.Async import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized -import com.alfresco.content.data.Entry -import com.alfresco.content.data.OfflineStatus import com.alfresco.content.data.OptionsModel import com.alfresco.content.data.ProcessEntry import com.alfresco.content.data.ResponseListForm @@ -22,9 +20,6 @@ data class FormViewState( val requestStartWorkflow: Async = Uninitialized, val requestOutcomes: Async> = Uninitialized, val requestSaveForm: Async> = Uninitialized, - val listContents: List = emptyList(), - val baseEntries: List = emptyList(), - val uploads: List = emptyList(), ) : MavericksState { constructor(target: ProcessEntry) : this(parent = target) @@ -32,65 +27,7 @@ data class FormViewState( * update the single process definition entry */ fun updateSingleProcessDefinition(response: ResponseListProcessDefinition): FormViewState { - if (parent == null) { - return this - } val processEntry = ProcessEntry.with(response.listProcessDefinitions.first(), parent) return copy(parent = processEntry) } - - /** - * delete content locally and update UI - */ - fun deleteUploads(contentId: String): FormViewState { - val listBaseEntries = baseEntries.filter { it.id != contentId } - val listUploads = uploads.filter { it.id != contentId } - return copyIncludingUploads(listBaseEntries, listUploads) - } - - /** - * updating the uploads entries with the server entries. - */ - fun updateUploads(uploads: List): FormViewState { - // Merge data only after at least the first page loaded - // [parent] is a good enough flag for the initial load. - return if (parent != null) { - copyIncludingUploads(baseEntries, uploads) - } else { - copy(uploads = uploads) - } - } - - private fun copyIncludingUploads( - entries: List, - uploads: List, - ): FormViewState { - val mixedUploads = uploads.transformCompletedUploads() - val mergedEntries = mergeInUploads(entries, mixedUploads) - val baseEntries = mergedEntries.filter { !it.isUpload } - - return copy( - listContents = mergedEntries, - baseEntries = baseEntries, - uploads = uploads, - ) - } - - private fun mergeInUploads(base: List, uploads: List): List { - return (uploads + base).distinctBy { it.id.ifEmpty { it.boxId } } - } - - /* - * Transforms completed uploads into network items, so further interaction with them - * doesn't require special logic. - */ - private fun List.transformCompletedUploads(): List = - map { - if (it.isUpload && it.isSynced) { - // Marking as partial avoids needing to store allowableOperations - it.copy(isUpload = false, offlineStatus = OfflineStatus.UNDEFINED, isPartial = true) - } else { - it - } - } } diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/ProcessAttachFilesFragment.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/ProcessAttachFilesFragment.kt index 0012600a8..543ef42b6 100644 --- a/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/ProcessAttachFilesFragment.kt +++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/ProcessAttachFilesFragment.kt @@ -10,10 +10,11 @@ import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import com.airbnb.epoxy.AsyncEpoxyController import com.airbnb.mvrx.MavericksView -import com.airbnb.mvrx.activityViewModel +import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import com.alfresco.content.common.EntryListener import com.alfresco.content.data.AnalyticsManager +import com.alfresco.content.data.AttachFilesData import com.alfresco.content.data.Entry import com.alfresco.content.data.PageView import com.alfresco.content.data.ParentEntry @@ -24,13 +25,17 @@ import com.alfresco.content.process.R import com.alfresco.content.process.databinding.FragmentAttachFilesBinding import com.alfresco.content.process.ui.epoxy.listViewAttachmentRow import com.alfresco.content.simpleController +import com.alfresco.events.EventBus +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch /** * Marked as ProcessAttachFilesFragment class */ class ProcessAttachFilesFragment : ProcessBaseFragment(), MavericksView, EntryListener { - val viewModel: FormViewModel by activityViewModel() + val viewModel: ProcessAttachFilesViewModel by fragmentViewModel() private lateinit var binding: FragmentAttachFilesBinding private val epoxyController: AsyncEpoxyController by lazy { epoxyController() } @@ -70,13 +75,9 @@ class ProcessAttachFilesFragment : ProcessBaseFragment(), MavericksView, EntryLi binding.refreshLayout.isRefreshing = false binding.loading.isVisible = false - val field = viewModel.selectedField - handler.post { - field?.let { data -> - val isError = (data.required && state.listContents.isEmpty()) + if (isAdded) { if (state.listContents.isNotEmpty()) { - viewModel.updateFieldValue(data.id, state.listContents, state, Pair(isError, "")) binding.tvNoOfAttachments.visibility = View.VISIBLE binding.tvNoOfAttachments.text = getString(R.string.text_multiple_attachment, state.listContents.size) } else { @@ -103,7 +104,7 @@ class ProcessAttachFilesFragment : ProcessBaseFragment(), MavericksView, EntryLi title(args.second) message(args.third) } - } else if (state.listContents.isNotEmpty()) { + } else { state.listContents.forEach { obj -> listViewAttachmentRow { id(stableId(obj)) @@ -134,4 +135,13 @@ class ProcessAttachFilesFragment : ProcessBaseFragment(), MavericksView, EntryLi localViewerIntent(entry as Entry) } } + + override fun onDestroy() { + withState(viewModel) { + CoroutineScope(Dispatchers.Main).launch { + EventBus.default.send(AttachFilesData(it.parent.field)) + } + } + super.onDestroy() + } } diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/ProcessAttachFilesViewModel.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/ProcessAttachFilesViewModel.kt new file mode 100644 index 000000000..aa40e070a --- /dev/null +++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/ProcessAttachFilesViewModel.kt @@ -0,0 +1,76 @@ +package com.alfresco.content.process.ui.fragments + +import android.content.Context +import com.airbnb.mvrx.MavericksViewModel +import com.airbnb.mvrx.MavericksViewModelFactory +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.ViewModelContext +import com.alfresco.content.common.EntryListener +import com.alfresco.content.data.OfflineRepository +import com.alfresco.content.data.TaskRepository +import com.alfresco.content.data.UploadServerType +import com.alfresco.content.process.R +import kotlinx.coroutines.Job + +class ProcessAttachFilesViewModel( + val state: ProcessAttachFilesViewState, + val context: Context, + private val repository: TaskRepository, +) : MavericksViewModel(state) { + + private var observeUploadsJob: Job? = null + var observerID: String = "" + private var entryListener: EntryListener? = null + + init { + observeUploads(state) + } + + /** + * returns the current logged in APS user profile data + */ + fun getAPSUser() = repository.getAPSUser() + + /** + * delete content locally + */ + fun deleteAttachment(contentId: String) = stateFlow.execute { + deleteUploads(contentId, observerID) + } + + private fun observeUploads(state: ProcessAttachFilesViewState) { + val process = state.parent.process + + observerID = process.id + + val repo = OfflineRepository() + + observeUploadsJob?.cancel() + observeUploadsJob = repo.observeProcessUploads(observerID, UploadServerType.UPLOAD_TO_PROCESS) + .execute { + if (it is Success) { + updateUploads(state.parent.field.id, it()) + } else { + this + } + } + } + + fun setListener(listener: EntryListener) { + entryListener = listener + } + + fun emptyMessageArgs(state: ProcessAttachFilesViewState) = + when { + else -> + Triple(R.drawable.ic_empty_files, R.string.no_attached_files, R.string.file_empty_message) + } + + companion object : MavericksViewModelFactory { + + override fun create( + viewModelContext: ViewModelContext, + state: ProcessAttachFilesViewState, + ) = ProcessAttachFilesViewModel(state, viewModelContext.activity(), TaskRepository()) + } +} diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/ProcessAttachFilesViewState.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/ProcessAttachFilesViewState.kt new file mode 100644 index 000000000..bb78e2271 --- /dev/null +++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/ProcessAttachFilesViewState.kt @@ -0,0 +1,69 @@ +package com.alfresco.content.process.ui.fragments + +import com.airbnb.mvrx.MavericksState +import com.alfresco.content.data.Entry +import com.alfresco.content.data.OfflineStatus +import com.alfresco.content.data.payloads.UploadData + +data class ProcessAttachFilesViewState( + val parent: UploadData = UploadData(), + val listContents: List = emptyList(), + val baseEntries: List = emptyList(), + val uploads: List = emptyList(), +) : MavericksState { + constructor(target: UploadData) : this(parent = target) + + /** + * delete content locally and update UI + */ + fun deleteUploads(contentId: String, observerId: String): ProcessAttachFilesViewState { + val listBaseEntries = baseEntries.filter { it.observerID == observerId }.filter { it.id != contentId } + val listUploads = uploads.filter { it.observerID == observerId }.filter { it.id != contentId } + return copyIncludingUploads(listBaseEntries, listUploads) + } + + /** + * updating the uploads entries with the server entries. + */ + fun updateUploads(observerId: String, uploads: List): ProcessAttachFilesViewState { + // Merge data only after at least the first page loaded + // [parent] is a good enough flag for the initial load + return copyIncludingUploads( + baseEntries.filter { it.observerID == observerId }, + uploads.filter { it.observerID == observerId }, + ) + } + + private fun copyIncludingUploads( + entries: List, + uploads: List, + ): ProcessAttachFilesViewState { + val mixedUploads = uploads.transformCompletedUploads() + val mergedEntries = mergeInUploads(entries, mixedUploads) + val baseEntries = mergedEntries.filter { !it.isUpload } + + return copy( + listContents = mergedEntries, + baseEntries = baseEntries, + uploads = uploads, + ) + } + + private fun mergeInUploads(base: List, uploads: List): List { + return (uploads + base).distinctBy { it.id.ifEmpty { it.boxId } } + } + + /* + * Transforms completed uploads into network items, so further interaction with them + * doesn't require special logic. + */ + private fun List.transformCompletedUploads(): List = + map { + if (it.isUpload && it.isSynced) { + // Marking as partial avoids needing to store allowableOperations + it.copy(isUpload = false, offlineStatus = OfflineStatus.UNDEFINED, isPartial = true) + } else { + it + } + } +} diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/ProcessBaseFragment.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/ProcessBaseFragment.kt index 7a680cbde..b33a1be51 100644 --- a/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/ProcessBaseFragment.kt +++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/ProcessBaseFragment.kt @@ -24,9 +24,9 @@ abstract class ProcessBaseFragment : Fragment(), DeleteContentListener { listener = this } - internal fun showCreateSheet(state: FormViewState, observerID: String) { + internal fun showCreateSheet(state: ProcessAttachFilesViewState, observerID: String) { AnalyticsManager().taskEvent(EventName.UploadProcessAttachment) - CreateActionsSheet.with(Entry.defaultWorkflowEntry(observerID)).show(childFragmentManager, null) + CreateActionsSheet.with(Entry.defaultWorkflowEntry(observerID, state.parent.field.id)).show(childFragmentManager, null) } /** diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/ProcessFragment.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/ProcessFragment.kt index afe68594c..66d124506 100644 --- a/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/ProcessFragment.kt +++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/ProcessFragment.kt @@ -23,6 +23,7 @@ import com.airbnb.mvrx.withState import com.alfresco.content.common.EntryListener import com.alfresco.content.data.Entry import com.alfresco.content.data.ParentEntry +import com.alfresco.content.data.payloads.FieldsData import com.alfresco.content.process.R import com.alfresco.content.process.databinding.FragmentProcessBinding import com.alfresco.content.process.ui.components.updateProcessList @@ -132,6 +133,21 @@ class ProcessFragment : Fragment(), MavericksView, EntryListener { } } + override fun onAttachFiles(field: FieldsData) = withState(viewModel) { state -> + if (isAdded) { + val listContents = viewModel.getContents(state, field.id) + val isError = field.required && listContents.isEmpty() + + viewModel.updateFieldValue( + field.id, + listContents, + state, + Pair(isError, ""), + ) + viewModel.selectedField = null + } + } + /** * It will prompt if user trying to start workflow and if any of content file is in uploaded */ diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/models/DataHolder.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/models/DataHolder.kt new file mode 100644 index 000000000..68fb82e66 --- /dev/null +++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/models/DataHolder.kt @@ -0,0 +1,11 @@ +package com.alfresco.content.process.ui.models + +import com.alfresco.content.data.Entry + +object DataHolder { + + val contentList: MutableList = mutableListOf() + + fun observeUploads(observerId: String) { + } +}