From 3380a2f63fd2e38be57a8f6950e6765175f96304 Mon Sep 17 00:00:00 2001 From: Amanpal Singh <87360222+aman-alfresco@users.noreply.github.com> Date: Fri, 22 Mar 2024 14:14:04 +0530 Subject: [PATCH] ADST-23 (#321) * submit collected data to server * code optimized * added UI for attachments * added attach files component * code optimize --- .../actions/sheet/ProcessDefinitionsSheet.kt | 2 +- .../browse/preview/LocalPreviewActivity.kt | 5 +- .../details/ProcessDetailViewModel.kt | 2 +- .../ProcessDetailViewModelExtension.kt | 2 +- .../processes/list/ProcessesViewModel.kt | 2 +- .../browse/tasks/BaseDetailFragment.kt | 3 +- .../attachments/AttachedFilesFragment.kt | 3 +- .../detail/TaskDetailViewModelExtension.kt | 2 +- browse/src/main/res/values-de/strings.xml | 8 - browse/src/main/res/values-es/strings.xml | 8 - browse/src/main/res/values-fr/strings.xml | 8 - browse/src/main/res/values-it/strings.xml | 8 - browse/src/main/res/values-nl/strings.xml | 8 - browse/src/main/res/values/strings.xml | 8 - .../src/main/res/drawable/ic_add_fab.xml | 0 common/src/main/res/values-de/strings.xml | 8 + common/src/main/res/values-es/strings.xml | 8 + common/src/main/res/values-fr/strings.xml | 9 + common/src/main/res/values-it/strings.xml | 8 + common/src/main/res/values-nl/strings.xml | 8 + common/src/main/res/values/strings.xml | 8 + .../alfresco/content/data/TaskRepository.kt | 32 ++- .../content/data/payloads/FieldsData.kt | 1 + gradle/libs.versions.toml | 1 + process-app/build.gradle | 18 +- process-app/src/main/AndroidManifest.xml | 2 +- .../alfresco/content/process/FormViewModel.kt | 158 ------------- .../content/process/NavigationComponent.kt | 25 -- .../content/process/ProcessFormActivity.kt | 68 ------ .../content/process/ui/ProcessFormActivity.kt | 37 +++ .../content/process/ui/ProcessFormFragment.kt | 36 --- .../process/ui/components/AttachFilesField.kt | 102 ++++++++ .../process/ui/components/ComposeTopBar.kt | 2 +- .../ui/components/FloatingActionButton.kt | 2 +- .../ui/components/FormViewModelExtension.kt | 4 +- .../content/process/ui/components/Outcomes.kt | 3 +- .../ui/{ => composeviews}/FormDetailScreen.kt | 16 +- .../FormScreen.kt | 12 +- .../FormScrollContent.kt | 35 ++- .../ui/composeviews/NavigationComponent.kt | 77 ++++++ .../ui/composeviews/ProcessAttachedFiles.kt | 30 +++ .../process/ui/epoxy/ListViewAttachmentRow.kt | 107 +++++++++ .../ui/fragments/BaseDetailFragment.kt | 88 +++++++ .../process/ui/fragments/FormViewModel.kt | 222 ++++++++++++++++++ .../process/ui/fragments/FormViewState.kt | 93 ++++++++ .../fragments/ProcessAttachedFilesFragment.kt | 133 +++++++++++ .../ui/{ => models}/UpdateProcessData.kt | 2 +- .../res/layout/fragment_attached_files.xml | 80 +++++++ .../res/layout/fragment_container_view.xml | 8 + .../res/layout/view_list_attachment_row.xml | 78 ++++++ process-app/src/main/res/values/dimens.xml | 5 + process-app/src/main/res/values/strings.xml | 1 + 52 files changed, 1203 insertions(+), 393 deletions(-) rename {browse => common}/src/main/res/drawable/ic_add_fab.xml (100%) delete mode 100644 process-app/src/main/kotlin/com/alfresco/content/process/FormViewModel.kt delete mode 100644 process-app/src/main/kotlin/com/alfresco/content/process/NavigationComponent.kt delete mode 100644 process-app/src/main/kotlin/com/alfresco/content/process/ProcessFormActivity.kt create mode 100644 process-app/src/main/kotlin/com/alfresco/content/process/ui/ProcessFormActivity.kt delete mode 100644 process-app/src/main/kotlin/com/alfresco/content/process/ui/ProcessFormFragment.kt create mode 100644 process-app/src/main/kotlin/com/alfresco/content/process/ui/components/AttachFilesField.kt rename process-app/src/main/kotlin/com/alfresco/content/process/ui/{ => composeviews}/FormDetailScreen.kt (84%) rename process-app/src/main/kotlin/com/alfresco/content/process/ui/{components => composeviews}/FormScreen.kt (90%) rename process-app/src/main/kotlin/com/alfresco/content/process/ui/{components => composeviews}/FormScrollContent.kt (78%) create mode 100644 process-app/src/main/kotlin/com/alfresco/content/process/ui/composeviews/NavigationComponent.kt create mode 100644 process-app/src/main/kotlin/com/alfresco/content/process/ui/composeviews/ProcessAttachedFiles.kt create mode 100644 process-app/src/main/kotlin/com/alfresco/content/process/ui/epoxy/ListViewAttachmentRow.kt create mode 100644 process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/BaseDetailFragment.kt create mode 100644 process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/FormViewModel.kt create mode 100644 process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/FormViewState.kt create mode 100644 process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/ProcessAttachedFilesFragment.kt rename process-app/src/main/kotlin/com/alfresco/content/process/ui/{ => models}/UpdateProcessData.kt (68%) create mode 100644 process-app/src/main/res/layout/fragment_attached_files.xml create mode 100644 process-app/src/main/res/layout/fragment_container_view.xml create mode 100644 process-app/src/main/res/layout/view_list_attachment_row.xml create mode 100644 process-app/src/main/res/values/dimens.xml diff --git a/actions/src/main/kotlin/com/alfresco/content/actions/sheet/ProcessDefinitionsSheet.kt b/actions/src/main/kotlin/com/alfresco/content/actions/sheet/ProcessDefinitionsSheet.kt index cd9682bbc..5f37bf4e9 100644 --- a/actions/src/main/kotlin/com/alfresco/content/actions/sheet/ProcessDefinitionsSheet.kt +++ b/actions/src/main/kotlin/com/alfresco/content/actions/sheet/ProcessDefinitionsSheet.kt @@ -83,7 +83,7 @@ class ProcessDefinitionsSheet : BottomSheetDialogFragment(), MavericksView { val intent = Intent( requireActivity(), - Class.forName("com.alfresco.content.process.ProcessFormActivity"), + Class.forName("com.alfresco.content.process.ui.ProcessFormActivity"), ) intent.putExtra(Mavericks.KEY_ARG, processEntry) startActivity(intent) diff --git a/browse/src/main/kotlin/com/alfresco/content/browse/preview/LocalPreviewActivity.kt b/browse/src/main/kotlin/com/alfresco/content/browse/preview/LocalPreviewActivity.kt index bc0e95e5c..28f4a73ca 100644 --- a/browse/src/main/kotlin/com/alfresco/content/browse/preview/LocalPreviewActivity.kt +++ b/browse/src/main/kotlin/com/alfresco/content/browse/preview/LocalPreviewActivity.kt @@ -8,6 +8,7 @@ import com.alfresco.content.actions.Action import com.alfresco.content.browse.R import com.alfresco.content.browse.databinding.ActivityLocalPreviewBinding import com.alfresco.content.data.Entry +import com.alfresco.content.process.ui.fragments.BaseDetailFragment.Companion.KEY_ENTRY_OBJ /** * Mark as Preview Activity @@ -45,8 +46,4 @@ class LocalPreviewActivity : AppCompatActivity() { fragment.arguments = intent.extras } } - - companion object { - const val KEY_ENTRY_OBJ = "entryObj" - } } diff --git a/browse/src/main/kotlin/com/alfresco/content/browse/processes/details/ProcessDetailViewModel.kt b/browse/src/main/kotlin/com/alfresco/content/browse/processes/details/ProcessDetailViewModel.kt index 6d862cea2..4c942332a 100644 --- a/browse/src/main/kotlin/com/alfresco/content/browse/processes/details/ProcessDetailViewModel.kt +++ b/browse/src/main/kotlin/com/alfresco/content/browse/processes/details/ProcessDetailViewModel.kt @@ -192,7 +192,7 @@ class ProcessDetailViewModel( fun startWorkflow() = withState { state -> val items = state.listContents.joinToString(separator = ",") { it.id } viewModelScope.launch { - repository::startWorkflow.asFlow(state.parent, items, emptyList()).execute { + repository::startWorkflow.asFlow(state.parent, items, mapOf()).execute { when (it) { is Loading -> copy(requestStartWorkflow = Loading()) is Fail -> copy(requestStartWorkflow = Fail(it.error)) diff --git a/browse/src/main/kotlin/com/alfresco/content/browse/processes/details/ProcessDetailViewModelExtension.kt b/browse/src/main/kotlin/com/alfresco/content/browse/processes/details/ProcessDetailViewModelExtension.kt index ae06a25eb..1c0cb2612 100644 --- a/browse/src/main/kotlin/com/alfresco/content/browse/processes/details/ProcessDetailViewModelExtension.kt +++ b/browse/src/main/kotlin/com/alfresco/content/browse/processes/details/ProcessDetailViewModelExtension.kt @@ -1,7 +1,7 @@ package com.alfresco.content.browse.processes.details import com.alfresco.content.browse.tasks.list.UpdateTasksData -import com.alfresco.content.process.ui.UpdateProcessData +import com.alfresco.content.process.ui.models.UpdateProcessData import com.alfresco.events.EventBus import kotlinx.coroutines.launch diff --git a/browse/src/main/kotlin/com/alfresco/content/browse/processes/list/ProcessesViewModel.kt b/browse/src/main/kotlin/com/alfresco/content/browse/processes/list/ProcessesViewModel.kt index 72a4d7d51..c4474e037 100644 --- a/browse/src/main/kotlin/com/alfresco/content/browse/processes/list/ProcessesViewModel.kt +++ b/browse/src/main/kotlin/com/alfresco/content/browse/processes/list/ProcessesViewModel.kt @@ -13,7 +13,7 @@ import com.alfresco.content.data.payloads.TaskProcessFiltersPayload import com.alfresco.content.getLocalizedName import com.alfresco.content.listview.processes.ProcessListViewModel import com.alfresco.content.listview.processes.ProcessListViewState -import com.alfresco.content.process.ui.UpdateProcessData +import com.alfresco.content.process.ui.models.UpdateProcessData import com.alfresco.coroutines.asFlow import com.alfresco.events.on import kotlinx.coroutines.launch diff --git a/browse/src/main/kotlin/com/alfresco/content/browse/tasks/BaseDetailFragment.kt b/browse/src/main/kotlin/com/alfresco/content/browse/tasks/BaseDetailFragment.kt index ad6dbc032..eca91e07c 100644 --- a/browse/src/main/kotlin/com/alfresco/content/browse/tasks/BaseDetailFragment.kt +++ b/browse/src/main/kotlin/com/alfresco/content/browse/tasks/BaseDetailFragment.kt @@ -14,6 +14,7 @@ import com.alfresco.content.browse.tasks.detail.TaskDetailViewState import com.alfresco.content.data.AnalyticsManager import com.alfresco.content.data.Entry import com.alfresco.content.data.EventName +import com.alfresco.content.process.ui.fragments.BaseDetailFragment.Companion.KEY_ENTRY_OBJ import com.alfresco.content.viewer.ViewerActivity import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar @@ -84,7 +85,7 @@ abstract class BaseDetailFragment : Fragment(), DeleteContentListener { */ fun localViewerIntent(contentEntry: Entry) = startActivity( Intent(requireActivity(), LocalPreviewActivity::class.java) - .putExtra(LocalPreviewActivity.KEY_ENTRY_OBJ, contentEntry), + .putExtra(KEY_ENTRY_OBJ, contentEntry), ) /** diff --git a/browse/src/main/kotlin/com/alfresco/content/browse/tasks/attachments/AttachedFilesFragment.kt b/browse/src/main/kotlin/com/alfresco/content/browse/tasks/attachments/AttachedFilesFragment.kt index 20b4424d4..a7174d323 100644 --- a/browse/src/main/kotlin/com/alfresco/content/browse/tasks/attachments/AttachedFilesFragment.kt +++ b/browse/src/main/kotlin/com/alfresco/content/browse/tasks/attachments/AttachedFilesFragment.kt @@ -30,6 +30,7 @@ import com.alfresco.content.data.PageView import com.alfresco.content.data.ParentEntry import com.alfresco.content.data.UploadServerType import com.alfresco.content.mimetype.MimeType +import com.alfresco.content.process.ui.fragments.BaseDetailFragment.Companion.KEY_ENTRY_OBJ import com.alfresco.content.simpleController import com.alfresco.ui.getDrawableForAttribute @@ -148,7 +149,7 @@ class AttachedFilesFragment : BaseDetailFragment(), MavericksView, EntryListener if (isAdded) { startActivity( Intent(requireActivity(), LocalPreviewActivity::class.java) - .putExtra(LocalPreviewActivity.KEY_ENTRY_OBJ, entry as Entry), + .putExtra(KEY_ENTRY_OBJ, entry as Entry), ) } } diff --git a/browse/src/main/kotlin/com/alfresco/content/browse/tasks/detail/TaskDetailViewModelExtension.kt b/browse/src/main/kotlin/com/alfresco/content/browse/tasks/detail/TaskDetailViewModelExtension.kt index e52d784d9..803ee7516 100644 --- a/browse/src/main/kotlin/com/alfresco/content/browse/tasks/detail/TaskDetailViewModelExtension.kt +++ b/browse/src/main/kotlin/com/alfresco/content/browse/tasks/detail/TaskDetailViewModelExtension.kt @@ -5,7 +5,7 @@ import com.alfresco.content.browse.tasks.list.UpdateTasksData import com.alfresco.content.data.Entry import com.alfresco.content.data.OfflineRepository import com.alfresco.content.data.UserGroupDetails -import com.alfresco.content.process.ui.UpdateProcessData +import com.alfresco.content.process.ui.models.UpdateProcessData import com.alfresco.events.EventBus import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch diff --git a/browse/src/main/res/values-de/strings.xml b/browse/src/main/res/values-de/strings.xml index 972b3bc62..4975652ab 100755 --- a/browse/src/main/res/values-de/strings.xml +++ b/browse/src/main/res/values-de/strings.xml @@ -46,7 +46,6 @@ Account Symbol für Fälligkeitsdatum Symbol für Benutzerprofil - Symbol für Datei Symbol für Priorität Zugewiesenes Symbol Symbol für Status @@ -63,15 +62,11 @@ Alle anzeigen %d Kommentare Angehängte Dateien - %d Anhänge Keine angehängten Dateien Kein Fälligkeitsdatum - Angehängte Dateien Abgeschlossen Aufgabe abschließen Sind Sie sicher, dass Sie diese Aufgabe abschließen wollen? Sie werden keine Änderungen mehr vornehmen können. - Abbrechen - Bestätigen Schaltfläche „Senden“ Keine Beschreibung Abgeschlossen-Symbol @@ -87,9 +82,6 @@ Aufgabe erstellen Verwerfen Sollen die Änderungen verworfen werden? - Symbol \'Anhang löschen\' - Eine Datei löschen? - Anhänge hinzufügen Symbol \'Anhang hinzufügen\' Sind Sie sicher, dass Sie die Aufgabe abschließen wollen? Nach Abschluss der Aufgabe können einige Dateien nicht hochgeladen werden und es können keine Änderungen vorgenommen werden. diff --git a/browse/src/main/res/values-es/strings.xml b/browse/src/main/res/values-es/strings.xml index c15d3a30c..3b2174e55 100755 --- a/browse/src/main/res/values-es/strings.xml +++ b/browse/src/main/res/values-es/strings.xml @@ -46,7 +46,6 @@ Cuenta Icono de fecha de vencimiento Icono de perfil de usuario - Icono de fichero Icono de prioridad Icono de usuario asignado Icono de estado @@ -63,15 +62,11 @@ Ver todo %d comentarios Ficheros adjuntos - %d adjuntos No hay ficheros adjuntos Sin fecha de vencimiento - Ficheros adjuntos Por completar Completar tarea ¿Está seguro de que desea completar esta tarea? Ya no podrá realizar ningún cambio. - Cancelar - Confirmar Botón Enviar Sin descripción Icono completado @@ -88,9 +83,6 @@ Crear tarea Descartar ¿Desea descartar los cambios? - Icono de eliminación de adjuntos - ¿Eliminar un fichero? - Añadir adjuntos Añadir icono adjunto ¿Está seguro de que quiere completar la tarea? Una vez completada, algunos ficheros no se pudieron cargar y no se pueden realizar cambios. diff --git a/browse/src/main/res/values-fr/strings.xml b/browse/src/main/res/values-fr/strings.xml index 2f0ef7b63..110a523f8 100755 --- a/browse/src/main/res/values-fr/strings.xml +++ b/browse/src/main/res/values-fr/strings.xml @@ -46,7 +46,6 @@ Compte Icône Date d\'échéance Icône du profil d\'utilisateur - Icône de fichier Icône de priorité Icône Assigné Icône du statut @@ -63,15 +62,11 @@ Afficher tout %d commentaires Fichiers joints - %d pièces jointes Aucun fichier joint Aucune date d\'échéance - Fichiers joints Terminé Terminer la tâche Voulez-vous vraiment terminer cette tâche ? Vous ne pourrez plus faire de modifications. - Annuler - Confirmer Bouton Envoyer Aucune description Icône Terminé @@ -87,9 +82,6 @@ Créer une tâche Ignorer Voulez-vous ignorer les modifications ? - Icône Supprimer la pièce jointe - Supprimer un fichier ? - Ajouter des pièces jointes Icône Ajouter une pièce jointe Voulez-vous vraiment terminer la tâche ? Une fois terminée, certains fichiers ne pourront plus être importés et aucun changement ne pourra être effectué. diff --git a/browse/src/main/res/values-it/strings.xml b/browse/src/main/res/values-it/strings.xml index b6f7827c3..c85359602 100755 --- a/browse/src/main/res/values-it/strings.xml +++ b/browse/src/main/res/values-it/strings.xml @@ -46,7 +46,6 @@ Account Icona scadenza Icona profilo utente - Icona file Icona priorità Icona assegnazione Icona stato @@ -63,10 +62,8 @@ Visualizza tutto %d commenti File allegati - %d allegati Nessun file allegato Nessuna scadenza - File allegati Completato Completa compito Vuoi completare il compito? Non potrai più apportare modifiche. @@ -85,11 +82,6 @@ Icona Modifica assegnatario Icona Modifica nome e descrizione Crea compito - Ignora - Vuoi ignorare le modifiche? - Icona Elimina allegato - Vuoi eliminare un file? - Aggiungi allegati Icona Aggiungi allegati Vuoi completare il compito? Una volta completato, non sarà possibile caricare alcuni file né apportare modifiche. diff --git a/browse/src/main/res/values-nl/strings.xml b/browse/src/main/res/values-nl/strings.xml index 575f51886..2580d8d5a 100755 --- a/browse/src/main/res/values-nl/strings.xml +++ b/browse/src/main/res/values-nl/strings.xml @@ -46,7 +46,6 @@ Account Pictogram Vervaldatum Pictogram Gebruikersprofiel - Pictogram Bestand Pictogram Prioriteit Pictogram Toegewezen Pictogram Status @@ -63,15 +62,11 @@ Alle weergeven %d opmerkingen Bijgevoegde bestanden - %d bijlagen Geen bijgevoegde bestanden Geen vervaldatum - Bijgevoegde bestanden Voltooien Taak voltooien Weet u zeker dat u de taak wilt voltooien? U kunt geen wijzigingen meer aanbrengen. - Annuleren - Bevestigen Knop Verzenden Geen beschrijving Pictogram Voltooid @@ -87,9 +82,6 @@ Taak creëren Negeren Wilt u de wijzigingen negeren? - Pictogram Bijlage verwijderen - Bestand verwijderen? - Bijlagen toevoegen Pictogram Bijlage toevoegen Weet u zeker dat u de taak wilt voltooien? Nadat de taak is voltooid, kunnen sommige bestanden niet worden geüpload en kunnen geen wijzigingen worden aangebracht. diff --git a/browse/src/main/res/values/strings.xml b/browse/src/main/res/values/strings.xml index 5a2a21b4c..308a7d023 100644 --- a/browse/src/main/res/values/strings.xml +++ b/browse/src/main/res/values/strings.xml @@ -46,7 +46,6 @@ Account Due date icon User profile icon - File icon Priority icon Assigned icon Status icon @@ -63,15 +62,11 @@ View all %d comments Attached files - %d attachments No Attached Files No due date - Attached files Complete Complete Task Are you sure you want to complete this task? You will no longer be able to make any changes. - Cancel - Confirm Send Button No description Completed icon @@ -87,9 +82,6 @@ Create task Discard Do you want to discard the changes? - Icon delete attachment - Delete a file? - Add attachments Add attachment icon Are you sure you want to complete the task? Once completed, some files could not be uploaded and no changes can be made. diff --git a/browse/src/main/res/drawable/ic_add_fab.xml b/common/src/main/res/drawable/ic_add_fab.xml similarity index 100% rename from browse/src/main/res/drawable/ic_add_fab.xml rename to common/src/main/res/drawable/ic_add_fab.xml diff --git a/common/src/main/res/values-de/strings.xml b/common/src/main/res/values-de/strings.xml index 7eb4c7b51..e0f14d872 100755 --- a/common/src/main/res/values-de/strings.xml +++ b/common/src/main/res/values-de/strings.xml @@ -30,4 +30,12 @@ %d Ausgewählt Bitte überprüfen Sie Ihre Internetverbindung und starten anschließend den Vorgang erneut. ...Alle anzeigen + %d Anhänge + Anhänge hinzufügen + Angehängte Dateien + Symbol für Datei + Symbol \'Anhang löschen\' + Eine Datei löschen? + Abbrechen + Bestätigen diff --git a/common/src/main/res/values-es/strings.xml b/common/src/main/res/values-es/strings.xml index f9b91786b..8bb2593b2 100755 --- a/common/src/main/res/values-es/strings.xml +++ b/common/src/main/res/values-es/strings.xml @@ -30,4 +30,12 @@ %d Seleccionado Verifique su conexión a Internet y vuelva a intentarlo. ...Ver todo + %d adjuntos + Añadir adjuntos + Ficheros adjuntos + Icono de fichero + Icono de eliminación de adjuntos + ¿Eliminar un fichero? + Cancelar + Confirmar diff --git a/common/src/main/res/values-fr/strings.xml b/common/src/main/res/values-fr/strings.xml index 843b09a56..88553c8ed 100755 --- a/common/src/main/res/values-fr/strings.xml +++ b/common/src/main/res/values-fr/strings.xml @@ -30,4 +30,13 @@ %d Sélectionné Veuillez vérifier votre connexion Internet et réessayer. …Afficher tout + %d pièces jointes + Ajouter des pièces jointes + Fichiers joints + Icône de fichier + Icône Supprimer la pièce jointe + Supprimer un fichier ? + Annuler + Confirmer + diff --git a/common/src/main/res/values-it/strings.xml b/common/src/main/res/values-it/strings.xml index f4f4c7881..0b96f77ce 100755 --- a/common/src/main/res/values-it/strings.xml +++ b/common/src/main/res/values-it/strings.xml @@ -30,4 +30,12 @@ %d Selezionato Controlla la connessione Internet e riprova. ...Visualizza tutto + %d allegati + Aggiungi allegati + File allegati + Icona file + Icona Elimina allegato + Vuoi eliminare un file? + Ignora + Vuoi ignorare le modifiche? diff --git a/common/src/main/res/values-nl/strings.xml b/common/src/main/res/values-nl/strings.xml index b1120ac49..5195da14c 100755 --- a/common/src/main/res/values-nl/strings.xml +++ b/common/src/main/res/values-nl/strings.xml @@ -30,4 +30,12 @@ %d Geselecteerd Controleer uw internetverbinding en probeer het opnieuw. ...Alle weergeven + %d bijlagen + Bijlagen toevoegen + Bijgevoegde bestanden + Pictogram Bestand + Pictogram Bijlage verwijderen + Bestand verwijderen? + Annuleren + Bevestigen diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index ddba320d3..71840605d 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -31,4 +31,12 @@ Please check your internet connection and Try again. …View all Workflow + %d attachments + Add attachments + Attached files + File icon + Icon delete attachment + Delete a file? + Cancel + Confirm 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 1277bfa8e..55a43a0e7 100644 --- a/data/src/main/kotlin/com/alfresco/content/data/TaskRepository.kt +++ b/data/src/main/kotlin/com/alfresco/content/data/TaskRepository.kt @@ -4,7 +4,6 @@ import android.content.SharedPreferences import androidx.preference.PreferenceManager import com.alfresco.content.data.Settings.Companion.IS_PROCESS_ENABLED_KEY import com.alfresco.content.data.payloads.CommentPayload -import com.alfresco.content.data.payloads.FieldType import com.alfresco.content.data.payloads.FieldsData import com.alfresco.content.data.payloads.LinkContentPayload import com.alfresco.content.data.payloads.SystemPropertiesEntry @@ -383,31 +382,17 @@ class TaskRepository { /** * Execute the start flow integration */ - suspend fun startWorkflow(processEntry: ProcessEntry?, items: String, fields: List) = ProcessEntry.with( + suspend fun startWorkflow(processEntry: ProcessEntry?, items: String, values: Map) = ProcessEntry.with( processesService.createProcessInstance( RequestProcessInstances( name = processEntry?.name, processDefinitionId = processEntry?.id, - values = convertFieldsToValues(fields), + values = values, ), ), ) - private fun convertFieldsToValues(fields: List): Map { - val values = mutableMapOf() - - fields.forEach { - if (it.type == FieldType.PEOPLE.value() || it.type == FieldType.FUNCTIONAL_GROUP.value()) { - values[it.id] = getUserOrGroup(it.value as UserGroupDetails) - } else { - values[it.id] = it.value - } - } - - return values - } - - private fun getUserOrGroup(userGroupInfo: UserGroupDetails?): Map { + fun getUserOrGroup(userGroupInfo: UserGroupDetails?): Map { return if (userGroupInfo?.isGroup == true) { mapOf( "id" to userGroupInfo.id, @@ -427,6 +412,17 @@ class TaskRepository { } } + fun mapStringToOptionValues(fieldsData: FieldsData): Map { + val id = fieldsData.options.find { it.name == fieldsData.value }?.id + + requireNotNull(id) + + return mapOf( + "id" to id, + "name" to fieldsData.name, + ) + } + /** * saving the accountInfo data in preferences */ diff --git a/data/src/main/kotlin/com/alfresco/content/data/payloads/FieldsData.kt b/data/src/main/kotlin/com/alfresco/content/data/payloads/FieldsData.kt index 667e7e62a..960f8dfe2 100644 --- a/data/src/main/kotlin/com/alfresco/content/data/payloads/FieldsData.kt +++ b/data/src/main/kotlin/com/alfresco/content/data/payloads/FieldsData.kt @@ -88,6 +88,7 @@ enum class FieldType { PEOPLE, FUNCTIONAL_GROUP, HYPERLINK, + UPLOAD, ; fun value() = name.lowercase() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 239643921..066fff2df 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -128,6 +128,7 @@ ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } material3 = { group = "androidx.compose.material3", name = "material3" } androidx-compose-material-iconsExtended = { module = "androidx.compose.material:material-icons-extended" } +ui-compose-viewbinding = "androidx.compose.ui:ui-viewbinding:1.6.3" navigation-compose = "androidx.navigation:navigation-compose:2.7.6" compose-runtime = "androidx.compose.runtime:runtime:1.5.4" diff --git a/process-app/build.gradle b/process-app/build.gradle index e376b6bf8..ea1707bfa 100644 --- a/process-app/build.gradle +++ b/process-app/build.gradle @@ -1,6 +1,7 @@ plugins { id 'com.android.library' id 'org.jetbrains.kotlin.android' + id 'kotlin-kapt' } android { @@ -29,7 +30,8 @@ android { } buildFeatures { - compose = true + compose true + viewBinding true } composeOptions { @@ -48,8 +50,15 @@ dependencies { implementation project(':common') implementation project(':actions') implementation project(':data') + implementation project(':listview') implementation project(':component') + implementation project(':viewer-common') + implementation project(':viewer-image') + implementation project(':viewer-media') + implementation project(':viewer-pdf') + implementation project(':viewer-text') implementation project(':viewer') + implementation project(':mimetype') implementation libs.androidx.core implementation libs.androidx.appcompat @@ -59,6 +68,7 @@ dependencies { implementation libs.ui implementation libs.activity.compose implementation libs.ui.graphics + implementation libs.ui.compose.viewbinding implementation libs.ui.tooling.preview implementation libs.material3 @@ -67,4 +77,10 @@ dependencies { implementation libs.androidx.compose.material.iconsExtended debugImplementation libs.androidx.ui.tooling + + implementation libs.androidx.swiperefreshlayout + implementation libs.mavericks + implementation libs.epoxy.core + kapt libs.epoxy.processor + } diff --git a/process-app/src/main/AndroidManifest.xml b/process-app/src/main/AndroidManifest.xml index 577f4d8ba..a1c319b56 100644 --- a/process-app/src/main/AndroidManifest.xml +++ b/process-app/src/main/AndroidManifest.xml @@ -3,7 +3,7 @@ diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/FormViewModel.kt b/process-app/src/main/kotlin/com/alfresco/content/process/FormViewModel.kt deleted file mode 100644 index 97ea1e1a9..000000000 --- a/process-app/src/main/kotlin/com/alfresco/content/process/FormViewModel.kt +++ /dev/null @@ -1,158 +0,0 @@ -package com.alfresco.content.process - -import android.content.Context -import com.airbnb.mvrx.Async -import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MavericksState -import com.airbnb.mvrx.MavericksViewModel -import com.airbnb.mvrx.MavericksViewModelFactory -import com.airbnb.mvrx.Success -import com.airbnb.mvrx.Uninitialized -import com.airbnb.mvrx.ViewModelContext -import com.alfresco.content.data.OptionsModel -import com.alfresco.content.data.ProcessEntry -import com.alfresco.content.data.ResponseListForm -import com.alfresco.content.data.ResponseListProcessDefinition -import com.alfresco.content.data.TaskRepository -import com.alfresco.content.data.UserGroupDetails -import com.alfresco.content.data.payloads.FieldsData -import com.alfresco.coroutines.asFlow -import kotlinx.coroutines.launch - -data class FormViewState( - val parent: ProcessEntry = ProcessEntry(), - val requestStartForm: Async = Uninitialized, - val requestProcessDefinition: Async = Uninitialized, - val formFields: List = emptyList(), - val processOutcomes: List = emptyList(), - val enabledOutcomes: Boolean = false, - val requestStartWorkflow: Async = Uninitialized, -) : MavericksState { - constructor(target: ProcessEntry) : this(parent = target) - - /** - * 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) - } -} - -class FormViewModel( - val state: FormViewState, - val context: Context, - private val repository: TaskRepository, -) : MavericksViewModel(state) { - - init { - singleProcessDefinition(state.parent.id) - } - - private fun singleProcessDefinition(appDefinitionId: String) = withState { state -> - viewModelScope.launch { - repository::singleProcessDefinition.asFlow(appDefinitionId).execute { - when (it) { - is Loading -> copy(requestProcessDefinition = Loading()) - is Fail -> copy(requestProcessDefinition = Fail(it.error)) - is Success -> { - val updatedState = updateSingleProcessDefinition(it()) - getStartForm(updatedState.parent) - copy(requestProcessDefinition = Success(it())) - } - - else -> { - this - } - } - } - } - } - - private fun getStartForm(processEntry: ProcessEntry) { - requireNotNull(processEntry.id) - viewModelScope.launch { - repository::startForm.asFlow(processEntry.id).execute { - when (it) { - is Loading -> copy(requestStartForm = Loading()) - is Fail -> { - it.error.printStackTrace() - copy(requestStartForm = Fail(it.error)) - } - - is Success -> { - copy( - parent = processEntry, - formFields = it().fields.flatMap { listData -> listData.fields }, - processOutcomes = it().outcomes, - requestStartForm = Success(it()), - ) - } - - else -> { - this - } - } - } - } - } - - fun updateFieldValue(fieldId: String, newValue: Any?, state: FormViewState) { - val updatedState = state.copy( - formFields = state.formFields.map { field -> - if (field.id == fieldId) { - var updateValue = newValue - when { - (updateValue is String) && updateValue.isEmpty() -> { - updateValue = null - } - - (updateValue is Boolean) && !updateValue -> { - updateValue = null - } - - (updateValue is UserGroupDetails) && updateValue.id == 0 -> { - updateValue = null - } - } - field.copy(value = updateValue) - } else { - field - } - }, - ) - - val hasAllRequiredData = hasFieldRequiredData(updatedState) - - setState { updatedState.copy(enabledOutcomes = hasAllRequiredData) } - } - - fun startWorkflow() = withState { state -> - viewModelScope.launch { - repository::startWorkflow.asFlow(state.parent, "", state.formFields).execute { - when (it) { - is Loading -> copy(requestStartWorkflow = Loading()) - is Fail -> copy(requestStartWorkflow = Fail(it.error)) - is Success -> copy(requestStartWorkflow = Success(it())) - else -> this - } - } - } - } - - private fun hasFieldRequiredData(state: FormViewState): Boolean { - return !state.formFields.filter { it.required }.any { it.value == null } - } - - companion object : MavericksViewModelFactory { - - override fun create( - viewModelContext: ViewModelContext, - state: FormViewState, - ) = FormViewModel(state, viewModelContext.activity(), TaskRepository()) - } -} diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/NavigationComponent.kt b/process-app/src/main/kotlin/com/alfresco/content/process/NavigationComponent.kt deleted file mode 100644 index a1e59b333..000000000 --- a/process-app/src/main/kotlin/com/alfresco/content/process/NavigationComponent.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.alfresco.content.process - -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Surface -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import com.alfresco.content.process.ui.components.FormScreen - -@Composable -fun NavigationComponent() { - val navController = rememberNavController() - - Surface(modifier = Modifier.fillMaxSize()) { - NavHost(navController = navController, startDestination = "first_screen") { - composable("first_screen") { - // Replace with the content of your first fragment - FormScreen(navController) - } - // Add more composable entries for other fragments in your navigation graph - } - } -} diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ProcessFormActivity.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ProcessFormActivity.kt deleted file mode 100644 index 622dfc8ac..000000000 --- a/process-app/src/main/kotlin/com/alfresco/content/process/ProcessFormActivity.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.alfresco.content.process - -import android.os.Bundle -import android.view.ViewGroup -import androidx.activity.compose.setContent -import androidx.appcompat.app.AppCompatActivity -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.fragment.app.FragmentContainerView -import com.alfresco.content.process.ui.ProcessFormFragment -import com.alfresco.content.process.ui.theme.AlfrescoBaseTheme - -class ProcessFormActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContent { - AlfrescoBaseTheme { - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background, - ) { - composeApp() - } - } - } - } - - private fun composeApp() { - val fragmentManager = supportFragmentManager - val containerId = resources.getIdentifier("frame_container", "id", packageName) - - // Check if the fragment is already added - - // Create FragmentContainerView and add it to the activity - val fragmentContainer = FragmentContainerView(this).apply { - id = containerId - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT, - ) - } - setContentView(fragmentContainer) - - fragmentManager - .beginTransaction() - .replace( - fragmentContainer.id, - ProcessFormFragment().apply { - // Set your data using intent extras - arguments = intent.extras - }, - "firstFragment", - ) - .commit() - } -} - -@Preview(showBackground = true) -@Composable -fun GreetingPreview() { - AlfrescoBaseTheme { - NavigationComponent() - } -} diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/ProcessFormActivity.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/ProcessFormActivity.kt new file mode 100644 index 000000000..d0049972b --- /dev/null +++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/ProcessFormActivity.kt @@ -0,0 +1,37 @@ +package com.alfresco.content.process.ui + +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.alfresco.content.process.ui.composeviews.NavigationComponent +import com.alfresco.content.process.ui.theme.AlfrescoBaseTheme + +class ProcessFormActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + AlfrescoBaseTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background, + ) { + NavigationComponent() + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun GreetingPreview() { + AlfrescoBaseTheme { + NavigationComponent() + } +} diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/ProcessFormFragment.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/ProcessFormFragment.kt deleted file mode 100644 index a048b15f7..000000000 --- a/process-app/src/main/kotlin/com/alfresco/content/process/ui/ProcessFormFragment.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.alfresco.content.process.ui - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment -import com.alfresco.content.process.NavigationComponent -import com.alfresco.content.process.ui.theme.AlfrescoBaseTheme - -class ProcessFormFragment : Fragment() { - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return ComposeView(requireContext()).apply { - layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) - setContent { - AlfrescoBaseTheme { - NavigationComponent() - } - } - } - } -} - -@Composable -fun BackButton(onClick: () -> Unit) { - IconButton(onClick = onClick) { - Icon(imageVector = Icons.Default.ArrowBack, contentDescription = "Back") - } -} 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 new file mode 100644 index 000000000..26285c200 --- /dev/null +++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/AttachFilesField.kt @@ -0,0 +1,102 @@ +package com.alfresco.content.process.ui.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Attachment +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import com.alfresco.content.data.Entry +import com.alfresco.content.data.payloads.FieldsData +import com.alfresco.content.process.R +import com.alfresco.content.process.ui.composeviews.NavigationScreen +import com.alfresco.content.process.ui.theme.AlfrescoBlue300 +import com.alfresco.content.process.ui.theme.AlfrescoError + +@Composable +fun AttachFilesField( + contents: List = emptyList(), + fieldsData: FieldsData = FieldsData(), + navController: NavController, +) { + val labelWithAsterisk = buildAnnotatedString { + append(fieldsData.name) + if (fieldsData.required) { + withStyle(style = SpanStyle(color = AlfrescoError)) { + append(" *") // Adding a red asterisk for mandatory fields + } + } + } + + val contentValue = if (contents.isEmpty()) { + stringResource(id = R.string.no_attachments) + } else { + stringResource(id = R.string.text_multiple_attachment, contents.size) + } + + val context = LocalContext.current + Column( + modifier = Modifier + .fillMaxSize() + .padding(top = 16.dp, bottom = 0.dp, start = 16.dp, end = 16.dp), + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = labelWithAsterisk, + modifier = Modifier + .padding(end = 4.dp) + .align(alignment = Alignment.CenterVertically), + ) + + IconButton(onClick = { + navController.navigate( + NavigationScreen.ATTACHED_FILES_SCREEN.value(), + ) + }) { + Icon( + imageVector = Icons.Default.Attachment, + tint = AlfrescoBlue300, + contentDescription = "", + ) + } + } + Text( + text = contentValue, + style = TextStyle( + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = 12.sp, + ), + modifier = Modifier + .padding(start = 4.dp, top = 0.dp) + .align(alignment = Alignment.Start), + ) + } +} + +@Preview +@Composable +fun AttachFilesFieldPreview() { +// AttachFilesField() +} diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/ComposeTopBar.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/ComposeTopBar.kt index 1ffc975d3..8a5da1fd5 100644 --- a/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/ComposeTopBar.kt +++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/ComposeTopBar.kt @@ -9,7 +9,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.alfresco.content.process.R -import com.alfresco.content.process.ui.BackButton +import com.alfresco.content.process.ui.composeviews.BackButton import com.alfresco.content.process.ui.theme.SeparateColorGrayLT @OptIn(ExperimentalMaterial3Api::class) 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 a88fb4521..1f12c2668 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 @@ -12,8 +12,8 @@ import androidx.compose.ui.res.stringResource import com.alfresco.content.component.ComponentBuilder import com.alfresco.content.component.ComponentData import com.alfresco.content.data.OptionsModel -import com.alfresco.content.process.FormViewModel import com.alfresco.content.process.R +import com.alfresco.content.process.ui.fragments.FormViewModel @Composable fun FloatingActionButton(outcomes: List, enabledOutcomes: Boolean, viewModel: FormViewModel) { diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/FormViewModelExtension.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/FormViewModelExtension.kt index f3e9dd7b2..c3b8ddd66 100644 --- a/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/FormViewModelExtension.kt +++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/FormViewModelExtension.kt @@ -1,7 +1,7 @@ package com.alfresco.content.process.ui.components -import com.alfresco.content.process.FormViewModel -import com.alfresco.content.process.ui.UpdateProcessData +import com.alfresco.content.process.ui.fragments.FormViewModel +import com.alfresco.content.process.ui.models.UpdateProcessData import com.alfresco.events.EventBus import kotlinx.coroutines.launch 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 df57c8d83..b17561ec0 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 @@ -10,9 +10,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.alfresco.content.data.OptionsModel -import com.alfresco.content.process.FormViewModel +import com.alfresco.content.process.ui.fragments.FormViewModel @Composable fun Outcomes(outcomes: List, enabledOutcomes: Boolean, viewModel: FormViewModel) { diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/FormDetailScreen.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/composeviews/FormDetailScreen.kt similarity index 84% rename from process-app/src/main/kotlin/com/alfresco/content/process/ui/FormDetailScreen.kt rename to process-app/src/main/kotlin/com/alfresco/content/process/ui/composeviews/FormDetailScreen.kt index c389e255b..2b7f8619b 100644 --- a/process-app/src/main/kotlin/com/alfresco/content/process/ui/FormDetailScreen.kt +++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/composeviews/FormDetailScreen.kt @@ -1,4 +1,4 @@ -package com.alfresco.content.process.ui +package com.alfresco.content.process.ui.composeviews import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -18,16 +18,17 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.tooling.preview.Preview +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController import com.alfresco.content.data.OptionsModel import com.alfresco.content.data.TaskRepository -import com.alfresco.content.process.FormViewModel -import com.alfresco.content.process.FormViewState -import com.alfresco.content.process.ui.components.FormScrollContent import com.alfresco.content.process.ui.components.Outcomes +import com.alfresco.content.process.ui.fragments.FormViewModel +import com.alfresco.content.process.ui.fragments.FormViewState @OptIn(ExperimentalComposeUiApi::class) @Composable -fun FormDetailScreen(state: FormViewState, viewModel: FormViewModel, outcomes: List) { +fun FormDetailScreen(state: FormViewState, viewModel: FormViewModel, outcomes: List, navController: NavController) { val keyboardController = LocalSoftwareKeyboardController.current val focusManager = LocalFocusManager.current @@ -41,7 +42,7 @@ fun FormDetailScreen(state: FormViewState, viewModel: FormViewModel, outcomes: L .clickable { // Hide the keyboard on click outside of input fields // keyboardController?.hide() -// focusManager.clearFocus() + focusManager.clearFocus() }, ) { LazyColumn( @@ -57,7 +58,7 @@ fun FormDetailScreen(state: FormViewState, viewModel: FormViewModel, outcomes: L }, items = formList, ) { field -> - FormScrollContent(field, viewModel, state) + FormScrollContent(field, viewModel, state, navController) } } @@ -85,5 +86,6 @@ fun PreviewProcessDetailScreen() { TaskRepository(), ), emptyList(), + rememberNavController(), ) } diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/FormScreen.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/composeviews/FormScreen.kt similarity index 90% rename from process-app/src/main/kotlin/com/alfresco/content/process/ui/components/FormScreen.kt rename to process-app/src/main/kotlin/com/alfresco/content/process/ui/composeviews/FormScreen.kt index 28a72b104..e70223dd9 100644 --- a/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/FormScreen.kt +++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/composeviews/FormScreen.kt @@ -1,4 +1,4 @@ -package com.alfresco.content.process.ui.components +package com.alfresco.content.process.ui.composeviews import ComposeTopBar import android.app.Activity @@ -18,9 +18,11 @@ import com.airbnb.mvrx.Success import com.airbnb.mvrx.compose.collectAsState import com.airbnb.mvrx.compose.mavericksActivityViewModel import com.alfresco.content.data.OptionsModel -import com.alfresco.content.process.FormViewModel import com.alfresco.content.process.R -import com.alfresco.content.process.ui.FormDetailScreen +import com.alfresco.content.process.ui.components.CustomLinearProgressIndicator +import com.alfresco.content.process.ui.components.FloatingActionButton +import com.alfresco.content.process.ui.components.updateProcessList +import com.alfresco.content.process.ui.fragments.FormViewModel @Composable fun FormScreen(navController: NavController) { @@ -63,7 +65,7 @@ fun FormScreen(navController: NavController) { if (state.requestStartForm is Loading) { CustomLinearProgressIndicator(padding) } - FormDetailScreen(state, viewModel, customOutcomes) + FormDetailScreen(state, viewModel, customOutcomes, navController) } } } @@ -86,7 +88,7 @@ fun FormScreen(navController: NavController) { if (state.requestStartForm is Loading || state.requestStartWorkflow is Loading) { CustomLinearProgressIndicator(padding) } - FormDetailScreen(state, viewModel, emptyList()) + FormDetailScreen(state, viewModel, emptyList(), navController) } } } diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/FormScrollContent.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/composeviews/FormScrollContent.kt similarity index 78% rename from process-app/src/main/kotlin/com/alfresco/content/process/ui/components/FormScrollContent.kt rename to process-app/src/main/kotlin/com/alfresco/content/process/ui/composeviews/FormScrollContent.kt index e8bb74690..16db8f035 100644 --- a/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/FormScrollContent.kt +++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/composeviews/FormScrollContent.kt @@ -1,23 +1,33 @@ -package com.alfresco.content.process.ui.components +package com.alfresco.content.process.ui.composeviews import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.res.stringResource +import androidx.navigation.NavController 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.process.FormViewModel -import com.alfresco.content.process.FormViewState import com.alfresco.content.process.R +import com.alfresco.content.process.ui.components.AmountInputField +import com.alfresco.content.process.ui.components.AttachFilesField +import com.alfresco.content.process.ui.components.CheckBoxField +import com.alfresco.content.process.ui.components.DateTimeField +import com.alfresco.content.process.ui.components.DropdownField +import com.alfresco.content.process.ui.components.HyperLinkField +import com.alfresco.content.process.ui.components.IntegerInputField +import com.alfresco.content.process.ui.components.MultiLineInputField +import com.alfresco.content.process.ui.components.PeopleField +import com.alfresco.content.process.ui.components.ReadOnlyField +import com.alfresco.content.process.ui.components.SingleLineInputField +import com.alfresco.content.process.ui.fragments.FormViewModel +import com.alfresco.content.process.ui.fragments.FormViewState -@OptIn(ExperimentalComposeUiApi::class) @Composable -fun FormScrollContent(field: FieldsData, viewModel: FormViewModel, state: FormViewState) { +fun FormScrollContent(field: FieldsData, viewModel: FormViewModel, state: FormViewState, navController: NavController) { when (field.type) { FieldType.TEXT.value() -> { var textFieldValue by remember { mutableStateOf(field.value as? String ?: "") } @@ -93,8 +103,9 @@ fun FormScrollContent(field: FieldsData, viewModel: FormViewModel, state: FormVi } FieldType.DROPDOWN.value(), FieldType.RADIO_BUTTONS.value() -> { - var textFieldValue by remember { mutableStateOf(field.placeHolder ?: "") } - var textFieldQuery by remember { mutableStateOf("") } + var textFieldValue by remember { mutableStateOf(field.value as? String ?: "") } + var textFieldQuery by remember { mutableStateOf(field.options.find { it.name == textFieldValue }?.id ?: "") } + DropdownField( nameText = textFieldValue, queryText = textFieldQuery, @@ -131,5 +142,13 @@ fun FormScrollContent(field: FieldsData, viewModel: FormViewModel, state: FormVi FieldType.HYPERLINK.value() -> { HyperLinkField(field) } + + FieldType.UPLOAD.value() -> { + AttachFilesField( + contents = state.listContents, + fieldsData = field, + navController = navController, + ) + } } } diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/composeviews/NavigationComponent.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/composeviews/NavigationComponent.kt new file mode 100644 index 000000000..66c70d778 --- /dev/null +++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/composeviews/NavigationComponent.kt @@ -0,0 +1,77 @@ +package com.alfresco.content.process.ui.composeviews + +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.viewinterop.AndroidViewBinding +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import com.alfresco.content.process.R +import com.alfresco.content.process.databinding.FragmentContainerViewBinding +import com.alfresco.content.process.ui.fragments.ProcessAttachedFilesFragment + +@Composable +fun NavigationComponent() { + val navController = rememberNavController() + + Surface(modifier = Modifier.fillMaxSize()) { + NavHost(navController = navController, startDestination = NavigationScreen.FIRST_SCREEN.value()) { + composable(NavigationScreen.FIRST_SCREEN.value()) { + // Replace with the content of your first fragment + FormScreen(navController) + } + // Add more composable entries for other fragments in your navigation graph + composable(NavigationScreen.ATTACHED_FILES_SCREEN.value()) { + // Replace with the content of ProcessAttachedFilesFragment + ProcessAttachedFilesScreen(navController) + } + } + } +} + +@Composable +fun ProcessAttachedFilesScreen(navController: NavHostController) { + val context = LocalContext.current + AndroidViewBinding( + FragmentContainerViewBinding::inflate, + ) { + // Adjust layout properties + fragmentContainerView.layoutParams = ViewGroup.MarginLayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ) + + val contextApp = (context as? AppCompatActivity) + + println(".ProcessAttachedFilesScreen $contextApp") + + // Adjust system UI visibility + contextApp?.window?.decorView?.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + + fragmentContainerView.setPadding( + fragmentContainerView.paddingLeft, + context.resources.getDimensionPixelSize(R.dimen.default_status_bar_height), + fragmentContainerView.paddingRight, + context.resources.getDimensionPixelSize(R.dimen.default_bottom_controller_height), + ) + + fragmentContainerView.getFragment() + } +} + +enum class NavigationScreen() { + FIRST_SCREEN, + ATTACHED_FILES_SCREEN, + ; + + fun value() = this.name.lowercase() +} diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/composeviews/ProcessAttachedFiles.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/composeviews/ProcessAttachedFiles.kt new file mode 100644 index 000000000..51b618076 --- /dev/null +++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/composeviews/ProcessAttachedFiles.kt @@ -0,0 +1,30 @@ +package com.alfresco.content.process.ui.composeviews + +import android.view.LayoutInflater +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView +import com.alfresco.content.process.R + +@Composable +fun ProcessAttachedFiles() { + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { context -> + // Inflate your XML layout here + LayoutInflater.from(context).inflate(R.layout.fragment_attached_files, null) + }, + ) +} + +@Composable +fun BackButton(onClick: () -> Unit) { + IconButton(onClick = onClick) { + Icon(imageVector = Icons.Default.ArrowBack, contentDescription = "Back") + } +} diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/epoxy/ListViewAttachmentRow.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/epoxy/ListViewAttachmentRow.kt new file mode 100644 index 000000000..07e098b88 --- /dev/null +++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/epoxy/ListViewAttachmentRow.kt @@ -0,0 +1,107 @@ +package com.alfresco.content.process.ui.epoxy + +import android.content.Context +import android.graphics.drawable.AnimatedVectorDrawable +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.widget.FrameLayout +import androidx.core.content.res.ResourcesCompat +import androidx.core.view.isVisible +import com.airbnb.epoxy.CallbackProp +import com.airbnb.epoxy.ModelProp +import com.airbnb.epoxy.ModelView +import com.alfresco.content.data.Entry +import com.alfresco.content.data.OfflineStatus +import com.alfresco.content.listview.R +import com.alfresco.content.mimetype.MimeType +import com.alfresco.content.process.databinding.ViewListAttachmentRowBinding + +/** + * Marked as ListViewAttachmentRow class + */ +@ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT) +class ListViewAttachmentRow @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, +) : FrameLayout(context, attrs, defStyleAttr) { + + private val binding = ViewListAttachmentRowBinding.inflate(LayoutInflater.from(context), this) + + /** + * set the content data on list row + */ + @ModelProp + fun setData(data: Entry) { + binding.tvName.text = data.name + binding.iconFile.setImageDrawable(ResourcesCompat.getDrawable(resources, MimeType.with(data.mimeType).icon, context.theme)) + + configureOfflineStatus(data) + + binding.deleteContentButton.visibility = if (actionButtonVisibility(data)) View.VISIBLE else View.INVISIBLE + } + + private fun configureOfflineStatus(entry: Entry) { + // Offline screen items and uploads + if (entry.isFile && entry.hasOfflineStatus) { + val drawableRes = makeOfflineStatusConfig(entry) + if (drawableRes != null) { + val drawable = + ResourcesCompat.getDrawable(resources, drawableRes, context.theme) + if (drawable is AnimatedVectorDrawable) { + drawable.start() + } + binding.offlineIcon.setImageDrawable(drawable) + binding.offlineIcon.isVisible = true + } else { + binding.offlineIcon.isVisible = false + } + } else { + binding.offlineIcon.isVisible = false + } + } + + private fun makeOfflineStatusConfig(entry: Entry): Int? = + when (entry.offlineStatus) { + OfflineStatus.PENDING -> + if (entry.isUpload) { + R.drawable.ic_offline_upload + } else { + R.drawable.ic_offline_status_pending + } + + OfflineStatus.SYNCING -> + R.drawable.ic_offline_status_in_progress_anim + + OfflineStatus.SYNCED -> + R.drawable.ic_offline_status_synced + + OfflineStatus.ERROR -> + R.drawable.ic_offline_status_error + + else -> + R.drawable.ic_offline_status_synced + } + + private fun actionButtonVisibility(entry: Entry) = + !entry.isLink && !entry.isUpload && + // Child folder in offline tab + !(entry.isFolder && entry.hasOfflineStatus && !entry.isOffline) && !entry.isReadOnly + + /** + * list row click listener + */ + @CallbackProp + fun setClickListener(listener: OnClickListener?) { + setOnClickListener(listener) + } + + /** + * delete icon click listener + */ + @CallbackProp + fun setDeleteContentClickListener(listener: OnClickListener?) { + binding.deleteContentButton.setOnClickListener(listener) + } +} diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/BaseDetailFragment.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/BaseDetailFragment.kt new file mode 100644 index 000000000..1905df134 --- /dev/null +++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/BaseDetailFragment.kt @@ -0,0 +1,88 @@ +package com.alfresco.content.process.ui.fragments + +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment +import com.alfresco.content.REMOTE +import com.alfresco.content.actions.CreateActionsSheet +import com.alfresco.content.data.AnalyticsManager +import com.alfresco.content.data.Entry +import com.alfresco.content.data.EventName +import com.alfresco.content.viewer.ViewerActivity +import com.google.android.material.snackbar.Snackbar +import java.lang.ref.WeakReference + +/** + * Marked as BaseDetailFragment class + */ +abstract class BaseDetailFragment : Fragment(), DeleteContentListener { + + private var deleteContentDialog = WeakReference(null) + lateinit var listener: DeleteContentListener + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + listener = this + } + + internal fun showCreateSheet(state: FormViewState, observerID: String) { + AnalyticsManager().taskEvent(EventName.UploadProcessAttachment) + CreateActionsSheet.with(Entry.defaultWorkflowEntry(observerID)).show(childFragmentManager, null) + } + + /** + * return the stable id of uploading contents + */ + fun stableId(entry: Entry): String = + if (entry.isUpload) { + entry.boxId.toString() + } else entry.id + + /** + * This intent will open the remote file + */ + fun remoteViewerIntent(entry: Entry) = startActivity( + Intent(requireActivity(), ViewerActivity::class.java) + .putExtra(ViewerActivity.KEY_ID, entry.id) + .putExtra(ViewerActivity.KEY_TITLE, entry.name) + .putExtra(ViewerActivity.KEY_MODE, REMOTE), + ) + + /** + * This intent will open the local file + */ + fun localViewerIntent(contentEntry: Entry) { + val intent = Intent( + requireActivity(), + Class.forName("com.alfresco.content.browse.preview.LocalPreviewActivity"), + ) + intent.putExtra(KEY_ENTRY_OBJ, contentEntry) + startActivity(intent) + } + + /** + * showing Snackbar + */ + fun showSnackar(snackView: View, message: String) = Snackbar.make( + snackView, + message, + Snackbar.LENGTH_SHORT, + ).show() + + companion object { + const val KEY_ENTRY_OBJ = "entryObj" + } +} + +/** + * Marked as DeleteContentListener interface + */ +interface DeleteContentListener { + + /** + * It will get call on confirm delete. + */ + fun onConfirmDelete(contentId: String) +} 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 new file mode 100644 index 000000000..b3f76bf75 --- /dev/null +++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/FormViewModel.kt @@ -0,0 +1,222 @@ +package com.alfresco.content.process.ui.fragments + +import android.content.Context +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.MavericksViewModel +import com.airbnb.mvrx.MavericksViewModelFactory +import com.airbnb.mvrx.Success +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.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 +import com.alfresco.content.getFormattedDate +import com.alfresco.coroutines.asFlow +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import java.util.UUID + +class FormViewModel( + val state: FormViewState, + val context: Context, + private val repository: TaskRepository, +) : MavericksViewModel(state) { + + private var observeUploadsJob: Job? = null + var entryListener: EntryListener? = null + var observerID: String = "" + private var isExecuted = false + + init { + observerID = UUID.randomUUID().toString() + singleProcessDefinition(state.parent.id) + } + + /** + * returns the current logged in APS user profile data + */ + 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 { + when (it) { + is Loading -> copy(requestProcessDefinition = Loading()) + is Fail -> copy(requestProcessDefinition = Fail(it.error)) + is Success -> { + val updatedState = updateSingleProcessDefinition(it()) + observeUploads(updatedState) + updatedState.parent.let { processEntry -> + getStartForm(processEntry) + } + copy(requestProcessDefinition = Success(it())) + } + + else -> { + this + } + } + } + } + } + + private fun getStartForm(processEntry: ProcessEntry) { + requireNotNull(processEntry.id) + viewModelScope.launch { + repository::startForm.asFlow(processEntry.id).execute { + when (it) { + is Loading -> copy(requestStartForm = Loading()) + is Fail -> { + it.error.printStackTrace() + copy(requestStartForm = Fail(it.error)) + } + + is Success -> { + val updatedState = copy( + parent = processEntry, + formFields = it().fields.flatMap { listData -> listData.fields }, + processOutcomes = it().outcomes, + requestStartForm = Success(it()), + ) + enableDisableActions(updatedState) + updatedState + } + + else -> { + this + } + } + } + } + } + + fun updateFieldValue(fieldId: String, newValue: Any?, state: FormViewState) { + val updatedFieldList = state.formFields.map { field -> + if (field.id == fieldId) { + var updatedValue = newValue + when { + (updatedValue is String) && updatedValue.isEmpty() -> { + updatedValue = null + } + + (updatedValue is Boolean) && !updatedValue -> { + updatedValue = null + } + + (updatedValue is UserGroupDetails) && updatedValue.id == 0 -> { + updatedValue = null + } + + (updatedValue is OptionsModel) && updatedValue.id.isEmpty() -> { + updatedValue = null + } + } + field.copy(value = updatedValue) + } else { + field + } + } + + val updatedState = state.copy( + formFields = updatedFieldList, + ) + + enableDisableActions(updatedState) + } + + fun startWorkflow() = withState { state -> + viewModelScope.launch { + repository::startWorkflow.asFlow(state.parent, "", convertFieldsToValues(state.formFields)).execute { + when (it) { + is Loading -> copy(requestStartWorkflow = Loading()) + is Fail -> copy(requestStartWorkflow = Fail(it.error)) + is Success -> copy(requestStartWorkflow = Success(it())) + else -> this + } + } + } + } + + private fun convertFieldsToValues(fields: List): Map { + val values = mutableMapOf() + + fields.forEach { + when (it.type) { + FieldType.PEOPLE.value(), FieldType.FUNCTIONAL_GROUP.value() -> { + values[it.id] = repository.getUserOrGroup(it.value as UserGroupDetails) + } + + FieldType.DATETIME.value(), FieldType.DATE.value() -> { + val convertedDate = (it.value as? String)?.getFormattedDate(DATE_FORMAT_4, DATE_FORMAT_5) + values[it.id] = convertedDate + } + + FieldType.RADIO_BUTTONS.value(), FieldType.DROPDOWN.value() -> { + values[it.id] = repository.mapStringToOptionValues(it) + } + + FieldType.UPLOAD.value() -> { + values[it.id] = state.listContents.joinToString(separator = ",") { content -> content.id } + } + + else -> { + values[it.id] = it.value + } + } + } + + return values + } + + private fun enableDisableActions(state: FormViewState) { + val hasAllRequiredData = hasFieldRequiredData(state) + + setState { state.copy(enabledOutcomes = hasAllRequiredData) } + } + + private fun hasFieldRequiredData(state: FormViewState): Boolean { + return !state.formFields.filter { it.required }.any { it.value == null } + } + + companion object : MavericksViewModelFactory { + + override fun create( + viewModelContext: ViewModelContext, + state: FormViewState, + ) = FormViewModel(state, viewModelContext.activity(), TaskRepository()) + } +} 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 new file mode 100644 index 000000000..77c64e8b4 --- /dev/null +++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/FormViewState.kt @@ -0,0 +1,93 @@ +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 +import com.alfresco.content.data.ResponseListProcessDefinition +import com.alfresco.content.data.payloads.FieldsData + +data class FormViewState( + val parent: ProcessEntry = ProcessEntry(), + val requestStartForm: Async = Uninitialized, + val requestProcessDefinition: Async = Uninitialized, + val formFields: List = emptyList(), + val processOutcomes: List = emptyList(), + val enabledOutcomes: Boolean = false, + val requestStartWorkflow: Async = Uninitialized, + val listContents: List = emptyList(), + val baseEntries: List = emptyList(), + val uploads: List = emptyList(), +) : MavericksState { + constructor(target: ProcessEntry) : this(parent = target) + + /** + * 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/ProcessAttachedFilesFragment.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/ProcessAttachedFilesFragment.kt new file mode 100644 index 000000000..33d2dfcaa --- /dev/null +++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/ProcessAttachedFilesFragment.kt @@ -0,0 +1,133 @@ +package com.alfresco.content.process.ui.fragments + +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +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.withState +import com.alfresco.content.common.EntryListener +import com.alfresco.content.data.AnalyticsManager +import com.alfresco.content.data.Entry +import com.alfresco.content.data.PageView +import com.alfresco.content.data.ParentEntry +import com.alfresco.content.data.UploadServerType +import com.alfresco.content.data.payloads.FieldType +import com.alfresco.content.mimetype.MimeType +import com.alfresco.content.process.R +import com.alfresco.content.process.databinding.FragmentAttachedFilesBinding +import com.alfresco.content.process.ui.epoxy.listViewAttachmentRow +import com.alfresco.content.simpleController +import com.alfresco.ui.getDrawableForAttribute + +/** + * Marked as ProcessAttachedFilesFragment class + */ +class ProcessAttachedFilesFragment : BaseDetailFragment(), MavericksView, EntryListener { + + val viewModel: FormViewModel by activityViewModel() + private lateinit var binding: FragmentAttachedFilesBinding + private val epoxyController: AsyncEpoxyController by lazy { epoxyController() } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + binding = FragmentAttachedFilesBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + AnalyticsManager().screenViewEvent(PageView.AttachedFiles) + binding.refreshLayout.isEnabled = false + binding.toolbar.apply { + navigationContentDescription = getString(R.string.label_navigation_back) + navigationIcon = requireContext().getDrawableForAttribute(R.attr.homeAsUpIndicator) + setNavigationOnClickListener { requireActivity().onBackPressed() } + title = resources.getString(R.string.title_attached_files) + } + + binding.recyclerView.setController(epoxyController) + + epoxyController.adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + if (positionStart == 0) { + // @see: https://github.com/airbnb/epoxy/issues/224 + binding.recyclerView.layoutManager?.scrollToPosition(0) + } + } + }) + } + + override fun onConfirmDelete(contentId: String) { + viewModel.deleteAttachment(contentId) + } + + override fun invalidate() = withState(viewModel) { state -> + val handler = Handler(Looper.getMainLooper()) + binding.refreshLayout.isRefreshing = false + binding.loading.isVisible = false + + val fields = state.formFields.find { it.type == FieldType.UPLOAD.value() }!! + + handler.post { + if (state.listContents.isNotEmpty()) { + viewModel.updateFieldValue(fields.id, state.listContents, state) + binding.tvNoOfAttachments.visibility = View.VISIBLE + binding.tvNoOfAttachments.text = getString(R.string.text_multiple_attachment, state.listContents.size) + } else { + binding.tvNoOfAttachments.visibility = View.GONE + } + } + + binding.fabAddAttachments.visibility = View.VISIBLE + binding.fabAddAttachments.setOnClickListener { + showCreateSheet(state, viewModel.observerID) + } + + epoxyController.requestModelBuild() + } + + private fun epoxyController() = simpleController(viewModel) { state -> + + if (state.listContents.isNotEmpty()) { + state.listContents.forEach { obj -> + listViewAttachmentRow { + id(stableId(obj)) + data(obj) + deleteContentClickListener { model, _, _, _ -> onConfirmDelete(model.data().id) } + } + } + } + } + + private fun onItemClicked(contentEntry: Entry) { + if (!contentEntry.isUpload) { + if (!contentEntry.source.isNullOrEmpty()) { + val entry = Entry.convertContentEntryToEntry( + contentEntry, + MimeType.isDocFile(contentEntry.mimeType), + UploadServerType.UPLOAD_TO_PROCESS, + ) + remoteViewerIntent(entry) + } + } else { + localViewerIntent(contentEntry) + } + } + + override fun onEntryCreated(entry: ParentEntry) { + if (isAdded) { + localViewerIntent(entry as Entry) + } + } +} diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/UpdateProcessData.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/models/UpdateProcessData.kt similarity index 68% rename from process-app/src/main/kotlin/com/alfresco/content/process/ui/UpdateProcessData.kt rename to process-app/src/main/kotlin/com/alfresco/content/process/ui/models/UpdateProcessData.kt index c61068eb7..2cdd507f2 100644 --- a/process-app/src/main/kotlin/com/alfresco/content/process/ui/UpdateProcessData.kt +++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/models/UpdateProcessData.kt @@ -1,4 +1,4 @@ -package com.alfresco.content.process.ui +package com.alfresco.content.process.ui.models /** * Mark as UpdateProcessData data class diff --git a/process-app/src/main/res/layout/fragment_attached_files.xml b/process-app/src/main/res/layout/fragment_attached_files.xml new file mode 100644 index 000000000..4597c4127 --- /dev/null +++ b/process-app/src/main/res/layout/fragment_attached_files.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/process-app/src/main/res/layout/fragment_container_view.xml b/process-app/src/main/res/layout/fragment_container_view.xml new file mode 100644 index 000000000..63cd22aa1 --- /dev/null +++ b/process-app/src/main/res/layout/fragment_container_view.xml @@ -0,0 +1,8 @@ + + + + diff --git a/process-app/src/main/res/layout/view_list_attachment_row.xml b/process-app/src/main/res/layout/view_list_attachment_row.xml new file mode 100644 index 000000000..3d1cfaccd --- /dev/null +++ b/process-app/src/main/res/layout/view_list_attachment_row.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/process-app/src/main/res/values/dimens.xml b/process-app/src/main/res/values/dimens.xml new file mode 100644 index 000000000..31013e734 --- /dev/null +++ b/process-app/src/main/res/values/dimens.xml @@ -0,0 +1,5 @@ + + + 24dp + 56dp + diff --git a/process-app/src/main/res/values/strings.xml b/process-app/src/main/res/values/strings.xml index 4f64f389d..38c7c3e99 100644 --- a/process-app/src/main/res/values/strings.xml +++ b/process-app/src/main/res/values/strings.xml @@ -9,4 +9,5 @@ This is a required field. Actions Process Actions Button + No Attachments