diff --git a/actions/src/main/kotlin/com/alfresco/content/actions/Action.kt b/actions/src/main/kotlin/com/alfresco/content/actions/Action.kt
index 889a18bab..d1cb76d63 100644
--- a/actions/src/main/kotlin/com/alfresco/content/actions/Action.kt
+++ b/actions/src/main/kotlin/com/alfresco/content/actions/Action.kt
@@ -5,6 +5,7 @@ import android.content.Context
import android.view.View
import androidx.annotation.StringRes
import com.alfresco.Logger
+import com.alfresco.content.GetMultipleContents.Companion.MAX_FILE_SIZE_10
import com.alfresco.content.data.APIEvent
import com.alfresco.content.data.AnalyticsManager
import com.alfresco.content.data.Entry
@@ -49,10 +50,16 @@ interface Action {
bus.send(newAction)
} catch (ex: CancellationException) {
// no-op
- if (entry is Entry && (entry as Entry).uploadServer == UploadServerType.UPLOAD_TO_TASK &&
- ex.message == ERROR_FILE_SIZE_EXCEED
- ) {
- bus.send(Error(context.getString(R.string.error_file_size_exceed)))
+ when {
+ entry is Entry && (entry as Entry).uploadServer == UploadServerType.UPLOAD_TO_TASK &&
+ ex.message == ERROR_FILE_SIZE_EXCEED -> {
+ bus.send(Error(context.getString(R.string.error_file_size_exceed)))
+ }
+
+ entry is Entry && (entry as Entry).uploadServer == UploadServerType.UPLOAD_TO_PROCESS &&
+ ex.message == ERROR_FILE_SIZE_EXCEED -> {
+ bus.send(Error(context.getString(R.string.error_file_size_exceed_10mb, MAX_FILE_SIZE_10)))
+ }
}
} catch (ex: Exception) {
sendAnalytics(false)
@@ -69,6 +76,7 @@ interface Action {
bus.send(Error(context.getString(R.string.error_duplicate_folder)))
}
}
+
else -> bus.send(Error(context.getString(R.string.action_generic_error)))
}
}
@@ -106,6 +114,7 @@ interface Action {
bus.send(Error(context.getString(R.string.error_duplicate_folder)))
}
}
+
else -> bus.send(Error(context.getString(R.string.action_generic_error)))
}
}
diff --git a/actions/src/main/kotlin/com/alfresco/content/actions/ActionCaptureMedia.kt b/actions/src/main/kotlin/com/alfresco/content/actions/ActionCaptureMedia.kt
index 2cf1f6614..333cd3553 100644
--- a/actions/src/main/kotlin/com/alfresco/content/actions/ActionCaptureMedia.kt
+++ b/actions/src/main/kotlin/com/alfresco/content/actions/ActionCaptureMedia.kt
@@ -41,6 +41,7 @@ data class ActionCaptureMedia(
item.description,
item.mimeType,
entry.uploadServer,
+ observerId = entry.observerID,
)
}
repository.setTotalTransferSize(result.size)
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..9e85af298 100644
--- a/actions/src/main/kotlin/com/alfresco/content/actions/ActionUploadFiles.kt
+++ b/actions/src/main/kotlin/com/alfresco/content/actions/ActionUploadFiles.kt
@@ -5,6 +5,8 @@ import android.view.View
import androidx.documentfile.provider.DocumentFile
import com.alfresco.content.ContentPickerFragment
import com.alfresco.content.GetMultipleContents
+import com.alfresco.content.GetMultipleContents.Companion.MAX_FILE_SIZE_10
+import com.alfresco.content.GetMultipleContents.Companion.MAX_FILE_SIZE_100
import com.alfresco.content.actions.Action.Companion.ERROR_FILE_SIZE_EXCEED
import com.alfresco.content.data.Entry
import com.alfresco.content.data.EventName
@@ -28,17 +30,18 @@ data class ActionUploadFiles(
private val repository = OfflineRepository()
override suspend fun execute(context: Context): Entry {
- val result = ContentPickerFragment.pickItems(context, MIME_TYPES)
+ val result = ContentPickerFragment.pickItems(context, MIME_TYPES, entry.isMultiple)
if (result.isNotEmpty()) {
when (entry.uploadServer) {
UploadServerType.UPLOAD_TO_TASK, UploadServerType.UPLOAD_TO_PROCESS -> {
result.forEach {
val fileLength = DocumentFile.fromSingleUri(context, it)?.length() ?: 0L
- if (GetMultipleContents.isFileSizeExceed(fileLength)) {
+ if (GetMultipleContents.isFileSizeExceed(fileLength, if (entry.observerID.isNotEmpty()) MAX_FILE_SIZE_10 else MAX_FILE_SIZE_100)) {
throw CancellationException(ERROR_FILE_SIZE_EXCEED)
}
}
}
+
else -> {}
}
withContext(Dispatchers.IO) {
@@ -48,6 +51,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..1010a159a 100644
--- a/actions/src/main/kotlin/com/alfresco/content/actions/ActionUploadMedia.kt
+++ b/actions/src/main/kotlin/com/alfresco/content/actions/ActionUploadMedia.kt
@@ -25,17 +25,18 @@ data class ActionUploadMedia(
private val repository = OfflineRepository()
override suspend fun execute(context: Context): Entry {
- val result = ContentPickerFragment.pickItems(context, MIME_TYPES)
+ val result = ContentPickerFragment.pickItems(context, MIME_TYPES, entry.isMultiple)
if (result.isNotEmpty()) {
when (entry.uploadServer) {
UploadServerType.UPLOAD_TO_TASK, UploadServerType.UPLOAD_TO_PROCESS -> {
result.forEach {
val fileLength = DocumentFile.fromSingleUri(context, it)?.length() ?: 0L
- if (GetMultipleContents.isFileSizeExceed(fileLength)) {
+ if (GetMultipleContents.isFileSizeExceed(fileLength, if (entry.observerID.isNotEmpty()) GetMultipleContents.MAX_FILE_SIZE_10 else GetMultipleContents.MAX_FILE_SIZE_100)) {
throw CancellationException(ERROR_FILE_SIZE_EXCEED)
}
}
}
+
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/actions/src/main/kotlin/com/alfresco/content/actions/CreateActionsSheet.kt b/actions/src/main/kotlin/com/alfresco/content/actions/CreateActionsSheet.kt
index 1daf5dad0..7d3d06ed1 100644
--- a/actions/src/main/kotlin/com/alfresco/content/actions/CreateActionsSheet.kt
+++ b/actions/src/main/kotlin/com/alfresco/content/actions/CreateActionsSheet.kt
@@ -2,6 +2,7 @@ package com.alfresco.content.actions
import android.content.Context
import android.os.Bundle
+import android.preference.PreferenceManager
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -83,6 +84,16 @@ class CreateActionsSheet : BottomSheetDialogFragment(), MavericksView {
private val viewModel: ActionCreateViewModel by fragmentViewModel()
private lateinit var binding: SheetActionCreateBinding
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ withState(viewModel) {
+ val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
+ val editor = sharedPrefs.edit()
+ editor.putBoolean(Settings.IS_PROCESS_UPLOAD_KEY, it.parent.observerID.isNotEmpty())
+ editor.apply()
+ }
+ }
+
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
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 54a532028..fb5dcb263 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.browse.processes.ProcessDetailActivity"),
+ Class.forName("com.alfresco.content.app.activity.ProcessActivity"),
)
intent.putExtra(Mavericks.KEY_ARG, processEntry)
startActivity(intent)
diff --git a/app/build.gradle b/app/build.gradle
index c476d02c1..5d4375460 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,4 +1,4 @@
-plugins{
+plugins {
id('com.android.application')
id('kotlin-android')
id('kotlin-kapt')
@@ -54,6 +54,8 @@ android {
viewBinding = true
}
+
+
compileOptions {
coreLibraryDesugaringEnabled true
}
@@ -98,7 +100,7 @@ dependencies {
implementation project(':viewer')
implementation project(':shareextension')
implementation project(':move')
-
+ implementation project(':process-app')
implementation project(':data')
implementation libs.alfresco.content
@@ -119,6 +121,8 @@ dependencies {
implementation libs.coil.core
implementation libs.gson
+ implementation libs.compose.runtime
+ implementation libs.constraintlayout
coreLibraryDesugaring libs.android.desugar
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 95b770bf0..5f1323c85 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -5,7 +5,8 @@
-
+
+
+
+
+
+
+
+
+
@@ -89,9 +101,8 @@
-
diff --git a/app/src/main/java/com/alfresco/content/app/activity/MainActivity.kt b/app/src/main/java/com/alfresco/content/app/activity/MainActivity.kt
index 492d561d2..980802138 100644
--- a/app/src/main/java/com/alfresco/content/app/activity/MainActivity.kt
+++ b/app/src/main/java/com/alfresco/content/app/activity/MainActivity.kt
@@ -114,6 +114,10 @@ class MainActivity : AppCompatActivity(), MavericksView, ActionMode.Callback {
editor.putBoolean(IS_PROCESS_ENABLED_KEY, it)
editor.apply()
}
+
+ if (savedInstanceState != null && viewModel.entriesMultiSelection.isNotEmpty()) {
+ enableMultiSelection(viewModel.entriesMultiSelection)
+ }
}
override fun onStart() {
@@ -132,11 +136,13 @@ class MainActivity : AppCompatActivity(), MavericksView, ActionMode.Callback {
MainActivityViewModel.NavigationMode.FOLDER -> {
bottomNav.selectedItemId = R.id.nav_browse
}
+
MainActivityViewModel.NavigationMode.FILE -> {
removeShareData()
if (!isNewIntent) checkLogin(data)
navigateToViewer(data)
}
+
MainActivityViewModel.NavigationMode.LOGIN -> navigateToLogin(data)
MainActivityViewModel.NavigationMode.DEFAULT -> checkLogin(data)
}
@@ -295,6 +301,7 @@ class MainActivity : AppCompatActivity(), MavericksView, ActionMode.Callback {
if (bottomNav.isVisible) {
bottomNav.visibility = View.GONE
}
+ MultiSelection.clearSelectionChangedFlow.tryEmit(false)
}
private fun disableMultiSelection() {
@@ -319,7 +326,9 @@ class MainActivity : AppCompatActivity(), MavericksView, ActionMode.Callback {
}
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
- if (viewModel.path.isNotEmpty() && viewModel.path == getString(com.alfresco.content.browse.R.string.nav_path_trash)) {
+ if ((viewModel.path.isNotEmpty() && viewModel.path == getString(com.alfresco.content.browse.R.string.nav_path_trash)) ||
+ navHostFragment?.navController?.currentDestination?.id == R.id.nav_offline
+ ) {
menu?.findItem(R.id.move)?.isVisible = false
}
return true
diff --git a/app/src/main/java/com/alfresco/content/app/activity/MoveActivity.kt b/app/src/main/java/com/alfresco/content/app/activity/MoveActivity.kt
index f71e467db..aac5dfb98 100644
--- a/app/src/main/java/com/alfresco/content/app/activity/MoveActivity.kt
+++ b/app/src/main/java/com/alfresco/content/app/activity/MoveActivity.kt
@@ -40,7 +40,7 @@ class MoveActivity : AppCompatActivity(), MavericksView {
setContentView(R.layout.activity_move)
if (intent.extras != null) {
- entryObj = intent.getParcelableExtra(ENTRY_OBJ_KEY) as Entry?
+ entryObj = intent.getParcelableExtra(ENTRY_OBJ_KEY) as? Entry?
}
configure()
@@ -54,7 +54,9 @@ class MoveActivity : AppCompatActivity(), MavericksView {
val graph = navController.navInflater.inflate(R.navigation.nav_move_paths)
graph.setStartDestination(R.id.nav_move)
val bundle = Bundle().apply {
- putParcelable(ENTRY_OBJ_KEY, entryObj)
+ if (entryObj != null) {
+ putParcelable(ENTRY_OBJ_KEY, entryObj)
+ }
}
navController.setGraph(graph, bundle)
setupActionToasts()
diff --git a/app/src/main/java/com/alfresco/content/app/activity/ProcessActivity.kt b/app/src/main/java/com/alfresco/content/app/activity/ProcessActivity.kt
new file mode 100644
index 000000000..640c5222f
--- /dev/null
+++ b/app/src/main/java/com/alfresco/content/app/activity/ProcessActivity.kt
@@ -0,0 +1,55 @@
+package com.alfresco.content.app.activity
+
+import android.content.pm.ActivityInfo
+import android.os.Bundle
+import androidx.lifecycle.lifecycleScope
+import androidx.navigation.fragment.NavHostFragment
+import com.airbnb.mvrx.MavericksView
+import com.alfresco.content.actions.Action
+import com.alfresco.content.app.R
+import com.alfresco.content.app.databinding.ActivityProcessBinding
+import com.alfresco.content.app.widget.ActionBarController
+import com.alfresco.content.app.widget.ActionBarLayout
+import com.alfresco.content.common.BaseActivity
+
+class ProcessActivity : BaseActivity(), MavericksView {
+
+ private lateinit var binding: ActivityProcessBinding
+ private lateinit var actionBarController: ActionBarController
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityProcessBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ if (!resources.getBoolean(R.bool.isTablet)) {
+ requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+ }
+
+ configureNav()
+ setupActionToasts()
+ }
+
+ private fun configureNav() {
+ val navHostFragment =
+ supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
+ val navController = navHostFragment.navController
+ val inflater = navController.navInflater
+ val graph = inflater.inflate(R.navigation.nav_process_paths)
+ navController.setGraph(graph, intent.extras)
+ val actionBarLayout = findViewById(R.id.toolbar)
+ actionBarController = ActionBarController(actionBarLayout)
+ actionBarController.setupActionBar(this, navController)
+
+ actionBarLayout.toolbar.setNavigationOnClickListener { onBackPressed() }
+ }
+
+ private fun setupActionToasts() = Action.showActionToasts(
+ lifecycleScope,
+ binding.parentView,
+ binding.bottomView,
+ )
+
+ override fun invalidate() {
+ }
+}
diff --git a/app/src/main/res/layout/activity_process.xml b/app/src/main/res/layout/activity_process.xml
new file mode 100644
index 000000000..66a7c0bd7
--- /dev/null
+++ b/app/src/main/res/layout/activity_process.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/navigation/nav_move_paths.xml b/app/src/main/res/navigation/nav_move_paths.xml
index 1af1022d5..278b71599 100644
--- a/app/src/main/res/navigation/nav_move_paths.xml
+++ b/app/src/main/res/navigation/nav_move_paths.xml
@@ -1,7 +1,7 @@
+
+ android:defaultValue=""
+ app:argType="string" />
-
+
+
@@ -49,19 +57,25 @@
+
+ android:defaultValue=""
+ app:argType="string" />
-
+
@@ -71,23 +85,31 @@
android:label="SearchFragment">
+ android:defaultValue=""
+ app:argType="string" />
+ android:defaultValue="false"
+ app:argType="boolean" />
+
+
+
diff --git a/auth/build.gradle b/auth/build.gradle
index 462f45c73..b500d5a40 100644
--- a/auth/build.gradle
+++ b/auth/build.gradle
@@ -16,6 +16,7 @@ dependencies {
implementation project(':base-ui')
implementation project(':theme')
implementation project(':data')
+ implementation project(':common')
implementation libs.kotlin.stdlib
diff --git a/auth/src/main/AndroidManifest.xml b/auth/src/main/AndroidManifest.xml
index 4b01ccd26..f14c8614f 100644
--- a/auth/src/main/AndroidManifest.xml
+++ b/auth/src/main/AndroidManifest.xml
@@ -1,5 +1,9 @@
+
+
+
+
diff --git a/auth/src/main/kotlin/com/alfresco/ui/SplashActivity.kt b/auth/src/main/kotlin/com/alfresco/ui/SplashActivity.kt
index 622d3b197..fa67299cb 100644
--- a/auth/src/main/kotlin/com/alfresco/ui/SplashActivity.kt
+++ b/auth/src/main/kotlin/com/alfresco/ui/SplashActivity.kt
@@ -1,11 +1,13 @@
package com.alfresco.ui
import android.content.Intent
+import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import androidx.appcompat.app.AppCompatActivity
import com.alfresco.android.aims.R
+import com.alfresco.content.common.SharedURLParser
import com.alfresco.content.data.rooted.CheckForRootWorker
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@@ -20,9 +22,16 @@ abstract class SplashActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ // Use setHideOverlayWindows method
+ window.setHideOverlayWindows(true);
+ }
if (intent.data != null && intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY == 0) {
// Handle the url passed through the intent
- entryId = getEntryIdFromShareURL()
+ val urlData = SharedURLParser().getEntryIdFromShareURL(intent.data.toString())
+ isRemoteFolder = urlData.third
+ entryId = urlData.second
+ isPreview = urlData.first
}
setContentView(R.layout.activity_alfresco_splash)
}
diff --git a/browse/build.gradle b/browse/build.gradle
index d50ad6be8..eee08ae6d 100644
--- a/browse/build.gradle
+++ b/browse/build.gradle
@@ -32,6 +32,7 @@ dependencies {
implementation project(':viewer-text')
implementation project(':viewer')
implementation project(':component')
+ implementation project(':process-app')
implementation libs.alfresco.content
implementation libs.alfresco.process
diff --git a/browse/src/main/kotlin/com/alfresco/content/browse/BrowseFragment.kt b/browse/src/main/kotlin/com/alfresco/content/browse/BrowseFragment.kt
index 409179ec1..dd03897ba 100644
--- a/browse/src/main/kotlin/com/alfresco/content/browse/BrowseFragment.kt
+++ b/browse/src/main/kotlin/com/alfresco/content/browse/BrowseFragment.kt
@@ -48,18 +48,21 @@ data class BrowseArgs(
val path: String,
val id: String?,
val moveId: String,
+ val isProcess: Boolean,
val title: String?,
) : Parcelable {
companion object {
private const val PATH_KEY = "path"
private const val ID_KEY = "id"
private const val TITLE_KEY = "title"
+ private const val IS_PROCESS_KEY = "isProcess"
fun with(args: Bundle): BrowseArgs {
return BrowseArgs(
args.getString(PATH_KEY, ""),
args.getString(ID_KEY, null),
args.getString(MOVE_ID_KEY, ""),
+ args.getBoolean(IS_PROCESS_KEY, false),
args.getString(TITLE_KEY, null),
)
}
@@ -194,7 +197,7 @@ class BrowseFragment : ListFragment() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.search -> {
- findNavController().navigateToContextualSearch(args.id ?: "", args.title ?: "", false)
+ findNavController().navigateToContextualSearch(args.id ?: "", args.title ?: "", isExtension = false)
true
}
diff --git a/browse/src/main/kotlin/com/alfresco/content/browse/BrowseViewModel.kt b/browse/src/main/kotlin/com/alfresco/content/browse/BrowseViewModel.kt
index 2ee160976..5beefd752 100644
--- a/browse/src/main/kotlin/com/alfresco/content/browse/BrowseViewModel.kt
+++ b/browse/src/main/kotlin/com/alfresco/content/browse/BrowseViewModel.kt
@@ -16,6 +16,7 @@ import com.alfresco.content.actions.ActionRemoveFavorite
import com.alfresco.content.actions.ActionRestore
import com.alfresco.content.actions.ActionUploadExtensionFiles
import com.alfresco.content.data.AnalyticsManager
+import com.alfresco.content.data.AttachFolderSearchData
import com.alfresco.content.data.BrowseRepository
import com.alfresco.content.data.Entry
import com.alfresco.content.data.FavoritesRepository
@@ -29,7 +30,10 @@ import com.alfresco.content.data.TrashCanRepository
import com.alfresco.content.listview.ListViewModel
import com.alfresco.content.listview.ListViewState
import com.alfresco.coroutines.asFlow
+import com.alfresco.events.EventBus
import com.alfresco.events.on
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
@@ -354,6 +358,12 @@ class BrowseViewModel(
)
}
+ fun setSearchResult(entry: Entry) {
+ CoroutineScope(Dispatchers.Main).launch {
+ EventBus.default.send(AttachFolderSearchData(entry))
+ }
+ }
+
override fun resetMaxLimitError() = setState { copy(maxLimitReachedForMultiSelection = false) }
companion object : MavericksViewModelFactory {
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..9140abe80 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.ProcessBaseFragment.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/ProcessDetailActivity.kt b/browse/src/main/kotlin/com/alfresco/content/browse/processes/ProcessDetailActivity.kt
index 010f45512..21b717a12 100644
--- a/browse/src/main/kotlin/com/alfresco/content/browse/processes/ProcessDetailActivity.kt
+++ b/browse/src/main/kotlin/com/alfresco/content/browse/processes/ProcessDetailActivity.kt
@@ -35,7 +35,7 @@ class ProcessDetailActivity : AppCompatActivity() {
private fun setupActionToasts() = Action.showActionToasts(
lifecycleScope,
- binding.root,
+ binding.parentView,
binding.bottomView,
)
}
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 e2f6a070b..f4bee7324 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
@@ -9,7 +9,6 @@ import com.airbnb.mvrx.Success
import com.airbnb.mvrx.ViewModelContext
import com.alfresco.content.actions.Action
import com.alfresco.content.actions.ActionOpenWith
-import com.alfresco.content.actions.ActionUpdateNameDescription
import com.alfresco.content.common.EntryListener
import com.alfresco.content.component.ComponentMetaData
import com.alfresco.content.data.Entry
@@ -48,9 +47,9 @@ class ProcessDetailViewModel(
entryListener?.onEntryCreated(it.entry)
}
}
- viewModelScope.on {
- setState { copy(parent = it.entry as ProcessEntry) }
- }
+// viewModelScope.on {
+// setState { copy(parent = it.entry as ProcessEntry) }
+// }
fetchUserProfile()
fetchAccountInfo()
@@ -192,7 +191,7 @@ class ProcessDetailViewModel(
fun startWorkflow() = withState { state ->
val items = state.listContents.joinToString(separator = ",") { it.id }
viewModelScope.launch {
- repository::startWorkflow.asFlow(state.parent, items).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 42327d8c2..47d725c4a 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.processes.list.UpdateProcessData
-import com.alfresco.content.browse.tasks.list.UpdateTasksData
+import com.alfresco.content.process.ui.models.UpdateProcessData
+import com.alfresco.content.process.ui.models.UpdateTasksData
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 390d2ce3b..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,6 +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.models.UpdateProcessData
import com.alfresco.coroutines.asFlow
import com.alfresco.events.on
import kotlinx.coroutines.launch
@@ -117,8 +118,3 @@ class ProcessesViewModel(
fetchInitial()
}
}
-
-/**
- * Mark as UpdateProcessData data class
- */
-data class UpdateProcessData(val isRefresh: Boolean)
diff --git a/browse/src/main/kotlin/com/alfresco/content/browse/processes/status/TaskStatusFragment.kt b/browse/src/main/kotlin/com/alfresco/content/browse/processes/status/TaskStatusFragment.kt
index 845de95b2..9f9e495e7 100644
--- a/browse/src/main/kotlin/com/alfresco/content/browse/processes/status/TaskStatusFragment.kt
+++ b/browse/src/main/kotlin/com/alfresco/content/browse/processes/status/TaskStatusFragment.kt
@@ -103,13 +103,13 @@ class TaskStatusFragment : Fragment(), MavericksView {
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
- inflater.inflate(R.menu.menu_task_status, menu)
+ inflater.inflate(R.menu.menu_process, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_save -> {
- viewModel.saveForm(binding.commentInput.text.toString().trim())
+// viewModel.saveForm(binding.commentInput.text.toString().trim())
true
}
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..67782d14a 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.ProcessBaseFragment.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..4eab3ea9d 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.ProcessBaseFragment.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/TaskDetailExtension.kt b/browse/src/main/kotlin/com/alfresco/content/browse/tasks/detail/TaskDetailExtension.kt
index 7c90e747e..fc2d9fa30 100644
--- a/browse/src/main/kotlin/com/alfresco/content/browse/tasks/detail/TaskDetailExtension.kt
+++ b/browse/src/main/kotlin/com/alfresco/content/browse/tasks/detail/TaskDetailExtension.kt
@@ -17,11 +17,9 @@ import com.alfresco.content.common.updatePriorityView
import com.alfresco.content.component.ComponentData
import com.alfresco.content.component.ComponentType
import com.alfresco.content.component.DatePickerBuilder
-import com.alfresco.content.data.AnalyticsManager
import com.alfresco.content.data.TaskEntry
import com.alfresco.content.formatDate
import com.alfresco.content.getFormattedDate
-import com.alfresco.content.getLocalizedName
import com.alfresco.content.parseDate
import com.alfresco.content.setSafeOnClickListener
import com.google.android.material.button.MaterialButton
@@ -57,13 +55,12 @@ internal fun TaskDetailFragment.updateTaskDetailUI(isEdit: Boolean) = withState(
internal fun TaskDetailFragment.enableTaskFormUI() = withState(viewModel) { state ->
binding.clComment.visibility = View.GONE
- binding.clIdentifier.visibility = View.GONE
- binding.iconStatusNav.visibility = View.VISIBLE
- binding.iconStatus.setImageResource(R.drawable.ic_task_status_star)
+ binding.clIdentifier.visibility = View.VISIBLE
+// binding.iconStatus.setImageResource(R.drawable.ic_task_status_star)
- binding.clStatus.setSafeOnClickListener {
+ /*binding.clStatus.setSafeOnClickListener {
findNavController().navigate(R.id.action_nav_task_detail_to_nav_task_status)
- }
+ }*/
}
internal fun TaskDetailFragment.setTaskDetailAfterResponse(dataObj: TaskEntry) = withState(viewModel) { state ->
@@ -92,43 +89,15 @@ internal fun TaskDetailFragment.setTaskDetailAfterResponse(dataObj: TaskEntry) =
(binding.clDueDate.layoutParams as ConstraintLayout.LayoutParams).apply {
topMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 24f, resources.displayMetrics).toInt()
}
- binding.clStatus.visibility = if (viewModel.isWorkflowTask && viewModel.hasTaskStatusEnabled(state)) {
- binding.tvStatusValue.text = dataObj.taskFormStatus
- View.VISIBLE
- } else View.GONE
+ binding.clStatus.visibility = View.GONE
} else {
binding.clCompleted.visibility = View.GONE
(binding.clDueDate.layoutParams as ConstraintLayout.LayoutParams).apply {
topMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0f, resources.displayMetrics).toInt()
}
- binding.clStatus.visibility = if (viewModel.isWorkflowTask && !viewModel.hasTaskStatusEnabled(state)) {
- View.GONE
- } else {
- View.VISIBLE
- }
-
- when (dataObj.memberOfCandidateGroup) {
- true -> {
- if (dataObj.assignee?.id == null || dataObj.assignee?.id == 0) {
- makeClaimButton()
- } else if (viewModel.isAssigneeAndLoggedInSame(dataObj.assignee)) {
- menuDetail.findItem(R.id.action_release).isVisible = true
- makeOutcomes()
- }
- }
-
- else -> {
- if (viewModel.isStartedByAndLoggedInSame(dataObj.processInstanceStartUserId) ||
- viewModel.isAssigneeAndLoggedInSame(dataObj.assignee)
- ) {
- makeOutcomes()
- }
- }
- }
+ binding.clStatus.visibility = View.VISIBLE
- binding.tvStatusValue.text = if (!viewModel.isWorkflowTask) {
- getString(R.string.status_active)
- } else dataObj.taskFormStatus
+ binding.tvStatusValue.text = getString(R.string.status_active)
}
}
}
@@ -260,34 +229,6 @@ internal fun TaskDetailFragment.showTitleDescriptionComponent() = withState(view
}
}
-internal fun TaskDetailFragment.makeOutcomes() = withState(viewModel) { state ->
- if (binding.parentOutcomes.childCount == 0) {
- state.parent?.outcomes?.forEach { dataObj ->
- val button = if (dataObj.outcome.lowercase() == "reject") {
- this.layoutInflater.inflate(R.layout.view_layout_negative_outcome, binding.parentOutcomes, false) as MaterialButton
- } else {
- this.layoutInflater.inflate(R.layout.view_layout_positive_outcome, binding.parentOutcomes, false) as MaterialButton
- }
- button.text = requireContext().getLocalizedName(dataObj.name)
- button.setOnClickListener {
- withState(viewModel) { newState ->
- if (viewModel.hasTaskStatusEnabled(newState) && !viewModel.hasTaskStatusValue(newState)
- ) {
- showSnackar(
- binding.root,
- getString(R.string.error_select_status),
- )
- } else {
- AnalyticsManager().taskFiltersEvent(dataObj.outcome)
- viewModel.actionOutcome(dataObj.outcome)
- }
- }
- }
- binding.parentOutcomes.addView(button)
- }
- }
-}
-
internal fun TaskDetailFragment.makeClaimButton() = withState(viewModel) { state ->
if (binding.parentOutcomes.childCount == 0) {
val button = this.layoutInflater.inflate(R.layout.view_layout_positive_outcome, binding.parentOutcomes, false) as MaterialButton
diff --git a/browse/src/main/kotlin/com/alfresco/content/browse/tasks/detail/TaskDetailFragment.kt b/browse/src/main/kotlin/com/alfresco/content/browse/tasks/detail/TaskDetailFragment.kt
index bb51fb1d7..81889c6c4 100644
--- a/browse/src/main/kotlin/com/alfresco/content/browse/tasks/detail/TaskDetailFragment.kt
+++ b/browse/src/main/kotlin/com/alfresco/content/browse/tasks/detail/TaskDetailFragment.kt
@@ -126,8 +126,10 @@ class TaskDetailFragment : BaseDetailFragment(), MavericksView, EntryListener {
}
}
} else {
- inflater.inflate(R.menu.menu_workflow_task_detail, menu)
+ inflater.inflate(R.menu.menu_task_detail, menu)
menuDetail = menu
+ menu.findItem(R.id.action_done).isVisible = false
+ menu.findItem(R.id.action_edit).isVisible = true
}
}
diff --git a/browse/src/main/kotlin/com/alfresco/content/browse/tasks/detail/TaskDetailViewModel.kt b/browse/src/main/kotlin/com/alfresco/content/browse/tasks/detail/TaskDetailViewModel.kt
index 78354150e..b22376a2f 100644
--- a/browse/src/main/kotlin/com/alfresco/content/browse/tasks/detail/TaskDetailViewModel.kt
+++ b/browse/src/main/kotlin/com/alfresco/content/browse/tasks/detail/TaskDetailViewModel.kt
@@ -61,9 +61,9 @@ class TaskDetailViewModel(
if (!isWorkflowTask) {
getComments()
getContents()
- viewModelScope.on {
- setState { copy(parent = it.entry as TaskEntry) }
- }
+ }
+ viewModelScope.on {
+ setState { copy(parent = it.entry as TaskEntry) }
}
}
@@ -88,7 +88,6 @@ class TaskDetailViewModel(
is Fail -> copy(request = Fail(it.error))
is Success -> {
val updateState = update(it())
- if (isWorkflowTask) getTaskForms(updateState)
updateState.copy(request = Success(it()))
}
@@ -377,29 +376,6 @@ class TaskDetailViewModel(
}
}
- private fun getTaskForms(oldState: TaskDetailViewState) = withState { state ->
- requireNotNull(oldState.parent)
- viewModelScope.launch {
- repository::getTaskForm.asFlow(oldState.parent.id).execute {
- when (it) {
- is Loading -> copy(requestTaskForm = Loading())
- is Fail -> {
- it.error.printStackTrace()
- copy(requestTaskForm = Fail(it.error))
- }
-
- is Success -> {
- update(oldState.parent, it()).copy(requestTaskForm = Success(it()))
- }
-
- else -> {
- this
- }
- }
- }
- }
- }
-
/**
* update the status of task related to workflow
*/
@@ -421,56 +397,6 @@ class TaskDetailViewModel(
}
}
- /**
- * execute the outcome api
- */
- fun actionOutcome(outcome: String) = withState { state ->
- requireNotNull(state.parent)
- viewModelScope.launch {
- repository::actionOutcomes.asFlow(outcome, state.parent).execute {
- when (it) {
- is Loading -> copy(requestOutcomes = Loading())
- is Fail -> {
- copy(requestOutcomes = Fail(it.error))
- }
-
- is Success -> {
- copy(requestOutcomes = Success(it()))
- }
-
- else -> {
- this
- }
- }
- }
- }
- }
-
- /**
- * execute the save-form api
- */
- fun saveForm(comment: String) = withState { state ->
- requireNotNull(state.parent)
- viewModelScope.launch {
- repository::saveForm.asFlow(state.parent, comment).execute {
- when (it) {
- is Loading -> copy(requestSaveForm = Loading())
- is Fail -> {
- copy(requestSaveForm = Fail(it.error))
- }
-
- is Success -> {
- copy(requestSaveForm = Success(it()))
- }
-
- else -> {
- this
- }
- }
- }
- }
- }
-
/**
* execute API to claim the task
*/
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 80aab9ea8..9abdc15b4 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
@@ -1,11 +1,11 @@
package com.alfresco.content.browse.tasks.detail
import com.alfresco.content.actions.Action
-import com.alfresco.content.browse.processes.list.UpdateProcessData
-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.models.UpdateProcessData
+import com.alfresco.content.process.ui.models.UpdateTasksData
import com.alfresco.events.EventBus
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@@ -74,5 +74,5 @@ internal fun TaskDetailViewModel.removeTaskEntries(state: TaskDetailViewState) {
internal fun TaskDetailViewModel.isAssigneeAndLoggedInSame(assignee: UserGroupDetails?) = getAPSUser().id == assignee?.id
internal fun TaskDetailViewModel.isStartedByAndLoggedInSame(initiatorId: String?) = getAPSUser().id.toString() == initiatorId
-internal fun TaskDetailViewModel.isTaskFormAndDetailRequestCompleted(state: TaskDetailViewState) = isWorkflowTask && state.requestTaskForm.complete
+internal fun TaskDetailViewModel.isTaskFormAndDetailRequestCompleted(state: TaskDetailViewState) = isWorkflowTask && state.request.complete
internal fun TaskDetailViewModel.isTaskDetailRequestCompleted(state: TaskDetailViewState) = !isWorkflowTask && state.request.complete
diff --git a/browse/src/main/kotlin/com/alfresco/content/browse/tasks/list/TasksFragment.kt b/browse/src/main/kotlin/com/alfresco/content/browse/tasks/list/TasksFragment.kt
index 1a93ed6ef..6ae9b4b42 100644
--- a/browse/src/main/kotlin/com/alfresco/content/browse/tasks/list/TasksFragment.kt
+++ b/browse/src/main/kotlin/com/alfresco/content/browse/tasks/list/TasksFragment.kt
@@ -26,6 +26,7 @@ import com.alfresco.content.data.AnalyticsManager
import com.alfresco.content.data.EventName
import com.alfresco.content.data.PageView
import com.alfresco.content.data.ParentEntry
+import com.alfresco.content.data.ProcessEntry
import com.alfresco.content.data.TaskEntry
import com.alfresco.content.data.TaskFilterData
import com.alfresco.content.hideSoftInput
@@ -189,11 +190,21 @@ class TasksFragment : TaskListFragment() {
) = continuation.resume(ComponentMetaData(name = name, query = query, queryMap = queryMap))
override fun onItemClicked(entry: TaskEntry) {
- val intent = Intent(
- requireActivity(),
- Class.forName("com.alfresco.content.app.activity.TaskViewerActivity"),
- )
- intent.putExtra(Mavericks.KEY_ARG, entry)
+ val intent = if (entry.processInstanceId != null) {
+ Intent(
+ requireActivity(),
+ Class.forName("com.alfresco.content.app.activity.ProcessActivity"),
+ ).apply {
+ putExtra(Mavericks.KEY_ARG, ProcessEntry.with(entry))
+ }
+ } else {
+ Intent(
+ requireActivity(),
+ Class.forName("com.alfresco.content.app.activity.TaskViewerActivity"),
+ ).apply {
+ putExtra(Mavericks.KEY_ARG, entry)
+ }
+ }
startActivity(intent)
}
}
diff --git a/browse/src/main/kotlin/com/alfresco/content/browse/tasks/list/TasksViewModel.kt b/browse/src/main/kotlin/com/alfresco/content/browse/tasks/list/TasksViewModel.kt
index 159d2e978..193c10622 100644
--- a/browse/src/main/kotlin/com/alfresco/content/browse/tasks/list/TasksViewModel.kt
+++ b/browse/src/main/kotlin/com/alfresco/content/browse/tasks/list/TasksViewModel.kt
@@ -16,6 +16,7 @@ import com.alfresco.content.data.payloads.TaskProcessFiltersPayload
import com.alfresco.content.getLocalizedName
import com.alfresco.content.listview.tasks.TaskListViewModel
import com.alfresco.content.listview.tasks.TaskListViewState
+import com.alfresco.content.process.ui.models.UpdateTasksData
import com.alfresco.coroutines.asFlow
import com.alfresco.events.on
import kotlinx.coroutines.GlobalScope
@@ -233,8 +234,3 @@ class TasksViewModel(
) = TasksViewModel(state, viewModelContext.activity, TaskRepository())
}
}
-
-/**
- * Mark as UpdateTasksData data class
- */
-data class UpdateTasksData(val isRefresh: Boolean)
diff --git a/browse/src/main/res/layout/activity_task_viewer.xml b/browse/src/main/res/layout/activity_task_viewer.xml
index 12c37f652..6f642ab0a 100644
--- a/browse/src/main/res/layout/activity_task_viewer.xml
+++ b/browse/src/main/res/layout/activity_task_viewer.xml
@@ -1,5 +1,6 @@
@@ -148,7 +148,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scaleType="fitCenter"
- android:src="@drawable/ic_edit"
+ android:src="@drawable/ic_edit_blue"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_weight="1"
@@ -230,7 +230,7 @@
android:layout_height="wrap_content"
android:contentDescription="@string/icon_due_date_edit"
android:scaleType="fitCenter"
- android:src="@drawable/ic_edit"
+ android:src="@drawable/ic_edit_blue"
android:visibility="invisible" />
@@ -305,7 +305,7 @@
android:layout_height="wrap_content"
android:contentDescription="@string/icon_priority_edit"
android:scaleType="fitCenter"
- android:src="@drawable/ic_edit"
+ android:src="@drawable/ic_edit_blue"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_weight="1"
@@ -372,7 +372,7 @@
android:layout_height="wrap_content"
android:contentDescription="@string/icon_assignee_edit"
android:scaleType="fitCenter"
- android:src="@drawable/ic_edit"
+ android:src="@drawable/ic_edit_blue"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_weight="1"
@@ -502,7 +502,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scaleType="fitCenter"
- android:src="@drawable/ic_edit"
+ android:src="@drawable/ic_edit_blue"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_weight="1"
diff --git a/browse/src/main/res/menu/menu_browse_folder.xml b/browse/src/main/res/menu/menu_browse_folder.xml
new file mode 100644
index 000000000..bdc62e839
--- /dev/null
+++ b/browse/src/main/res/menu/menu_browse_folder.xml
@@ -0,0 +1,13 @@
+
+
diff --git a/browse/src/main/res/navigation/nav_task_paths.xml b/browse/src/main/res/navigation/nav_task_paths.xml
index 6c0f05e99..d26488255 100644
--- a/browse/src/main/res/navigation/nav_task_paths.xml
+++ b/browse/src/main/res/navigation/nav_task_paths.xml
@@ -30,12 +30,12 @@
+ tools:layout="@layout/fragment_attach_files" />
+ tools:layout="@layout/fragment_attach_files" />
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,9 @@
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
@@ -79,7 +72,6 @@
Symbol \'Priorität bearbeiten\'
Symbol \'Fälligkeitsdatum löschen\'
Symbol \'Fälligkeitsdatum bearbeiten\'
- ...Alle anzeigen
Aufgabenfortschritt
Möchten Sie den Fortschritt speichern?
Als abgeschlossen markieren
@@ -88,9 +80,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.
@@ -101,7 +90,6 @@
Workflows sind für diesen Account nicht aktiviert.
Warnung
- Das Hochladen von Dateien ist in Bearbeitung. Tippen Sie auf Bestätigen, um ohne laufende Dateien fortzufahren.
Genehmigen
Ablehnen
Erneut zur Genehmigung senden
@@ -110,7 +98,6 @@
Gestartet von
Startdatum
Wird ausgeführt
- Workflow
Bitte wählen Sie einen zugewiesenen Benutzer
Zugewiesenen Benutzer auswählen
Wählen Sie den Status aus
diff --git a/browse/src/main/res/values-es/strings.xml b/browse/src/main/res/values-es/strings.xml
index c15d3a30c..1ffe07364 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,9 @@
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
@@ -79,7 +72,6 @@
Editar icono de prioridad
Borrar el icono de la fecha de vencimiento
Editar el icono de la fecha de vencimiento
- ...Ver todo
Progreso de la tarea
¿Desea guardar el progreso?
Marcar como completado
@@ -88,9 +80,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.
@@ -101,7 +90,6 @@
Flujos de trabajo no disponibles
Los flujos de trabajo aparecerán aquí cuando sean creados
Advertencia
- La carga de ficheros está en curso. Toque Confirmar para continuar sin los ficheros en curso.
Aprobar
Rechazar
Enviar para aprobación otra vez
@@ -110,7 +98,6 @@
Iniciado por
Fecha de inicio
Ejecutándose
- Flujo de trabajo
Por favor seleccionar usuario asignado
Seleccionar usuario asignado
Seleccionar estado
diff --git a/browse/src/main/res/values-fr/strings.xml b/browse/src/main/res/values-fr/strings.xml
index a986e392a..9542e53c8 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,9 @@
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é
@@ -79,7 +72,6 @@
Icône Modifier la priorité
Icône Effacer la date d\'échéance
Icône Modifier la date d\'échéance
- …Afficher tout
Progression de la tâche
Voulez-vous enregistrer la progression ?
Marquer comme terminé
@@ -88,9 +80,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é.
@@ -101,7 +90,6 @@
Les workflows ne sont pas activées pour ce compte.
Avertissement
- Le téléchargement des fichiers est en cours. Tapez sur Confirmer pour continuer sans fichiers en cours.
Approuver
Rejeter
Envoyer à nouveau pour approbation
@@ -110,7 +98,6 @@
Démarré par
Date de début
En cours
- Workflow
Veuillez sélectionner la personne assignée
Sélectionner la personne assignée
Sélectionnez le statut
diff --git a/browse/src/main/res/values-it/strings.xml b/browse/src/main/res/values-it/strings.xml
index 29dd1f0a6..68e404224 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,15 +62,9 @@
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.
- Annulla
- Conferma
Pulsante Invia
Nessuna descrizione
Icona Completato
@@ -79,7 +72,6 @@
Icona Modifica priorità
Icona Cancella scadenza
Icona Modifica scadenza
- ...Visualizza tutto
Stato di avanzamento compito
Vuoi salvare lo stato di avanzamento?
Contrassegna come completato
@@ -88,9 +80,6 @@
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.
@@ -101,7 +90,6 @@
I workflow non sono abilitati per questo account.
Avviso
- Il caricamento dei file è in corso. Toccare su Conferma per continuare senza questi file.
Approva
Respingi
Invia di nuovo per approvazione
@@ -110,7 +98,6 @@
Avviato da
Data di inizio
In esecuzione
- Workflow
Selezionare assegnatario
Selezionare assegnatario
Seleziona uno stato
diff --git a/browse/src/main/res/values-nl/strings.xml b/browse/src/main/res/values-nl/strings.xml
index 7c1751722..148702718 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,9 @@
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
@@ -79,7 +72,6 @@
Pictogram Prioriteit bewerken
Pictogram Vervaldatum wissen
Pictogram Vervaldatum bewerken
- ...Alle weergeven
Voortgang van de taak
Wilt u de voortgang opslaan?
Als voltooid markeren
@@ -88,9 +80,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.
@@ -101,7 +90,6 @@
Workflows niet beschikbaar!
Wanneer workflows zijn gemaakt, worden deze hier weergegeven.
Waarschuwing
- Er worden bestanden geüpload. Tik om te bevestigen dat u wilt doorgaan zonder de bestanden in uitvoering.
Goedkeuren
Afwijzen
Nogmaals verzenden voor goedkeuring
@@ -110,7 +98,6 @@
Gestart door
Begindatum
Wordt uitgevoerd
- Workflow
Selecteer toegewezen persoon
Selecteer toegewezen persoon
Please select status
diff --git a/browse/src/main/res/values/strings.xml b/browse/src/main/res/values/strings.xml
index cf040bced..9b3666425 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,9 @@
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
@@ -79,7 +72,6 @@
Edit priority icon
Clear due date icon
Edit due date icon
- …View all
Task Progress
Do you want to save progress?
Mark as complete
@@ -88,9 +80,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.
@@ -103,7 +92,6 @@
Workflows unavailable!
Workflows will appear here when created.
Warning
- Files uploading is in progress. Tap on Confirm to continue without in-progress files.
Approve
Reject
Send for Approval Again
@@ -112,7 +100,6 @@
Started By
Start Date
Running
- Workflow
Please select assignee
Select assignee
Please select status
diff --git a/build.gradle b/build.gradle
index 75298354f..e7f5bcdca 100644
--- a/build.gradle
+++ b/build.gradle
@@ -43,11 +43,11 @@ subprojects {
afterEvaluate {
if(it.hasProperty('android')) {
android {
- compileSdkVersion 33
+ compileSdkVersion 34
defaultConfig {
minSdkVersion 24
- targetSdkVersion 33
+ targetSdkVersion 34
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
diff --git a/capture/src/main/kotlin/com/alfresco/capture/CameraFragment.kt b/capture/src/main/kotlin/com/alfresco/capture/CameraFragment.kt
index 6739072f3..df956ee64 100644
--- a/capture/src/main/kotlin/com/alfresco/capture/CameraFragment.kt
+++ b/capture/src/main/kotlin/com/alfresco/capture/CameraFragment.kt
@@ -320,6 +320,7 @@ class CameraFragment : Fragment(), KeyHandler, MavericksView {
ImageCapture.OutputFileOptions.Builder(photoFile)
.setMetadata(viewModel.getMetaData()).build()
}
+
else -> {
ImageCapture.OutputFileOptions.Builder(photoFile).build()
}
@@ -399,7 +400,7 @@ class CameraFragment : Fragment(), KeyHandler, MavericksView {
layout.animatePreviewHide()
enableShutterButton(true)
if (length > 0L) {
- if (!GetMultipleContents.isFileSizeExceed(length)) {
+ if (!GetMultipleContents.isFileSizeExceed(length, if (viewModel.isProcessUpload) GetMultipleContents.MAX_FILE_SIZE_10 else GetMultipleContents.MAX_FILE_SIZE_100)) {
viewModel.onCaptureVideo(savedUri)
if (viewModel.isEnterprise()) {
requireActivity().runOnUiThread {
diff --git a/capture/src/main/kotlin/com/alfresco/capture/CaptureViewModel.kt b/capture/src/main/kotlin/com/alfresco/capture/CaptureViewModel.kt
index 6fa445bac..61177cefd 100644
--- a/capture/src/main/kotlin/com/alfresco/capture/CaptureViewModel.kt
+++ b/capture/src/main/kotlin/com/alfresco/capture/CaptureViewModel.kt
@@ -30,6 +30,7 @@ class CaptureViewModel(
private fun distributionVersion() = Settings(context).getDistributionVersion
var flashMode = ImageCapture.FLASH_MODE_AUTO
var lensFacing = -1
+ var isProcessUpload = Settings(context).isProcessUpload
init {
// Clear any pending captures from a previous session
diff --git a/capture/src/main/res/values-de/strings.xml b/capture/src/main/res/values-de/strings.xml
index 21659421a..975762ac3 100644
--- a/capture/src/main/res/values-de/strings.xml
+++ b/capture/src/main/res/values-de/strings.xml
@@ -1,36 +1,36 @@
-
-
- Schließen
- Aufnehmen
- Kamera wechseln
- Gallerie
- Blitzlicht-Modus
-
- Foto
- Video
-
- Fotovorschau
- Dateiname
- Beschreibung
- Foto löschen
- Speichern
- Der Dateiname darf keines der folgenden Zeichen enthalten: ?:"*|/\\<>
- Der Dateiname darf nicht leer sein.
-
- Vorschau
-
- Audio
- An
- Aus
-
- @string/capture_failure_permissions
- Zum Aufnehmen von Medien aktivieren Sie die Berechtigungen für Kamera und Mikrofon.
- %02d:%02d:%02d
- %02d:%02d
- Die ausgewählte Dateigröße für den Upload darf 100MB nicht überschreiten.
-
- Aufnahmen verwerfen?
- %d Mediendateien werden verworfen, wenn Sie beenden.
- Abbrechen
- Verwerfen
-
+
+
+ Schließen
+ Aufnehmen
+ Kamera wechseln
+ Gallerie
+ Blitzlicht-Modus
+
+ Foto
+ Video
+
+ Fotovorschau
+ Dateiname
+ Beschreibung
+ Foto löschen
+ Speichern
+ Der Dateiname darf keines der folgenden Zeichen enthalten: ?:"*|/\\<>
+ Der Dateiname darf nicht leer sein.
+
+ Vorschau
+
+ Audio
+ An
+ Aus
+
+ @string/capture_failure_permissions
+ Zum Aufnehmen von Medien aktivieren Sie die Berechtigungen für Kamera und Mikrofon.
+ %02d:%02d:%02d
+ %02d:%02d
+ Die ausgewählte Dateigröße für den Upload darf 100MB nicht überschreiten.
+
+ Aufnahmen verwerfen?
+ %d Mediendateien werden verworfen, wenn Sie beenden.
+ Abbrechen
+ Verwerfen
+
diff --git a/capture/src/main/res/values-es/strings.xml b/capture/src/main/res/values-es/strings.xml
index 9f9e976b1..34017ef7b 100644
--- a/capture/src/main/res/values-es/strings.xml
+++ b/capture/src/main/res/values-es/strings.xml
@@ -1,36 +1,36 @@
-
-
- Cerrar
- Tomar foto
- Cambiar de cámara
- Galería
- Modo con flash
-
- Foto
- Vídeo
-
- Vista previa de foto
- Nombre de fichero
- Añadir descripción
- Eliminar foto
- Guardar
- El nombre del fichero no puede contener ninguno de los siguientes caracteres: ?: "*|/\\<>
- El nombre del fichero no puede estar vacío.
-
- Vista previa
-
- Automático
- Encendido
- Apagado
-
- @string/capture_failure_permissions
- Para capturar contenido multimedia, active los permisos de Cámara y Micrófono.
- %02d:%02d:%02d
- %02d:%02d
- El tamaño de fichero seleccionado no puede exceder los 100 MB para ser cargado
-
- ¿Descartar capturas?
- Si sale, se descartarán %d ficheros multimedia.
- Cancelar
- Descartar
-
+
+
+ Cerrar
+ Tomar foto
+ Cambiar de cámara
+ Galería
+ Modo con flash
+
+ Foto
+ Vídeo
+
+ Vista previa de foto
+ Nombre de fichero
+ Añadir descripción
+ Eliminar foto
+ Guardar
+ El nombre del fichero no puede contener ninguno de los siguientes caracteres: ?: "*|/\\<>
+ El nombre del fichero no puede estar vacío.
+
+ Vista previa
+
+ Automático
+ Encendido
+ Apagado
+
+ @string/capture_failure_permissions
+ Para capturar contenido multimedia, active los permisos de Cámara y Micrófono.
+ %02d:%02d:%02d
+ %02d:%02d
+ El tamaño de fichero seleccionado no puede exceder los 100 MB para ser cargado
+
+ ¿Descartar capturas?
+ Si sale, se descartarán %d ficheros multimedia.
+ Cancelar
+ Descartar
+
diff --git a/capture/src/main/res/values-fr/strings.xml b/capture/src/main/res/values-fr/strings.xml
index 9e80701a2..0ef2cba82 100644
--- a/capture/src/main/res/values-fr/strings.xml
+++ b/capture/src/main/res/values-fr/strings.xml
@@ -1,37 +1,37 @@
-
-
- Fermer
- Capture
- Changer de caméra
- Galerie
- Modèle de flash
-
- Photo
- Vidéo
-
- Aperçu de la photo
- Nom de fichier
- Ajouter une description
- Supprimer la photo
- Enregistrer
- Le nom de fichier ne peut contenir aucun des caractères suivants : ? : \"*|/\\<>
- Le nom du fichier ne peut pas être vide.
-
- Aperçu
-
- Auto
- Activé
- Désactivé
-
- @string/capture_failure_permissions
- Pour capturer un fichier multimédia, activez les autorisations pour l\'appareil photo et le microphone.
- %02d:%02d:%02d
- %02d:%02d
- La taille du fichier sélectionné ne peut pas dépasser 100 Mo pour l’importer.
-
- Supprimer les captures ?
- Les fichiers multimédias %d seront supprimés si vous quittez.
- Annuler
- Supprimer
-
-
+
+
+ Fermer
+ Capture
+ Changer de caméra
+ Galerie
+ Modèle de flash
+
+ Photo
+ Vidéo
+
+ Aperçu de la photo
+ Nom de fichier
+ Ajouter une description
+ Supprimer la photo
+ Enregistrer
+ Le nom de fichier ne peut contenir aucun des caractères suivants : ? : \"*|/\\<>
+ Le nom du fichier ne peut pas être vide.
+
+ Aperçu
+
+ Auto
+ Activé
+ Désactivé
+
+ @string/capture_failure_permissions
+ Pour capturer un fichier multimédia, activez les autorisations pour l\'appareil photo et le microphone.
+ %02d:%02d:%02d
+ %02d:%02d
+ La taille du fichier sélectionné ne peut pas dépasser 100 Mo pour l’importer.
+
+ Supprimer les captures ?
+ Les fichiers multimédias %d seront supprimés si vous quittez.
+ Annuler
+ Supprimer
+
+
diff --git a/capture/src/main/res/values-it/strings.xml b/capture/src/main/res/values-it/strings.xml
index 4a4c231ba..a1e565f61 100644
--- a/capture/src/main/res/values-it/strings.xml
+++ b/capture/src/main/res/values-it/strings.xml
@@ -1,37 +1,37 @@
-
-
- Chiudi
- Acquisisci
- Passa dalla fotocamera anteriore a quella posteriore
- Galleria
- Modalità flash
-
- Foto
- Video
-
- Anteprima foto
- Nome file
- Aggiungi descrizione
- Elimina foto
- Salva
- Il nome del file non può contenere i caratteri seguenti: ?:"*|/\\<>
- Il nome file non può essere vuoto.
-
- Anteprima
-
- Automatico
- Attiva
- Disattiva
-
- @string/capture_failure_permissions
- Per acquisire i contenuti multimediali, attivare le autorizzazioni per la fotocamera e il microfono.
- %02d:%02d:%02d
- %02d:%02d
- La dimensione del file selezionato non può superare il limite di 100 MB per il caricamento.
-
- Eliminare le acquisizioni?
- Se esci, verranno eliminati %d file multimediali.
- Annulla
- Elimina
-
-
+
+
+ Chiudi
+ Acquisisci
+ Passa dalla fotocamera anteriore a quella posteriore
+ Galleria
+ Modalità flash
+
+ Foto
+ Video
+
+ Anteprima foto
+ Nome file
+ Aggiungi descrizione
+ Elimina foto
+ Salva
+ Il nome del file non può contenere i caratteri seguenti: ?:"*|/\\<>
+ Il nome file non può essere vuoto.
+
+ Anteprima
+
+ Automatico
+ Attiva
+ Disattiva
+
+ @string/capture_failure_permissions
+ Per acquisire i contenuti multimediali, attivare le autorizzazioni per la fotocamera e il microfono.
+ %02d:%02d:%02d
+ %02d:%02d
+ La dimensione del file selezionato non può superare il limite di 100 MB per il caricamento.
+
+ Eliminare le acquisizioni?
+ Se esci, verranno eliminati %d file multimediali.
+ Annulla
+ Elimina
+
+
diff --git a/capture/src/main/res/values-nl/strings.xml b/capture/src/main/res/values-nl/strings.xml
index 2d2a49f51..d621e55c4 100644
--- a/capture/src/main/res/values-nl/strings.xml
+++ b/capture/src/main/res/values-nl/strings.xml
@@ -1,38 +1,38 @@
-
-
- Sluiten
- Vastleggen
- Van camera wisselen
- Galerie
- Flitsmodus
-
- Foto
- Video
-
- Fotopreview
- Bestandsnaam
- Beschrijving toevoegen
- Foto verwijderen
- Opslaan
- Bestandsnaam mag geen van de volgende tekens bevatten: ?:"*|/\\<>
- Bestandsnaam mag niet leeg zijn.
-
- Preview
-
- Auto
- Aan
- Uit
-
- @string/capture_failure_permissions
- Om media vast te leggen, schakelt u camera- en microfoonrechten in.
- %02d:%02d:%02d
- %02d:%02d
- Het geselecteerde bestand mag niet groter zijn dan 100 MB om te kunnen worden geüpload.
-
- Opnamen verwijderen?
- %d mediabestanden worden verwijderd als u afsluit.
- Annuleren
- Verwijderen
-
-
-
+
+
+ Sluiten
+ Vastleggen
+ Van camera wisselen
+ Galerie
+ Flitsmodus
+
+ Foto
+ Video
+
+ Fotopreview
+ Bestandsnaam
+ Beschrijving toevoegen
+ Foto verwijderen
+ Opslaan
+ Bestandsnaam mag geen van de volgende tekens bevatten: ?:"*|/\\<>
+ Bestandsnaam mag niet leeg zijn.
+
+ Preview
+
+ Auto
+ Aan
+ Uit
+
+ @string/capture_failure_permissions
+ Om media vast te leggen, schakelt u camera- en microfoonrechten in.
+ %02d:%02d:%02d
+ %02d:%02d
+ Het geselecteerde bestand mag niet groter zijn dan 100 MB om te kunnen worden geüpload.
+
+ Opnamen verwijderen?
+ %d mediabestanden worden verwijderd als u afsluit.
+ Annuleren
+ Verwijderen
+
+
+
diff --git a/capture/src/main/res/values/strings.xml b/capture/src/main/res/values/strings.xml
index 28c0bbd71..8e0764de5 100644
--- a/capture/src/main/res/values/strings.xml
+++ b/capture/src/main/res/values/strings.xml
@@ -34,5 +34,6 @@
%02d:%02d:%02d
%02d:%02d
The selected file size cannot exceed 100MB to upload.
+ The selected file size cannot exceed %d MB to upload.
diff --git a/common/src/main/kotlin/com/alfresco/content/ContentPickerFragment.kt b/common/src/main/kotlin/com/alfresco/content/ContentPickerFragment.kt
index cbbe5a6e0..0875ee27c 100644
--- a/common/src/main/kotlin/com/alfresco/content/ContentPickerFragment.kt
+++ b/common/src/main/kotlin/com/alfresco/content/ContentPickerFragment.kt
@@ -10,6 +10,7 @@ import kotlinx.coroutines.suspendCancellableCoroutine
class ContentPickerFragment : Fragment() {
private lateinit var requestLauncher: ActivityResultLauncher>
+ private lateinit var requestLauncherSingle: ActivityResultLauncher>
private var onResult: CancellableContinuation>? = null
override fun onCreate(savedInstanceState: Bundle?) {
@@ -18,12 +19,20 @@ class ContentPickerFragment : Fragment() {
requestLauncher = registerForActivityResult(GetMultipleContents()) {
onResult?.resume(it, null)
}
+
+ requestLauncherSingle = registerForActivityResult(GetSingleContent()) {
+ onResult?.resume(it, null)
+ }
}
- private suspend fun pickItems(mimeTypes: Array): List =
+ private suspend fun pickItems(mimeTypes: Array, isMultiple: Boolean): List =
suspendCancellableCoroutine { continuation ->
onResult = continuation
- requestLauncher.launch(mimeTypes)
+ if (!isMultiple) {
+ requestLauncherSingle.launch(mimeTypes)
+ } else {
+ requestLauncher.launch(mimeTypes)
+ }
}
companion object {
@@ -32,11 +41,12 @@ class ContentPickerFragment : Fragment() {
suspend fun pickItems(
context: Context,
mimeTypes: Array,
+ isMultiple: Boolean = false,
): List =
withFragment(
context,
TAG,
- { it.pickItems(mimeTypes) },
+ { it.pickItems(mimeTypes, isMultiple) },
{ ContentPickerFragment() },
)
}
diff --git a/common/src/main/kotlin/com/alfresco/content/GetMultipleContents.kt b/common/src/main/kotlin/com/alfresco/content/GetMultipleContents.kt
index f025a6101..76bbc39ff 100644
--- a/common/src/main/kotlin/com/alfresco/content/GetMultipleContents.kt
+++ b/common/src/main/kotlin/com/alfresco/content/GetMultipleContents.kt
@@ -32,14 +32,15 @@ class GetMultipleContents : ActivityResultContract, List>() {
}
companion object {
- const val MAX_FILE_SIZE = 100
+ const val MAX_FILE_SIZE_100 = 100
+ const val MAX_FILE_SIZE_10 = 10
/**
* returns true if file exceed the 100mb length otherwise false
*/
- fun isFileSizeExceed(length: Long): Boolean {
+ fun isFileSizeExceed(length: Long, maxSize: Int): Boolean {
val fileLength = length.div(1024L).div(1024L)
- return fileLength > MAX_FILE_SIZE.minus(1).toLong()
+ return fileLength > maxSize.minus(1).toLong()
}
fun getClipDataUris(intent: Intent): List {
diff --git a/common/src/main/kotlin/com/alfresco/content/GetSingleContent.kt b/common/src/main/kotlin/com/alfresco/content/GetSingleContent.kt
new file mode 100644
index 000000000..d5333625f
--- /dev/null
+++ b/common/src/main/kotlin/com/alfresco/content/GetSingleContent.kt
@@ -0,0 +1,69 @@
+package com.alfresco.content
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import androidx.activity.result.contract.ActivityResultContract
+import androidx.annotation.CallSuper
+
+/**
+ * An ActivityResultContract similar to
+ * [androidx.activity.result.contract.ActivityResultContracts.GetSingleContents]
+ * that allows specifying multiple mimeTypes.
+ */
+class GetSingleContent : ActivityResultContract, List>() {
+
+ @CallSuper
+ override fun createIntent(context: Context, input: Array): Intent {
+ return Intent(Intent.ACTION_GET_CONTENT)
+ .addCategory(Intent.CATEGORY_OPENABLE)
+ .setType("*/*")
+ .putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false)
+ .putExtra(Intent.EXTRA_MIME_TYPES, input)
+ }
+
+ override fun parseResult(resultCode: Int, intent: Intent?): List {
+ return if (intent == null || resultCode != Activity.RESULT_OK) {
+ emptyList()
+ } else {
+ getClipDataUris(intent)
+ }
+ }
+
+ companion object {
+ const val MAX_FILE_SIZE = 100
+
+ /**
+ * returns true if file exceed the 100mb length otherwise false
+ */
+ fun isFileSizeExceed(length: Long): Boolean {
+ val fileLength = length.div(1024L).div(1024L)
+ return fileLength > MAX_FILE_SIZE.minus(1).toLong()
+ }
+
+ fun getClipDataUris(intent: Intent): List {
+ // Use a LinkedHashSet to maintain any ordering that may be
+ // present in the ClipData
+ val resultSet = LinkedHashSet()
+
+ val intentData = intent.data
+ if (intentData != null) {
+ resultSet.add(intentData)
+ }
+
+ val clipData = intent.clipData
+ if (clipData == null && resultSet.isEmpty()) {
+ return emptyList()
+ } else if (clipData != null) {
+ for (i in 0 until clipData.itemCount) {
+ val uri = clipData.getItemAt(i).uri
+ if (uri != null) {
+ resultSet.add(uri)
+ }
+ }
+ }
+ return ArrayList(resultSet)
+ }
+ }
+}
diff --git a/common/src/main/kotlin/com/alfresco/content/NavControllerExt.kt b/common/src/main/kotlin/com/alfresco/content/NavControllerExt.kt
index be275847d..0222e2638 100644
--- a/common/src/main/kotlin/com/alfresco/content/NavControllerExt.kt
+++ b/common/src/main/kotlin/com/alfresco/content/NavControllerExt.kt
@@ -22,8 +22,8 @@ private fun NavController.navigateToFolder(entry: Entry) =
/**
* navigate to move screen
*/
-fun NavController.navigateToFolder(entry: Entry, moveId: String) =
- navigateToChildFolder(entry.id, entry.name, moveId, modeFor(entry))
+fun NavController.navigateToFolder(entry: Entry, moveId: String = "", isProcess: Boolean = false) =
+ navigateToChildFolder(entry.id, entry.name, moveId, modeFor(entry), isProcess = isProcess)
/**
* navigate to extension child folders
@@ -68,6 +68,13 @@ fun NavController.navigateToContextualSearch(id: String, title: String, isExtens
}
}
+/**
+ * navigate to contextual search from process app
+ */
+fun NavController.navigateToContextualSearch(id: String, title: String, isProcess: Boolean) {
+ navigate(Uri.parse("$BASE_URI/search/folder/$id?title=${Uri.encode(title)},isProcess=$isProcess"))
+}
+
/**
* navigate to browse parent folder
*/
@@ -78,19 +85,27 @@ fun NavController.navigateToParent(id: String, title: String, mode: String = REM
/**
* navigate to browse move parent folder
*/
-fun NavController.navigateToMoveParent(id: String, moveId: String, title: String) {
+fun NavController.navigateToMoveParent(id: String, moveId: String, title: String, isProcess: Boolean = false) {
val path = "move"
- navigate(Uri.parse("$BASE_URI/browse_move_parent/$id?title=${Uri.encode(title)},moveId=$moveId,path=$path"))
+ navigate(Uri.parse("$BASE_URI/browse_move_parent/$id?title=${Uri.encode(title)},moveId=$moveId,path=$path,isProcess=$isProcess"))
}
/**
* navigate to browse child folder
*/
-fun NavController.navigateToChildFolder(id: String, title: String, moveId: String = "", mode: String = REMOTE) {
- if (moveId.isNotEmpty()) {
- navigate(Uri.parse("$BASE_URI/browse_child/extension/$mode/$id/$moveId?title=${Uri.encode(title)}"))
- } else {
- navigate(Uri.parse("$BASE_URI/browse_child/extension/$mode/$id?title=${Uri.encode(title)}"))
+fun NavController.navigateToChildFolder(id: String, title: String, moveId: String = "", mode: String = REMOTE, isProcess: Boolean = false) {
+ when {
+ moveId.isNotEmpty() -> {
+ navigate(Uri.parse("$BASE_URI/browse_move_child/$mode/$id?title=${Uri.encode(title)},moveId=$moveId,path=extension"))
+ }
+
+ isProcess -> {
+ navigate(Uri.parse("$BASE_URI/browse_move_child/$mode/$id?title=${Uri.encode(title)},isProcess=$isProcess,path=extension"))
+ }
+
+ else -> {
+ navigate(Uri.parse("$BASE_URI/browse_child/extension/$mode/$id?title=${Uri.encode(title)}"))
+ }
}
}
diff --git a/common/src/main/kotlin/com/alfresco/content/ZoneDateTimeExt.kt b/common/src/main/kotlin/com/alfresco/content/ZoneDateTimeExt.kt
index 606bb92c4..c3591cce7 100644
--- a/common/src/main/kotlin/com/alfresco/content/ZoneDateTimeExt.kt
+++ b/common/src/main/kotlin/com/alfresco/content/ZoneDateTimeExt.kt
@@ -7,11 +7,15 @@ import java.util.TimeZone
const val DATE_FORMAT_1 = "yyyy-MM-dd"
const val DATE_FORMAT_2 = "dd-MMM-yyyy"
-const val DATE_FORMAT_3 = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
+const val DATE_FORMAT_2_1 = "dd-MM-yyyy"
+const val DATE_FORMAT_3 = "yyyy-MM-dd'T'hh:mm:ss.SSSZ"
+const val DATE_FORMAT_3_1 = "yyyy-MM-dd'T'hh:mm:ssZ"
const val DATE_FORMAT_4 = "dd MMM yyyy"
-const val DATE_FORMAT_5 = "yyyy-MM-dd'T'HH:mm:ss'Z'"
+const val DATE_FORMAT_4_1 = "dd MMM yyyy hh:mm a"
+const val DATE_FORMAT_5 = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
const val DATE_FORMAT_6 = "yyyy-MM-dd'T'HH:mm:ss"
const val DATE_FORMAT_7 = "dd MMM,yyyy hh:mm:ss a"
+const val DATE_FORMAT_8 = "dd-MM-yyyy hh:mm:ss a"
/**
* pare the string date and returns the Date obj
@@ -63,3 +67,34 @@ fun String.getLocalFormattedDate(currentFormat: String, convertFormat: String):
}
return ""
}
+
+/**
+ * convert the UTC format date to Local date and time and returns the String obj
+ * @param currentFormat
+ * @param convertFormat
+ */
+fun String.getLocalFormattedDate1(currentFormat: String, convertFormat: String): String {
+ val parserFormat = SimpleDateFormat(currentFormat, Locale.getDefault())
+ parserFormat.timeZone = TimeZone.getTimeZone("UTC")
+ val date = parserFormat.parse(this)
+ if (date != null) {
+ val formatter = SimpleDateFormat(convertFormat, Locale.getDefault())
+ formatter.timeZone = TimeZone.getTimeZone(Locale.getDefault().isO3Language)
+ val formattedDate = formatter.format(date)
+ return formattedDate
+ }
+ return ""
+}
+
+fun updateDateFormat(originalDateString: String?): String? {
+ originalDateString ?: return null
+
+ val (datePart, timePart) = originalDateString.split(" ", limit = 2)
+ val updatedDatePart = datePart.split("-").joinToString("-") { part ->
+ if (part.equals("MM", ignoreCase = true)) {
+ part // Keep "MM" as is
+ } else part.lowercase() // Convert "DD" and "YYYY" to lowercase
+ }
+
+ return if (timePart.isNotEmpty()) "$updatedDatePart $timePart" else updatedDatePart
+}
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 8d05a9c54..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
@@ -16,4 +17,7 @@ interface EntryListener {
* It will get called on tap of start workflow on the option list
*/
fun onProcessStart(entries: List) {}
+
+ fun onAttachFolder(entry: ParentEntry) {}
+ fun onAttachFiles(field: FieldsData) {}
}
diff --git a/common/src/main/kotlin/com/alfresco/content/common/SharedURLParser.kt b/common/src/main/kotlin/com/alfresco/content/common/SharedURLParser.kt
new file mode 100644
index 000000000..f5d0c090f
--- /dev/null
+++ b/common/src/main/kotlin/com/alfresco/content/common/SharedURLParser.kt
@@ -0,0 +1,80 @@
+package com.alfresco.content.common
+
+import com.alfresco.content.session.Session
+import com.alfresco.content.session.SessionManager
+import com.alfresco.content.session.SessionNotFoundException
+import java.net.URL
+import java.net.URLDecoder
+
+class SharedURLParser {
+
+ lateinit var session: Session
+
+ init {
+ try {
+ session = SessionManager.requireSession
+ } catch (e: SessionNotFoundException) {
+ e.printStackTrace()
+ }
+ }
+
+ /**
+ * isPreview (Boolean)
+ * String
+ * isRemoteFolder (Boolean)
+ */
+ fun getEntryIdFromShareURL(url: String, isHyperLink: Boolean = false): Triple {
+ val extData = URLDecoder.decode(url, "UTF-8")
+
+ val hostname = URL(session.baseUrl).host
+
+ if (!url.contains(hostname)) {
+ return Triple(
+ true,
+ "",
+ false,
+ )
+ }
+
+ if (!isHyperLink && !extData.contains(SCHEME)) return Triple(false, "", false)
+
+ if (extData.contains(IDENTIFIER_PREVIEW)) {
+ return Triple(
+ true,
+ extData.substringAfter(SCHEME),
+ false,
+ )
+ }
+
+ if (!extData.contains(IDENTIFIER_PERSONAL_FILES)) return Triple(false, "", false)
+
+ return if (extData.contains(IDENTIFIER_VIEWER)) {
+ Triple(
+ false,
+ extData.substringAfter(IDENTIFIER_VIEWER).substringBefore(DELIMITER_BRACKET),
+ false,
+ )
+ } else {
+ Triple(
+ false,
+ extData.substringAfter(IDENTIFIER_PERSONAL_FILES)
+ .substringBefore(DELIMITER_FORWARD_SLASH),
+ true,
+ )
+ }
+ }
+
+ companion object {
+ const val ID_KEY = "id"
+ const val MODE_KEY = "mode"
+ const val VALUE_REMOTE = "remote"
+ const val VALUE_SHARE = "share"
+ const val KEY_FOLDER = "folder"
+ const val SCHEME = "androidamw:///"
+ const val IDENTIFIER_PREVIEW = "/preview"
+ const val IDENTIFIER_VIEWER = "viewer:view/"
+ const val IDENTIFIER_PERSONAL_FILES = "/personal-files/"
+ const val DELIMITER_BRACKET = ")"
+ const val DELIMITER_FORWARD_SLASH = "/"
+ }
+}
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
old mode 100644
new mode 100755
index 3c56b55e5..03807c05f
--- a/common/src/main/res/values-de/strings.xml
+++ b/common/src/main/res/values-de/strings.xml
@@ -29,4 +29,18 @@
Keine
%d Ausgewählt
Bitte überprüfen Sie Ihre Internetverbindung und starten anschließend den Vorgang erneut.
+ ...Alle anzeigen
+ Workflow
+ %d Anhänge
+ Dateien anhängen
+ Angehängte Dateien
+ Symbol für Datei
+ Symbol \'Anhang löschen\'
+ Eine Datei löschen?
+ Abbrechen
+ Bestätigen
+ Abgeschlossen
+ Warnung
+ Das Hochladen von Dateien ist in Bearbeitung. Tippen Sie auf Bestätigen, um ohne laufende Dateien fortzufahren.
+ Keine angehängten Dateien
diff --git a/common/src/main/res/values-es/strings.xml b/common/src/main/res/values-es/strings.xml
old mode 100644
new mode 100755
index 71bad5e9d..a5bf9aca8
--- a/common/src/main/res/values-es/strings.xml
+++ b/common/src/main/res/values-es/strings.xml
@@ -5,11 +5,11 @@
Cancelar
Volver
Baja
- Mediano
+ Mediana
Alta
NA
Yo
- Y
+ M
Título: %s Asignado: %s Prioridad %s
Título: %s Ruta: %s
Título: %s
@@ -18,15 +18,29 @@
Encabezado: %s
Más
Descargar
- Añadir Favorito
- Eliminar Favorito
+ Añadir favorito
+ Eliminar favorito
Crear
Ya existe una carpeta con este nombre. Pruebe un nombre diferente.
- Título: %s Usuario asignado: %s
- Individual
+ Título: %s Asignado: %s
+ Personal
Grupo
La carga ya está en curso. ¿Desea continuar sin cargar?
Ninguno
- %d Seleccionado
+ %d seleccionado
Verifique su conexión a Internet y vuelva a intentarlo.
+ ...Ver todo
+ Flujo de trabajo
+ %d adjuntos
+ Añadir adjuntos
+ Ficheros adjuntos
+ Icono de fichero
+ Icono de eliminación de adjunto
+ ¿Eliminar un fichero?
+ Cancelar
+ Confirmar
+ Por completar
+ Advertencia
+ La carga de ficheros está en curso. Toque Confirmar para continuar sin los ficheros en curso.
+ No hay ficheros adjuntos
diff --git a/common/src/main/res/values-fr/strings.xml b/common/src/main/res/values-fr/strings.xml
old mode 100644
new mode 100755
index 57a330442..9699395f9
--- a/common/src/main/res/values-fr/strings.xml
+++ b/common/src/main/res/values-fr/strings.xml
@@ -1,7 +1,7 @@
Permissions nécessaires
- Ok
+ OK
Annuler
Retour
Basse
@@ -21,12 +21,26 @@
Ajouter aux favoris
Supprimer des favoris
Créer
- Un dossier du même nom existe déjà. Essayez avec un nom différent.
+ Un dossier portant le même nom existe déjà. Veuillez essayer avec un nom différent.
Titre : %s Assigné : %s
Individuel
Groupe
- L\'importation est actuellement en cours. Voulez-vous continuer sans importer ?
+ Chargement en cours. Souhaitez-vous continuer sans charger ?
Aucun
- %d Sélectionné
+ %d sélectionné
Veuillez vérifier votre connexion Internet et réessayer.
+ …Afficher tout
+ Workflow
+ %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
+ Terminé
+ Avertissement
+ Le téléchargement des fichiers est en cours. Tapez sur Confirmer pour continuer sans fichiers en cours.
+ Aucun fichier joint
diff --git a/common/src/main/res/values-it/strings.xml b/common/src/main/res/values-it/strings.xml
old mode 100644
new mode 100755
index 15dd5d214..e5f496bf5
--- a/common/src/main/res/values-it/strings.xml
+++ b/common/src/main/res/values-it/strings.xml
@@ -18,8 +18,8 @@
Intestazione: %s
Altro
Scarica
- Aggiungi a Preferiti
- Rimuovi da Preferiti
+ Aggiungi preferito
+ Rimuovi preferito
Crea
Esiste già una cartella con questo nome. Provare un nome diverso.
Titolo: %s Assegnatario: %s
@@ -27,6 +27,20 @@
Gruppo
Il caricamento è attualmente in corso. Continuare senza eseguire il caricamento?
Nessuno
- %d Selezionato
- Controlla la connessione Internet e riprova.
+ %d selezionato/i
+ Controllare la connessione Internet e riprovare.
+ ...Visualizza tutto
+ Workflow
+ %d allegati
+ Aggiungi allegati
+ File allegati
+ Icona file
+ Icona Elimina allegato
+ Eliminare un file?
+ Annulla
+ Conferma
+ Completato
+ Avviso
+ Il caricamento dei file è in corso. Toccare su Conferma per continuare senza questi file.
+ Nessun file allegato
diff --git a/common/src/main/res/values-nl/strings.xml b/common/src/main/res/values-nl/strings.xml
old mode 100644
new mode 100755
index dff3f09f4..1f115bf5c
--- a/common/src/main/res/values-nl/strings.xml
+++ b/common/src/main/res/values-nl/strings.xml
@@ -27,6 +27,20 @@
Groep
De upload is momenteel bezig. Wilt u doorgaan zonder uploaden?
Geen
- %d Geselecteerd
+ %d seselecteerd
Controleer uw internetverbinding en probeer het opnieuw.
+ ...Alle weergeven
+ Workflow
+ %d bijlagen
+ Bijlagen toevoegen
+ Bijgevoegde bestanden
+ Pictogram Bestand
+ Pictogram Bijlage verwijderen
+ Bestand verwijderen?
+ Annuleren
+ Bevestigen
+ Voltooien
+ Waarschuwing
+ Er worden bestanden geüpload. Tik om te bevestigen dat u wilt doorgaan zonder de bestanden in uitvoering.
+ Geen bijgevoegde bestanden
diff --git a/common/src/main/res/values/colors.xml b/common/src/main/res/values/colors.xml
index 76dffde4b..1976400e6 100644
--- a/common/src/main/res/values/colors.xml
+++ b/common/src/main/res/values/colors.xml
@@ -11,4 +11,5 @@
#212121
#1F212121
#262626
+ #3D2A7DE1
diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml
index f84124ea2..04459084d 100644
--- a/common/src/main/res/values/strings.xml
+++ b/common/src/main/res/values/strings.xml
@@ -29,4 +29,18 @@
None
%d Selected
Please check your internet connection and Try again.
+ …View all
+ Workflow
+ %d attachment(s)
+ Add attachments
+ Attached files
+ File icon
+ Icon delete attachment
+ Delete a file?
+ Cancel
+ Confirm
+ CompleteWarning
+ Files uploading is in progress. Tap on Confirm to continue without in-progress files.
+ No Attached Files
+ No Name
diff --git a/component/build.gradle b/component/build.gradle
index feaa39089..fe94e69a8 100644
--- a/component/build.gradle
+++ b/component/build.gradle
@@ -13,6 +13,12 @@ android {
buildFeatures {
viewBinding true
}
+ buildFeatures { // Enables Jetpack Compose for this module
+ compose = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.5.4"
+ }
}
kapt {
@@ -40,6 +46,9 @@ dependencies {
implementation libs.epoxy.core
kapt libs.epoxy.processor
+ implementation libs.ui
+ implementation libs.activity.compose
+
// Testing
testImplementation libs.junit
androidTestImplementation libs.androidx.test.core
diff --git a/component/src/main/java/com/alfresco/content/component/ComponentData.kt b/component/src/main/java/com/alfresco/content/component/ComponentData.kt
index 3d6ddbcda..463ea5d95 100644
--- a/component/src/main/java/com/alfresco/content/component/ComponentData.kt
+++ b/component/src/main/java/com/alfresco/content/component/ComponentData.kt
@@ -2,8 +2,10 @@ package com.alfresco.content.component
import android.os.Parcelable
import com.alfresco.content.data.Facets
+import com.alfresco.content.data.OptionsModel
import com.alfresco.content.data.TaskEntry
import com.alfresco.content.data.TaskFilterData
+import com.alfresco.content.data.payloads.FieldsData
import com.alfresco.content.models.CategoriesItem
import kotlinx.parcelize.Parcelize
@@ -43,6 +45,39 @@ data class ComponentData(
)
}
+ /**
+ * update the ComponentData obj after getting the result (name and id) from field options
+ * @param fieldsData
+ * @param name
+ * @param query
+ */
+ fun with(fieldsData: FieldsData, name: String, query: String): ComponentData {
+ return ComponentData(
+ id = fieldsData.id,
+ name = fieldsData.name,
+ selector = ComponentType.DROPDOWN_RADIO.value,
+ options = fieldsData.options.filter { it.id != "empty" }
+ .map { ComponentOptions.withProcess(it) },
+ selectedName = name,
+ selectedQuery = query,
+ )
+ }
+
+ /**
+ * update the ComponentData obj after getting the result (name and id) from field options
+ * @param outcomes
+ * @param name
+ * @param query
+ */
+ fun with(outcomes: List, name: String, query: String): ComponentData {
+ return ComponentData(
+ selector = ComponentType.PROCESS_ACTION.value,
+ options = outcomes.map { ComponentOptions.withProcess(it) },
+ selectedName = name,
+ selectedQuery = query,
+ )
+ }
+
/**
* update the ComponentData obj after getting the result (name and query) from filters
* @param facets
@@ -105,7 +140,11 @@ data class ComponentData(
* @param selectedName
* @param selectedQueryMap
*/
- fun with(obj: ComponentData?, selectedName: String, selectedQueryMap: Map): ComponentData {
+ fun with(
+ obj: ComponentData?,
+ selectedName: String,
+ selectedQueryMap: Map,
+ ): ComponentData {
return ComponentData(
id = obj?.id,
name = obj?.name,
diff --git a/component/src/main/java/com/alfresco/content/component/ComponentOptions.kt b/component/src/main/java/com/alfresco/content/component/ComponentOptions.kt
index f5fddc55d..4d9f9a27b 100644
--- a/component/src/main/java/com/alfresco/content/component/ComponentOptions.kt
+++ b/component/src/main/java/com/alfresco/content/component/ComponentOptions.kt
@@ -68,5 +68,18 @@ data class ComponentOptions(
)
}
+
+ /**
+ * return the updated ComponentOptions obj by using Options obj
+ * @param options
+ */
+ fun withProcess(options: OptionsModel): ComponentOptions {
+ return ComponentOptions(
+ label = options.name,
+ query = options.id,
+ default = options.default,
+
+ )
+ }
}
}
diff --git a/component/src/main/java/com/alfresco/content/component/ComponentSheet.kt b/component/src/main/java/com/alfresco/content/component/ComponentSheet.kt
index aa7581e1f..8741d6361 100644
--- a/component/src/main/java/com/alfresco/content/component/ComponentSheet.kt
+++ b/component/src/main/java/com/alfresco/content/component/ComponentSheet.kt
@@ -38,6 +38,7 @@ class ComponentSheet : BottomSheetDialogFragment(), MavericksView {
val epoxyCheckListController: AsyncEpoxyController by lazy { epoxyCheckListController() }
val epoxyRadioListController: AsyncEpoxyController by lazy { epoxyRadioListController() }
val epoxyCheckFacetListController: AsyncEpoxyController by lazy { epoxyCheckFacetListController() }
+ val epoxyActionListController: AsyncEpoxyController by lazy { epoxyActionListController() }
private var executedPicker = false
val minVisibleItem = 10
@@ -194,11 +195,26 @@ class ComponentSheet : BottomSheetDialogFragment(), MavericksView {
private fun setupComponents() = withState(viewModel) { state ->
binding.parentView.removeAllViews()
- binding.parentView.addView(binding.topView)
- binding.parentView.addView(binding.separator)
+
+ if (state.parent?.selector != ComponentType.PROCESS_ACTION.value) {
+ binding.parentView.addView(binding.topView)
+ binding.parentView.addView(binding.separator)
+ }
+
+ when {
+ (state.parent?.selector == ComponentType.DROPDOWN_RADIO.value) ||
+ (state.parent?.selector == ComponentType.PROCESS_ACTION.value) -> {
+ }
+
+ else -> {
+ binding.bottomSeparator.visibility = View.VISIBLE
+ binding.bottomView.visibility = View.VISIBLE
+ }
+ }
val replacedString = state.parent?.name?.replace(" ", ".") ?: ""
val localizedName = requireContext().getLocalizedName(replacedString)
+
if (localizedName == replacedString) {
binding.title.text = state.parent?.name ?: ""
} else if (state.parent?.name?.lowercase().equals(textFileSize)) {
@@ -211,7 +227,7 @@ class ComponentSheet : BottomSheetDialogFragment(), MavericksView {
ComponentType.TASK_PROCESS_PRIORITY.value -> setupTaskPriorityComponent(state)
ComponentType.VIEW_TEXT.value -> setupTextComponent(state)
ComponentType.CHECK_LIST.value -> setupCheckListComponent(viewModel)
- ComponentType.RADIO.value -> setupRadioListComponent(state, viewModel)
+ ComponentType.RADIO.value, ComponentType.DROPDOWN_RADIO.value -> setupRadioListComponent(state, viewModel)
ComponentType.NUMBER_RANGE.value -> setupNumberRangeComponent(state, viewModel)
ComponentType.SLIDER.value -> setupSliderComponent(state, viewModel)
ComponentType.DATE_RANGE.value, ComponentType.DATE_RANGE_FUTURE.value -> {
@@ -233,7 +249,9 @@ class ComponentSheet : BottomSheetDialogFragment(), MavericksView {
}
}
}
+
ComponentType.FACETS.value -> setupFacetComponent(state, viewModel)
+ ComponentType.PROCESS_ACTION.value -> setupProcessActionsComponent(state, viewModel)
}
}
@@ -243,24 +261,41 @@ class ComponentSheet : BottomSheetDialogFragment(), MavericksView {
when (state.parent?.selector) {
ComponentType.DATE_RANGE.value -> {
if (viewModel.fromDate.isEmpty()) {
- binding.dateRangeComponent.fromInputLayout.error = getString(R.string.component_number_range_empty)
+ binding.dateRangeComponent.fromInputLayout.error =
+ getString(R.string.component_number_range_empty)
} else if (viewModel.toDate.isEmpty()) {
- binding.dateRangeComponent.toInputLayout.error = getString(R.string.component_number_range_empty)
+ binding.dateRangeComponent.toInputLayout.error =
+ getString(R.string.component_number_range_empty)
} else {
- onApply?.invoke(state.parent.selectedName, state.parent.selectedQuery, state.parent.selectedQueryMap)
+ onApply?.invoke(
+ state.parent.selectedName,
+ state.parent.selectedQuery,
+ state.parent.selectedQueryMap,
+ )
dismiss()
}
}
+
ComponentType.DATE_RANGE_FUTURE.value -> {
if (viewModel.fromDate.isEmpty() && viewModel.toDate.isEmpty()) {
- binding.dateRangeComponent.fromInputLayout.error = getString(R.string.component_number_range_empty)
+ binding.dateRangeComponent.fromInputLayout.error =
+ getString(R.string.component_number_range_empty)
} else {
- onApply?.invoke(state.parent.selectedName, state.parent.selectedQuery, state.parent.selectedQueryMap)
+ onApply?.invoke(
+ state.parent.selectedName,
+ state.parent.selectedQuery,
+ state.parent.selectedQueryMap,
+ )
dismiss()
}
}
+
else -> {
- onApply?.invoke(state.parent?.selectedName ?: "", state.parent?.selectedQuery ?: "", state.parent?.selectedQueryMap ?: mapOf())
+ onApply?.invoke(
+ state.parent?.selectedName ?: "",
+ state.parent?.selectedQuery ?: "",
+ state.parent?.selectedQueryMap ?: mapOf(),
+ )
dismiss()
}
}
@@ -285,12 +320,18 @@ class ComponentSheet : BottomSheetDialogFragment(), MavericksView {
ComponentType.CHECK_LIST.value -> {
epoxyCheckListController.requestModelBuild()
}
- ComponentType.RADIO.value -> {
+
+ ComponentType.RADIO.value, ComponentType.DROPDOWN_RADIO.value -> {
epoxyRadioListController.requestModelBuild()
}
+
ComponentType.FACETS.value -> {
epoxyCheckFacetListController.requestModelBuild()
}
+
+ ComponentType.PROCESS_ACTION.value -> {
+ epoxyActionListController.requestModelBuild()
+ }
}
}
@@ -302,7 +343,10 @@ class ComponentSheet : BottomSheetDialogFragment(), MavericksView {
data(option)
optionSelected(viewModel.isOptionSelected(state, option))
clickListener { model, _, _, _ ->
- viewModel.updateMultipleComponentData(model.data().label, model.data().value)
+ viewModel.updateMultipleComponentData(
+ model.data().label,
+ model.data().value,
+ )
}
}
}
@@ -317,10 +361,19 @@ class ComponentSheet : BottomSheetDialogFragment(), MavericksView {
data(option)
optionSelected(viewModel.isOptionSelected(state, option))
clickListener { model, _, _, _ ->
- viewModel.updateSingleComponentData(
- requireContext().getLocalizedName(model.data().label),
- model.data().query,
- )
+ if (state.parent.selector == ComponentType.DROPDOWN_RADIO.value) {
+ onApply?.invoke(
+ requireContext().getLocalizedName(model.data().label),
+ model.data().query,
+ mapOf(),
+ )
+ dismiss()
+ } else {
+ viewModel.updateSingleComponentData(
+ requireContext().getLocalizedName(model.data().label),
+ model.data().query,
+ )
+ }
}
}
}
@@ -354,6 +407,22 @@ class ComponentSheet : BottomSheetDialogFragment(), MavericksView {
}
}
+ private fun epoxyActionListController() = simpleController(viewModel) { state ->
+
+ if (state.parent?.options?.isNotEmpty() == true) {
+ state.parent.options.forEach { option ->
+ listViewActionsRow {
+ id(option.hashCode())
+ data(option)
+ clickListener { model, _, _, _ ->
+ onApply?.invoke(model.data().label, model.data().query, mapOf())
+ dismiss()
+ }
+ }
+ }
+ }
+ }
+
private fun showCalendar(isFrom: Boolean, isFutureDate: Boolean) {
viewLifecycleOwner.lifecycleScope.launch {
val result = suspendCoroutine {
diff --git a/component/src/main/java/com/alfresco/content/component/ComponentSheetExtension.kt b/component/src/main/java/com/alfresco/content/component/ComponentSheetExtension.kt
index be9fcc2dd..4d625a060 100644
--- a/component/src/main/java/com/alfresco/content/component/ComponentSheetExtension.kt
+++ b/component/src/main/java/com/alfresco/content/component/ComponentSheetExtension.kt
@@ -183,13 +183,32 @@ fun ComponentSheet.setupFacetComponent(state: ComponentState, viewModel: Compone
binding.facetCheckListComponent.searchInputLayout.editText?.addTextChangedListener(searchInputTextWatcher)
}
+/**
+ * setup the Process Actions Component
+ * @param state
+ * @param viewModel
+ */
+@SuppressLint("ClickableViewAccessibility")
+fun ComponentSheet.setupProcessActionsComponent(state: ComponentState, viewModel: ComponentViewModel) {
+ viewModel.buildCheckListModel()
+ binding.parentView.addView(binding.frameActions)
+
+ binding.processActions.componentParent.visibility = View.VISIBLE
+
+ binding.processActions.recyclerView.setController(epoxyActionListController)
+}
+
/**
* setup the Title Description Component
* @param state
*/
fun ComponentSheet.setupTextComponent(state: ComponentState) {
binding.parentView.addView(binding.frameTitleDescription)
- binding.titleDescriptionComponent.tvTitle.text = state.parent?.query ?: ""
+ if (state.parent?.query.isNullOrEmpty()) {
+ binding.titleDescriptionComponent.tvTitle.visibility = View.GONE
+ } else {
+ binding.titleDescriptionComponent.tvTitle.text = state.parent?.query ?: ""
+ }
binding.titleDescriptionComponent.tvDescription.text = state.parent?.value ?: ""
binding.bottomView.visibility = View.GONE
diff --git a/component/src/main/java/com/alfresco/content/component/ComponentType.kt b/component/src/main/java/com/alfresco/content/component/ComponentType.kt
index 4179bc899..dcceec73c 100644
--- a/component/src/main/java/com/alfresco/content/component/ComponentType.kt
+++ b/component/src/main/java/com/alfresco/content/component/ComponentType.kt
@@ -13,7 +13,9 @@ enum class ComponentType(val value: String) {
DATE_RANGE("date-range"),
DATE_RANGE_FUTURE("date-range-future"),
RADIO("radio"),
+ DROPDOWN_RADIO("dropdown_radio"),
FACETS("facets"),
TASK_PROCESS_PRIORITY("task-process-priority"),
+ PROCESS_ACTION("process-actions"),
None("none"),
}
diff --git a/component/src/main/java/com/alfresco/content/component/DatePickerBuilder.kt b/component/src/main/java/com/alfresco/content/component/DatePickerBuilder.kt
index ba3692416..9b855f96b 100644
--- a/component/src/main/java/com/alfresco/content/component/DatePickerBuilder.kt
+++ b/component/src/main/java/com/alfresco/content/component/DatePickerBuilder.kt
@@ -3,11 +3,16 @@ package com.alfresco.content.component
import android.content.Context
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
+import com.alfresco.content.DATE_FORMAT_2_1
+import com.alfresco.content.DATE_FORMAT_6
+import com.alfresco.content.data.payloads.FieldType
+import com.alfresco.content.data.payloads.FieldsData
import com.google.android.material.datepicker.CalendarConstraints
import com.google.android.material.datepicker.CompositeDateValidator
import com.google.android.material.datepicker.DateValidatorPointBackward
import com.google.android.material.datepicker.DateValidatorPointForward
import com.google.android.material.datepicker.MaterialDatePicker
+import com.google.android.material.timepicker.MaterialTimePicker
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
@@ -29,6 +34,7 @@ data class DatePickerBuilder(
var isFutureDate: Boolean = false,
var onSuccess: DatePickerOnSuccess? = null,
var onFailure: DatePickerOnFailure? = null,
+ var fieldsData: FieldsData? = null,
) {
private val dateFormatddMMMyy = "dd-MMM-yy"
@@ -65,13 +71,17 @@ data class DatePickerBuilder(
val constraintsBuilder = CalendarConstraints.Builder()
- constraintsBuilder.setValidator(CompositeDateValidator.allOf(getValidators()))
+ constraintsBuilder.setValidator(CompositeDateValidator.allOf(getValidators(fieldsData)))
val datePicker = MaterialDatePicker.Builder.datePicker().apply {
- if (isFrom) {
- setTitleText(context.getString(R.string.hint_range_from_date))
+ if (fieldsData != null) {
+ setTitleText(fieldsData?.name)
} else {
- setTitleText(context.getString(R.string.hint_range_to_date))
+ if (isFrom) {
+ setTitleText(context.getString(R.string.hint_range_from_date))
+ } else {
+ setTitleText(context.getString(R.string.hint_range_to_date))
+ }
}
setSelection(getSelectionDate())
setCalendarConstraints(constraintsBuilder.build())
@@ -79,11 +89,39 @@ data class DatePickerBuilder(
datePicker.show(fragmentManager, DatePickerBuilder::class.java.simpleName)
+ val timePicker = MaterialTimePicker
+ .Builder()
+ .setTitleText(fieldsData?.name)
+ .build()
+
+ var stringDateTime = ""
datePicker.addOnPositiveButtonClickListener {
val date = Date(it)
- val stringDate = getFormatDate(date)
- onSuccess?.invoke(stringDate)
+ if (fieldsData?.type == FieldType.DATETIME.value()) {
+ stringDateTime = getFormatDate(date)
+ timePicker.show(fragmentManager, DatePickerBuilder::class.java.name)
+ } else {
+ onSuccess?.invoke(getFormatDate(date))
+ }
+ }
+
+ timePicker.addOnPositiveButtonClickListener {
+ val hour = if (timePicker.hour == 0) 12 else timePicker.hour
+ val minute = if (timePicker.minute == 0) "00" else timePicker.minute.toString()
+ val amPm = if (timePicker.hour < 12) "AM" else "PM"
+
+ val combinedDateTime = "$stringDateTime $hour:$minute $amPm"
+ onSuccess?.invoke(combinedDateTime)
}
+
+ timePicker.addOnNegativeButtonClickListener {
+ onFailure?.invoke()
+ }
+
+ timePicker.addOnCancelListener {
+ onFailure?.invoke()
+ }
+
datePicker.addOnCancelListener {
onFailure?.invoke()
}
@@ -92,27 +130,42 @@ data class DatePickerBuilder(
}
}
- private fun getValidators(): ArrayList {
+ private fun getValidators(fieldsData: FieldsData? = null): ArrayList {
val validators: ArrayList = ArrayList()
var endDate = MaterialDatePicker.todayInUtcMilliseconds()
var requiredEndDate = false
- if (isFrom) {
- if (toDate.isNotEmpty()) {
- toDate.getDateFromString()?.let { date ->
- endDate = Date(date.time.plus(addOneDay)).time
+
+ if (fieldsData != null) {
+ fieldsData.minValue?.apply {
+ this.getDateFromString(getFieldDateFormat(fieldsData))?.let { date ->
+ validators.add(DateValidatorPointForward.from(date.time))
+ }
+ }
+
+ fieldsData.maxValue?.apply {
+ this.getDateFromString(getFieldDateFormat(fieldsData))?.let { date ->
+ validators.add(DateValidatorPointBackward.before(date.time))
}
- requiredEndDate = true
}
} else {
- if (fromDate.isNotEmpty()) {
- fromDate.getDateFromString()?.let { date ->
- validators.add(DateValidatorPointForward.from(date.time))
+ if (isFrom) {
+ if (toDate.isNotEmpty()) {
+ toDate.getDateFromString()?.let { date ->
+ endDate = Date(date.time.plus(addOneDay)).time
+ }
+ requiredEndDate = true
+ }
+ } else {
+ if (fromDate.isNotEmpty()) {
+ fromDate.getDateFromString()?.let { date ->
+ validators.add(DateValidatorPointForward.from(date.time))
+ }
+ requiredEndDate = !isFutureDate
}
- requiredEndDate = !isFutureDate
}
- }
- if (requiredEndDate) {
- validators.add(DateValidatorPointBackward.before(endDate))
+ if (requiredEndDate) {
+ validators.add(DateValidatorPointBackward.before(endDate))
+ }
}
return validators
@@ -140,8 +193,8 @@ data class DatePickerBuilder(
return SimpleDateFormat(dateFormat, Locale.ENGLISH).format(currentTime)
}
- private fun String.getDateFromString(): Date? {
- return SimpleDateFormat(dateFormat, Locale.ENGLISH).parse(this)
+ private fun String.getDateFromString(format: String = dateFormat): Date? {
+ return SimpleDateFormat(format, Locale.ENGLISH).parse(this)
}
private fun String.getddMMyyyyStringDate(): String? {
@@ -162,4 +215,12 @@ data class DatePickerBuilder(
calendar[Calendar.YEAR] = splitDate[2].toInt()
return calendar.timeInMillis
}
+
+ private fun getFieldDateFormat(fieldsData: FieldsData? = null): String {
+ return when (fieldsData?.type) {
+ FieldType.DATETIME.value() -> DATE_FORMAT_6
+ FieldType.DATE.value() -> DATE_FORMAT_2_1
+ else -> dateFormat
+ }
+ }
}
diff --git a/component/src/main/java/com/alfresco/content/component/ListViewActionsRow.kt b/component/src/main/java/com/alfresco/content/component/ListViewActionsRow.kt
new file mode 100644
index 000000000..57627bf3e
--- /dev/null
+++ b/component/src/main/java/com/alfresco/content/component/ListViewActionsRow.kt
@@ -0,0 +1,31 @@
+package com.alfresco.content.component
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.widget.FrameLayout
+import com.airbnb.epoxy.CallbackProp
+import com.airbnb.epoxy.ModelProp
+import com.airbnb.epoxy.ModelView
+import com.alfresco.content.component.databinding.ViewActionsListRowBinding
+import com.alfresco.content.getLocalizedName
+
+@ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT)
+internal class ListViewActionsRow @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+) : FrameLayout(context, attrs, defStyleAttr) {
+ private val binding =
+ ViewActionsListRowBinding.inflate(LayoutInflater.from(context), this, true)
+
+ @ModelProp
+ fun setData(options: ComponentOptions) {
+ binding.actionButton.text = context.getLocalizedName(options.label ?: "")
+ }
+
+ @CallbackProp
+ fun setClickListener(listener: OnClickListener?) {
+ binding.actionButton.setOnClickListener(listener)
+ }
+}
diff --git a/component/src/main/res/layout/sheet_component_filter.xml b/component/src/main/res/layout/sheet_component_filter.xml
index 2d9968c29..6896575f7 100644
--- a/component/src/main/res/layout/sheet_component_filter.xml
+++ b/component/src/main/res/layout/sheet_component_filter.xml
@@ -3,10 +3,10 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
+ android:layout_height="match_parent"
android:orientation="vertical"
android:paddingTop="16dp"
- android:paddingBottom="16dp"
- android:layout_height="match_parent">
+ android:paddingBottom="16dp">
+
+
+
+
+
@@ -160,7 +171,8 @@
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="16dp"
- android:background="@color/color_view_line" />
+ android:background="@color/color_view_line"
+ android:visibility="gone" />
+ android:orientation="vertical"
+ android:visibility="gone">
+ android:layout_height="match_parent"
+ android:layout_marginTop="16dp">
-
diff --git a/component/src/main/res/values/colors.xml b/component/src/main/res/values/colors.xml
index b4f52db9c..f53a788b3 100644
--- a/component/src/main/res/values/colors.xml
+++ b/component/src/main/res/values/colors.xml
@@ -1,6 +1,5 @@
- #3D2A7DE1
#212121
#1F212121
#0D212328
diff --git a/data/objectbox-models/default.json b/data/objectbox-models/default.json
index f6e247b07..55e381c82 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": "34:9044184444890640140",
"name": "Entry",
"properties": [
{
@@ -153,6 +153,16 @@
"id": "32:675229831295035009",
"name": "canDelete",
"type": 1
+ },
+ {
+ "id": "33:8784310188115192790",
+ "name": "observerID",
+ "type": 9
+ },
+ {
+ "id": "34:9044184444890640140",
+ "name": "isMultiple",
+ "type": 1
}
],
"relations": []
diff --git a/data/objectbox-models/default.json.bak b/data/objectbox-models/default.json.bak
index f6e247b07..68f720100 100644
--- a/data/objectbox-models/default.json.bak
+++ b/data/objectbox-models/default.json.bak
@@ -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/AnalyticsManager.kt b/data/src/main/kotlin/com/alfresco/content/data/AnalyticsManager.kt
index a525d0035..a31e6331c 100644
--- a/data/src/main/kotlin/com/alfresco/content/data/AnalyticsManager.kt
+++ b/data/src/main/kotlin/com/alfresco/content/data/AnalyticsManager.kt
@@ -107,7 +107,7 @@ class AnalyticsManager(val session: Session = SessionManager.requireSession) {
/**
* analytics for API tracker
*/
- fun apiTracker(apiName: APIEvent, status: Boolean = false, size: String = "") {
+ fun apiTracker(apiName: APIEvent, status: Boolean = false, size: String = "", outcome: String = "") {
val apiTrackName = if (status) "${apiName.value}_success".lowercase() else "${apiName.value}_fail".lowercase()
val params = repository.defaultParams()
@@ -116,6 +116,9 @@ class AnalyticsManager(val session: Session = SessionManager.requireSession) {
if (size.isNotEmpty()) {
params.putString(Parameters.FileSize.value, size)
}
+ if (outcome.isNotEmpty()) {
+ params.putString(Parameters.ActionOutcome.value, outcome)
+ }
repository.logEvent(apiTrackName, params)
}
}
diff --git a/data/src/main/kotlin/com/alfresco/content/data/AnalyticsRepository.kt b/data/src/main/kotlin/com/alfresco/content/data/AnalyticsRepository.kt
index 7da25ef46..704085257 100644
--- a/data/src/main/kotlin/com/alfresco/content/data/AnalyticsRepository.kt
+++ b/data/src/main/kotlin/com/alfresco/content/data/AnalyticsRepository.kt
@@ -158,6 +158,8 @@ enum class APIEvent(val value: String) {
DeleteTaskAttachment("event_api_delete_task_attachment"),
AssignUser("event_api_assign_user"),
SearchUser("event_api_search_user"),
+ StartWorkflow("event_api_start_workflow"),
+ Outcomes("event_api_outcomes"),
}
/**
@@ -193,4 +195,5 @@ enum class Parameters(val value: String) {
NumberOfFiles("number_of_files"),
FacetName("facet_name"),
Success("success"),
+ ActionOutcome("action_outcome"),
}
diff --git a/data/src/main/kotlin/com/alfresco/content/data/AttachFolderSearchData.kt b/data/src/main/kotlin/com/alfresco/content/data/AttachFolderSearchData.kt
new file mode 100644
index 000000000..026e638a8
--- /dev/null
+++ b/data/src/main/kotlin/com/alfresco/content/data/AttachFolderSearchData.kt
@@ -0,0 +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/BrowseRepository.kt b/data/src/main/kotlin/com/alfresco/content/data/BrowseRepository.kt
index 782d61fe1..cb3b9435d 100644
--- a/data/src/main/kotlin/com/alfresco/content/data/BrowseRepository.kt
+++ b/data/src/main/kotlin/com/alfresco/content/data/BrowseRepository.kt
@@ -84,6 +84,7 @@ class BrowseRepository(otherSession: Session? = null) {
skipCount,
maxItems,
include = extraFields(),
+ includeSource = true,
),
)
diff --git a/data/src/main/kotlin/com/alfresco/content/data/ContentEntry.kt b/data/src/main/kotlin/com/alfresco/content/data/ContentEntry.kt
index de34ca938..2886d6acf 100644
--- a/data/src/main/kotlin/com/alfresco/content/data/ContentEntry.kt
+++ b/data/src/main/kotlin/com/alfresco/content/data/ContentEntry.kt
@@ -4,7 +4,6 @@ package com.alfresco.content.data
import android.os.Parcelable
import com.alfresco.process.models.ContentDataEntry
import kotlinx.parcelize.Parcelize
-import java.time.ZonedDateTime
/**
* Marked as ContentEntry class
@@ -14,7 +13,6 @@ import java.time.ZonedDateTime
data class ContentEntry(
val id: Int = 0,
val name: String = "",
- val created: ZonedDateTime? = null,
val userDetails: UserGroupDetails? = null,
val isRelatedContent: Boolean? = false,
val isContentAvailable: Boolean? = false,
@@ -34,7 +32,6 @@ data class ContentEntry(
return ContentEntry(
id = data.id ?: 0,
name = data.name ?: "",
- created = data.created,
userDetails = data.createdBy?.let { UserGroupDetails.with(it) } ?: UserGroupDetails(),
isRelatedContent = data.relatedContent,
isContentAvailable = data.contentAvailable,
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..cc6f92b91 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,8 @@ data class Entry(
val uploadServer: UploadServerType = UploadServerType.DEFAULT,
val isReadOnly: Boolean = false,
var isSelectedForMultiSelection: Boolean = false,
+ var observerID: String = "",
+ var isMultiple: Boolean = false,
) : ParentEntry(), Parcelable {
val isSynced: Boolean
@@ -146,6 +148,7 @@ data class Entry(
isFavorite = other.isFavorite,
canDelete = other.canDelete,
otherId = other.otherId,
+ observerID = other.observerID,
)
enum class Type {
@@ -376,6 +379,23 @@ data class Entry(
).withOfflineStatus()
}
+ fun with(contentEntry: ContentEntry, observerID: String, parentId: String?): Entry {
+ return Entry(
+ parentId = parentId,
+ id = contentEntry.id.toString(),
+ name = contentEntry.name,
+ userGroupDetails = contentEntry.userDetails,
+ hasLink = contentEntry.hasLink,
+ isRelatedContent = contentEntry.isRelatedContent,
+ isContentAvailable = contentEntry.isContentAvailable,
+ mimeType = contentEntry.mimeType,
+ observerID = observerID,
+ uploadServer = UploadServerType.UPLOAD_TO_PROCESS,
+ offlineStatus = OfflineStatus.UNDEFINED,
+
+ )
+ }
+
/**
* return the Entry obj by using the contentEntry obj.
*/
@@ -395,7 +415,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 +432,34 @@ data class Entry(
previewStatus = data.previewStatus,
thumbnailStatus = data.thumbnailStatus,
uploadServer = uploadServer,
+ observerID = observerID,
+ )
+ }
+
+ /**
+ * return the Entry obj
+ */
+ fun with(data: Entry, parentId: String? = null, observerID: String = ""): Entry {
+ return Entry(
+ id = data.id,
+ parentId = parentId,
+ name = data.name,
+ type = data.type,
+ created = data.created,
+ userGroupDetails = data.userGroupDetails,
+ isRelatedContent = data.isRelatedContent,
+ isContentAvailable = data.isContentAvailable,
+ mimeType = data.mimeType,
+ simpleType = data.simpleType,
+ source = data.source,
+ sourceId = data.sourceId,
+ previewStatus = data.previewStatus,
+ thumbnailStatus = data.thumbnailStatus,
+ uploadServer = UploadServerType.UPLOAD_TO_PROCESS,
+ observerID = observerID,
+ offlineStatus = OfflineStatus.SYNCED,
+ path = data.path,
+ isUpload = true,
)
}
@@ -432,8 +480,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 = "", isMultiple: Boolean = false): Entry {
+ return Entry(uploadServer = UploadServerType.UPLOAD_TO_PROCESS, parentId = id, observerID = fieldId, isMultiple = isMultiple)
}
fun withSelectedEntries(entries: List): Entry {
diff --git a/data/src/main/kotlin/com/alfresco/content/data/FormVariables.kt b/data/src/main/kotlin/com/alfresco/content/data/FormVariables.kt
index e19f5cee8..adb197adb 100644
--- a/data/src/main/kotlin/com/alfresco/content/data/FormVariables.kt
+++ b/data/src/main/kotlin/com/alfresco/content/data/FormVariables.kt
@@ -1,15 +1,19 @@
package com.alfresco.content.data
+import android.os.Parcelable
import com.alfresco.process.models.ResultFormVariables
+import kotlinx.parcelize.Parcelize
+import kotlinx.parcelize.RawValue
/**
* Marked as FormVariables
*/
+@Parcelize
data class FormVariables(
val id: String? = null,
val type: String? = null,
- val value: Any? = null,
-) {
+ val value: @RawValue Any? = null,
+) : Parcelable {
companion object {
/**
* returns the FormVariables using the ResultFormVariables
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 2b2603405..780f6d288 100644
--- a/data/src/main/kotlin/com/alfresco/content/data/OfflineRepository.kt
+++ b/data/src/main/kotlin/com/alfresco/content/data/OfflineRepository.kt
@@ -199,6 +199,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
@@ -223,9 +224,12 @@ class OfflineRepository(otherSession: Session? = null) {
offlineStatus = OfflineStatus.PENDING,
isExtension = isExtension,
uploadServer = uploadServerType,
+ observerID = observerId ?: "",
)
- clearData()
+ if (observerId == null) {
+ clearData()
+ }
update(entry)
@@ -252,6 +256,7 @@ class OfflineRepository(otherSession: Session? = null) {
description: String,
mimeType: String,
uploadServerType: UploadServerType,
+ observerId: String? = null,
) {
// TODO: This process may fail resulting in an orphan file? or node?
val entry = Entry(
@@ -263,9 +268,13 @@ class OfflineRepository(otherSession: Session? = null) {
isUpload = true,
offlineStatus = OfflineStatus.PENDING,
uploadServer = uploadServerType,
+ observerID = observerId ?: "",
)
- clearData()
+ if (observerId == null) {
+ clearData()
+ }
+
update(entry)
val dest = File(session.uploadDir, entry.boxId.toString())
@@ -274,6 +283,10 @@ class OfflineRepository(otherSession: Session? = null) {
File(srcPath).renameTo(dest)
}
+ fun addServerEntry(entry: Entry) {
+ update(entry)
+ }
+
internal fun fetchPendingUploads() =
box.query()
.equal(Entry_.isUpload, true)
@@ -281,6 +294,16 @@ 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()
+ return query.find()
+ }
+
/**
* returns the list of uploads which is being uploaded on the server.
*/
@@ -297,6 +320,21 @@ 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, isUpload: Boolean = true): Flow> = callbackFlow {
+ val query = box.query()
+ .equal(Entry_.parentId, parentId, StringOrder.CASE_SENSITIVE)
+ .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
*/
@@ -330,6 +368,19 @@ class OfflineRepository(otherSession: Session? = null) {
box.query()
.equal(Entry_.isUpload, true)
.equal(Entry_.offlineStatus, OfflineStatus.SYNCED.value(), StringOrder.CASE_SENSITIVE)
+ .notEqual(Entry_.uploadServer, UploadServerType.UPLOAD_TO_PROCESS.value(), StringOrder.CASE_SENSITIVE)
+ .apply {
+ if (parentId != null) {
+ equal(Entry_.parentId, parentId, StringOrder.CASE_SENSITIVE)
+ }
+ }.build().remove()
+
+ /**
+ * remove the task entries on the basis of task ID from local db.
+ */
+ fun removeCompletedUploadsProcess(parentId: String? = null) =
+ box.query()
+ .equal(Entry_.uploadServer, UploadServerType.UPLOAD_TO_PROCESS.value(), StringOrder.CASE_SENSITIVE)
.apply {
if (parentId != null) {
equal(Entry_.parentId, parentId, StringOrder.CASE_SENSITIVE)
diff --git a/data/src/main/kotlin/com/alfresco/content/data/OptionsModel.kt b/data/src/main/kotlin/com/alfresco/content/data/OptionsModel.kt
index df137dbac..6906907ad 100644
--- a/data/src/main/kotlin/com/alfresco/content/data/OptionsModel.kt
+++ b/data/src/main/kotlin/com/alfresco/content/data/OptionsModel.kt
@@ -46,3 +46,13 @@ data class OptionsModel(
}
}
}
+
+enum class DefaultOutcomesID {
+ DEFAULT_START_WORKFLOW,
+ DEFAULT_SAVE,
+ DEFAULT_CLAIM,
+ DEFAULT_RELEASE,
+ DEFAULT_COMPLETE,
+ ;
+ fun value() = name.lowercase()
+}
diff --git a/data/src/main/kotlin/com/alfresco/content/data/ProcessEntry.kt b/data/src/main/kotlin/com/alfresco/content/data/ProcessEntry.kt
index 8562d5323..ae4c4de05 100644
--- a/data/src/main/kotlin/com/alfresco/content/data/ProcessEntry.kt
+++ b/data/src/main/kotlin/com/alfresco/content/data/ProcessEntry.kt
@@ -1,6 +1,7 @@
package com.alfresco.content.data
import android.os.Parcelable
+import com.alfresco.content.data.payloads.FieldType
import com.alfresco.content.data.payloads.FieldsData
import com.alfresco.process.models.ProcessInstanceEntry
import kotlinx.parcelize.Parcelize
@@ -16,6 +17,7 @@ data class ProcessEntry(
val description: String = "",
val businessKey: String? = null,
val processDefinitionId: String? = null,
+ val processInstanceId: String? = null,
val tenantId: String? = null,
val started: ZonedDateTime? = null,
val ended: ZonedDateTime? = null,
@@ -33,6 +35,7 @@ data class ProcessEntry(
val formattedDueDate: String? = null,
val defaultEntries: List = emptyList(),
val reviewerType: ReviewerType = ReviewerType.OTHER,
+ val taskEntry: TaskEntry = TaskEntry(),
) : ParentEntry(), Parcelable {
companion object {
@@ -75,6 +78,26 @@ data class ProcessEntry(
)
}
+ /**
+ * return the ProcessEntry using RuntimeProcessDefinitionDataEntry
+ */
+ fun with(data: ProcessEntry, entries: List): ProcessEntry {
+ return data.copy(defaultEntries = entries)
+ }
+
+ /**
+ * return the ProcessEntry using TaskEntry
+ */
+ fun with(data: TaskEntry): ProcessEntry {
+ return ProcessEntry(
+ name = data.name,
+ description = data.description ?: "",
+ processDefinitionId = data.processDefinitionId,
+ processInstanceId = data.processInstanceId,
+ taskEntry = data,
+ )
+ }
+
/**
* return the ProcessEntry using RuntimeProcessDefinitionDataEntry
*/
@@ -246,6 +269,38 @@ data class ProcessEntry(
reviewerType = reviewerType,
)
}
+
+ fun withProcess(data: ProcessEntry, fieldType: String): ProcessEntry {
+ var reviewerType: ReviewerType = ReviewerType.PEOPLE
+
+ if (fieldType == FieldType.FUNCTIONAL_GROUP.value()) {
+ reviewerType = ReviewerType.FUNCTIONAL_GROUP
+ }
+
+ return ProcessEntry(
+ id = data.id,
+ name = data.name,
+ description = data.description,
+ businessKey = data.businessKey,
+ processDefinitionId = data.processDefinitionId,
+ tenantId = data.tenantId,
+ started = data.started,
+ ended = data.ended,
+ startedBy = data.startedBy,
+ processDefinitionName = data.processDefinitionName,
+ processDefinitionDescription = data.processDefinitionDescription,
+ processDefinitionKey = data.processDefinitionKey,
+ processDefinitionCategory = data.processDefinitionCategory,
+ processDefinitionVersion = data.processDefinitionVersion,
+ processDefinitionDeploymentId = data.processDefinitionDeploymentId,
+ graphicalNotationDefined = data.graphicalNotationDefined,
+ startFormDefined = data.startFormDefined,
+ suspended = data.suspended,
+ formattedDueDate = data.formattedDueDate,
+ priority = data.priority,
+ reviewerType = reviewerType,
+ )
+ }
}
}
diff --git a/data/src/main/kotlin/com/alfresco/content/data/ResponseFormVariables.kt b/data/src/main/kotlin/com/alfresco/content/data/ResponseFormVariables.kt
new file mode 100644
index 000000000..20a67e8c4
--- /dev/null
+++ b/data/src/main/kotlin/com/alfresco/content/data/ResponseFormVariables.kt
@@ -0,0 +1,26 @@
+package com.alfresco.content.data
+
+import android.os.Parcelable
+import com.alfresco.process.models.ResultFormVariables
+import kotlinx.parcelize.Parcelize
+
+/**
+ * Marked as ResponseFormVariables
+ */
+@Parcelize
+data class ResponseFormVariables(
+ val listFormVariables: List = listOf(),
+ val mapFormVariables: Map = mapOf(),
+) : Parcelable {
+ companion object {
+ /**
+ * returns the ResponseFormVariables obj by using ResultFormVariables obj
+ */
+ fun with(raw: List): ResponseFormVariables {
+ return ResponseFormVariables(
+ listFormVariables = raw.map { FormVariables.with(it) },
+ mapFormVariables = raw.associate { it.id to it.type },
+ )
+ }
+ }
+}
diff --git a/data/src/main/kotlin/com/alfresco/content/data/ResponsePaging.kt b/data/src/main/kotlin/com/alfresco/content/data/ResponsePaging.kt
index 3d9b36c1d..4432d3851 100644
--- a/data/src/main/kotlin/com/alfresco/content/data/ResponsePaging.kt
+++ b/data/src/main/kotlin/com/alfresco/content/data/ResponsePaging.kt
@@ -22,6 +22,7 @@ data class ResponsePaging(
return ResponsePaging(
raw.list?.entries?.map { Entry.with(it.entry, true) } ?: emptyList(),
Pagination.with(raw.list!!.pagination!!),
+ Source.with(raw.list?.source),
)
}
diff --git a/data/src/main/kotlin/com/alfresco/content/data/Settings.kt b/data/src/main/kotlin/com/alfresco/content/data/Settings.kt
index 674e483d1..bd8d4a36a 100644
--- a/data/src/main/kotlin/com/alfresco/content/data/Settings.kt
+++ b/data/src/main/kotlin/com/alfresco/content/data/Settings.kt
@@ -20,10 +20,15 @@ class Settings(
private val listener =
SharedPreferences.OnSharedPreferenceChangeListener { prefs, key ->
- if (key != IS_PROCESS_ENABLED_KEY) {
- AnalyticsManager().theme(prefs.getString(key, "") ?: "")
+ when (key) {
+ IS_PROCESS_ENABLED_KEY, IS_PROCESS_UPLOAD_KEY -> {}
+ else -> {
+ AnalyticsManager().theme(prefs.getString(key, "") ?: "")
+ }
+ }
+ key?.apply {
+ preferenceChangedFlow.tryEmit(this)
}
- preferenceChangedFlow.tryEmit(key)
}
fun observeChanges() {
@@ -63,6 +68,8 @@ class Settings(
var isProcessEnabled = sharedPref.getBoolean(IS_PROCESS_ENABLED_KEY, false)
+ var isProcessUpload = sharedPref.getBoolean(IS_PROCESS_UPLOAD_KEY, false)
+
val getDistributionVersion: DistributionVersion
get() = checkVersion(sharedPref.getString(DISTRIBUTION_VERSION, ""))
@@ -91,5 +98,6 @@ class Settings(
const val DISTRIBUTION_VERSION = "DISTRIBUTION_VERSION"
const val Enterprise = "Enterprise"
const val IS_PROCESS_ENABLED_KEY = "is_process_enabled"
+ const val IS_PROCESS_UPLOAD_KEY = "is_process_upload"
}
}
diff --git a/data/src/main/kotlin/com/alfresco/content/data/TaskEntry.kt b/data/src/main/kotlin/com/alfresco/content/data/TaskEntry.kt
index fe845f056..2031c5355 100644
--- a/data/src/main/kotlin/com/alfresco/content/data/TaskEntry.kt
+++ b/data/src/main/kotlin/com/alfresco/content/data/TaskEntry.kt
@@ -25,6 +25,7 @@ data class TaskEntry(
val duration: Long? = null,
val isNewTaskCreated: Boolean = false,
val processInstanceId: String? = null,
+ val processDefinitionId: String? = null,
val statusOption: List = emptyList(),
val listContents: List = emptyList(),
val outcomes: List = emptyList(),
@@ -56,8 +57,10 @@ data class TaskEntry(
involvedPeople = data.involvedPeople?.map { UserGroupDetails.with(it) } ?: emptyList(),
isNewTaskCreated = isNewTaskCreated,
processInstanceId = data.processInstanceId,
+ processDefinitionId = data.processDefinitionId,
processInstanceStartUserId = data.processInstanceStartUserId,
memberOfCandidateGroup = data.memberOfCandidateGroup,
+ formattedDueDate = data.dueDate?.toLocalDate()?.toString(),
)
}
@@ -120,6 +123,7 @@ data class TaskEntry(
endDate = parent.endDate,
duration = parent.duration,
processInstanceId = parent.processInstanceId,
+ processDefinitionId = parent.processDefinitionId,
outcomes = response.outcomes,
processInstanceStartUserId = parent.processInstanceStartUserId,
memberOfCandidateGroup = parent.memberOfCandidateGroup,
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 47d4c87f0..7d879bab4 100644
--- a/data/src/main/kotlin/com/alfresco/content/data/TaskRepository.kt
+++ b/data/src/main/kotlin/com/alfresco/content/data/TaskRepository.kt
@@ -15,8 +15,6 @@ import com.alfresco.events.EventBus
import com.alfresco.process.apis.ProcessAPI
import com.alfresco.process.apis.TaskAPI
import com.alfresco.process.models.AssignUserBody
-import com.alfresco.process.models.CommonOptionModel
-import com.alfresco.process.models.GroupInfo
import com.alfresco.process.models.ProfileData
import com.alfresco.process.models.RequestComment
import com.alfresco.process.models.RequestLinkContent
@@ -26,8 +24,6 @@ import com.alfresco.process.models.RequestProcessInstancesQuery
import com.alfresco.process.models.RequestSaveForm
import com.alfresco.process.models.RequestTaskFilters
import com.alfresco.process.models.TaskBodyCreate
-import com.alfresco.process.models.UserInfo
-import com.alfresco.process.models.ValuesModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -331,6 +327,7 @@ class TaskRepository {
),
local.parentId,
uploadServer = uploadServerType,
+ observerID = local.observerID,
)
else -> Entry()
@@ -350,7 +347,7 @@ class TaskRepository {
processesService.linkContentToProcess(
includeLinkContent(linkContentPayload),
),
- uploadServer = UploadServerType.NONE,
+ uploadServer = UploadServerType.UPLOAD_TO_PROCESS,
)
private fun includeLinkContent(payload: LinkContentPayload): RequestLinkContent {
@@ -383,59 +380,16 @@ class TaskRepository {
/**
* Execute the start flow integration
*/
- suspend fun startWorkflow(processEntry: ProcessEntry?, items: String) = ProcessEntry.with(
+ suspend fun startWorkflow(processEntry: ProcessEntry?, items: String, values: Map) = ProcessEntry.with(
processesService.createProcessInstance(
RequestProcessInstances(
name = processEntry?.name,
processDefinitionId = processEntry?.id,
- values = ValuesModel(
- due = processEntry?.formattedDueDate,
- message = processEntry?.description,
- priority = if (processEntry?.priority != -1) {
- CommonOptionModel(
- id = getTaskPriority(processEntry?.priority ?: 0).name,
- name = getTaskPriority(processEntry?.priority ?: 0).name,
- )
- } else {
- null
- },
- reviewer = getUser(processEntry?.startedBy),
- reviewGroups = getGroup(processEntry?.startedBy),
- items = items,
- sendEmailNotifications = false,
- ),
+ values = values,
),
),
)
- private fun getUser(userGroupInfo: UserGroupDetails?): UserInfo? {
- return if (userGroupInfo?.isGroup == true) {
- null
- } else {
- UserInfo(
- id = userGroupInfo?.id,
- firstName = userGroupInfo?.firstName,
- lastName = userGroupInfo?.lastName,
- email = userGroupInfo?.email,
- )
- }
- }
-
- private fun getGroup(userGroupInfo: UserGroupDetails?): GroupInfo? {
- return if (userGroupInfo?.isGroup == false) {
- null
- } else {
- GroupInfo(
- id = userGroupInfo?.id,
- name = userGroupInfo?.name,
- externalId = userGroupInfo?.externalId,
- status = userGroupInfo?.status,
- parentGroupId = userGroupInfo?.parentGroupId,
- groups = userGroupInfo?.groups,
- )
- }
- }
-
/**
* saving the accountInfo data in preferences
*/
@@ -455,40 +409,40 @@ class TaskRepository {
*/
suspend fun getTaskForm(taskID: String) = ResponseListForm.with(tasksService.taskForm(taskID))
+ /**
+ * Call to fetch the task's form variables related to workflow
+ */
+ suspend fun getTaskFormVariables(taskID: String) = ResponseFormVariables.with(tasksService.taskFormVariables(taskID))
+
/**
* Call to perform the outcomes
*/
- suspend fun actionOutcomes(outcome: String, taskEntry: TaskEntry) = tasksService.taskFormAction(
+ suspend fun actionOutcomes(outcome: String, taskEntry: TaskEntry, values: Map) = tasksService.taskFormAction(
taskEntry.id,
RequestOutcomes(
outcome = outcome,
- values = if (taskEntry.taskFormStatus != null) {
- ValuesModel(
- status = CommonOptionModel(
- id = taskEntry.taskFormStatus,
- name = taskEntry.taskFormStatus,
- ),
- comment = taskEntry.comment,
- )
- } else {
- null
- },
+ values = values,
+ ),
+ )
+
+ /**
+ * Call to perform the outcomes
+ */
+ suspend fun actionCompleteOutcome(taskID: String, values: Map) = tasksService.taskFormAction(
+ taskID,
+ RequestOutcomes(
+ outcome = null,
+ values = values,
),
)
/**
* Call to save the form data
*/
- suspend fun saveForm(taskEntry: TaskEntry, comment: String) = tasksService.saveForm(
- taskEntry.id,
+ suspend fun saveForm(taskID: String, values: Map) = tasksService.saveForm(
+ taskID,
RequestSaveForm(
- values = ValuesModel(
- status = CommonOptionModel(
- id = taskEntry.taskFormStatus,
- name = taskEntry.taskFormStatus,
- ),
- comment = comment,
- ),
+ values = values,
),
)
diff --git a/data/src/main/kotlin/com/alfresco/content/data/payloads/ConvertDataToMapValues.kt b/data/src/main/kotlin/com/alfresco/content/data/payloads/ConvertDataToMapValues.kt
new file mode 100644
index 000000000..e85ac1d70
--- /dev/null
+++ b/data/src/main/kotlin/com/alfresco/content/data/payloads/ConvertDataToMapValues.kt
@@ -0,0 +1,48 @@
+package com.alfresco.content.data.payloads
+
+import com.alfresco.content.data.TaskEntry
+import com.alfresco.content.data.UserGroupDetails
+
+fun convertModelToMapValues(data: UserGroupDetails?) =
+ if (data?.isGroup == true) {
+ mapOf(
+ "id" to data.id,
+ "name" to data.name,
+ "externalId" to data.externalId,
+ "status" to data.status,
+ "parentGroupId" to data.parentGroupId,
+ "groups" to data.groups,
+ )
+ } else {
+ mapOf(
+ "id" to data?.id,
+ "firstName" to data?.firstName,
+ "lastName" to data?.lastName,
+ "email" to data?.email,
+ )
+ }
+
+fun convertModelToMapValues(data: FieldsData): Map {
+ if (data.value == null) {
+ return mapOf()
+ }
+ val id = data.options.find { it.name == data.value }?.id
+ requireNotNull(id)
+ return mapOf(
+ "id" to id,
+ "name" to data.name,
+ )
+}
+
+fun convertModelToMapValues(data: TaskEntry, commentEntry: String? = null): Map =
+ if (data.taskFormStatus != null) {
+ mapOf(
+ "status" to mapOf(
+ "id" to data.taskFormStatus,
+ "name" to data.taskFormStatus,
+ ),
+ "comment" to (commentEntry ?: data.comment),
+ )
+ } else {
+ mapOf()
+ }
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 5db933ef0..fcd9cbc54 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
@@ -1,10 +1,17 @@
package com.alfresco.content.data.payloads
import android.os.Parcelable
+import com.alfresco.content.data.ContentEntry
+import com.alfresco.content.data.Entry
import com.alfresco.content.data.OptionsModel
+import com.alfresco.content.data.UserGroupDetails
+import com.alfresco.process.models.FieldParams
+import com.alfresco.process.models.FieldSource
import com.alfresco.process.models.Fields
+import com.google.gson.Gson
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue
+import org.json.JSONObject
/**
* Marked as FieldsData
@@ -16,14 +23,66 @@ data class FieldsData(
var name: String = "",
var message: String = "",
var type: String = "",
+ var placeHolder: String? = null,
var value: @RawValue Any? = null,
+ var minLength: Int = 0,
+ var maxLength: Int = 0,
+ var minValue: String? = null,
+ var maxValue: String? = null,
+ var regexPattern: String? = null,
var required: Boolean = false,
var readOnly: Boolean = false,
var overrideId: Boolean = false,
+ var enableFractions: Boolean = false,
+ var enablePeriodSeparator: Boolean = false,
+ var currency: String? = null,
var fields: List = emptyList(),
+ var params: Params? = null,
var options: List = emptyList(),
+ var dateDisplayFormat: String? = null,
+ var hyperlinkUrl: String? = null,
+ var displayText: String? = null,
+ var errorData: Pair = Pair(false, ""),
) : Parcelable {
+ fun getContentList(parentId: String? = null): List {
+ return if (((value as? List<*>)?.firstOrNull() is Map<*, *>)) {
+ (value as? List<*>)?.map { Gson().fromJson(JSONObject(it as Map).toString(), ContentEntry::class.java) }?.map { Entry.with(it, id, parentId) } ?: emptyList()
+ } else {
+ (value as? List<*>)?.mapNotNull { it as? Entry } ?: emptyList()
+ }
+ }
+
+ fun getUserGroupDetails(apsUser: UserGroupDetails?): UserGroupDetails? {
+ if (value == null) {
+ return null
+ }
+
+ val userGroupDetails: UserGroupDetails? = if ((value is Map<*, *>)) {
+ Gson().fromJson(JSONObject(value as Map).toString(), UserGroupDetails::class.java)
+ } else {
+ value as? UserGroupDetails
+ }
+
+ val isAssigneeUser = apsUser?.id == userGroupDetails?.id
+
+ if (isAssigneeUser && userGroupDetails != null) {
+ return UserGroupDetails.with(userGroupDetails)
+ }
+
+ return userGroupDetails
+ }
+
+ fun getDate(serverFormat: String, localFormat: String): Pair {
+ val dateTime = value as? String ?: ""
+
+ if (dateTime.isNotEmpty() && dateTime.contains("T")) {
+ return Pair(dateTime.split("T").firstOrNull() ?: "", serverFormat)
+ }
+
+ return Pair(dateTime, localFormat)
+ }
+
companion object {
/**
* returns the FieldsData obj by using Fields obj
@@ -35,13 +94,116 @@ data class FieldsData(
name = raw.name ?: "",
message = raw.message ?: "",
type = raw.type?.replace("-", "_")?.lowercase() ?: "",
+ placeHolder = raw.placeholder,
value = raw.value,
+ minLength = raw.minLength,
+ maxLength = raw.maxLength,
+ minValue = raw.minValue,
+ maxValue = raw.maxValue,
+ regexPattern = raw.regexPattern,
required = raw.required ?: false,
readOnly = raw.readOnly ?: false,
overrideId = raw.overrideId ?: false,
+ params = Params.with(raw.params),
+ currency = raw.currency,
+ enableFractions = raw.enableFractions ?: false,
+ enablePeriodSeparator = raw.enablePeriodSeparator ?: false,
options = raw.options?.map { OptionsModel.with(it) } ?: emptyList(),
fields = raw.getFieldMapAsList()?.map { with(it) } ?: emptyList(),
+ dateDisplayFormat = raw.dateDisplayFormat,
+ hyperlinkUrl = raw.hyperlinkUrl,
+ displayText = raw.displayText,
+ )
+ }
+
+ /**
+ * returns the FieldsData obj by using Fields obj
+ */
+ fun withUpdateField(raw: FieldsData, value: Any?, errorData: Pair): FieldsData {
+ return FieldsData(
+ fieldType = raw.fieldType,
+ id = raw.id,
+ name = raw.name,
+ message = raw.message,
+ type = raw.type,
+ placeHolder = raw.placeHolder,
+ value = value,
+ minLength = raw.minLength,
+ maxLength = raw.maxLength,
+ minValue = raw.minValue,
+ maxValue = raw.maxValue,
+ regexPattern = raw.regexPattern,
+ required = raw.required,
+ readOnly = raw.readOnly,
+ overrideId = raw.overrideId,
+ params = raw.params,
+ currency = raw.currency,
+ enableFractions = raw.enableFractions,
+ enablePeriodSeparator = raw.enablePeriodSeparator,
+ options = raw.options,
+ fields = raw.fields,
+ dateDisplayFormat = raw.dateDisplayFormat,
+ hyperlinkUrl = raw.hyperlinkUrl,
+ displayText = raw.displayText,
+ errorData = errorData,
)
}
}
}
+
+enum class FieldType {
+ TEXT,
+ MULTI_LINE_TEXT,
+ INTEGER,
+ AMOUNT,
+ BOOLEAN,
+ DATETIME,
+ DATE,
+ DROPDOWN,
+ RADIO_BUTTONS,
+ READONLY_TEXT,
+ READONLY,
+ PEOPLE,
+ FUNCTIONAL_GROUP,
+ HYPERLINK,
+ UPLOAD,
+ SELECT_FOLDER,
+ ;
+
+ fun value() = name.lowercase()
+}
+
+@Parcelize
+data class Params(
+ val fractionLength: Int = 0,
+ val multiple: Boolean = false,
+ val fileSource: FileSourceData? = null,
+ val field: FieldsData? = null,
+ var dateDisplayFormat: String? = null,
+) : Parcelable {
+ companion object {
+ fun with(raw: FieldParams?): Params {
+ return Params(
+ raw?.fractionLength ?: 0,
+ raw?.multiple ?: false,
+ FileSourceData.with(raw?.fileSource),
+ field = raw?.field?.let { FieldsData.with(it) },
+ dateDisplayFormat = raw?.dateDisplayFormat,
+ )
+ }
+ }
+}
+
+@Parcelize
+data class FileSourceData(
+ val serviceId: String = "",
+ val name: String = "",
+) : Parcelable {
+ companion object {
+ fun with(
+ raw: FieldSource?,
+ ): FileSourceData {
+ return FileSourceData(raw?.serviceId ?: "", raw?.name ?: "")
+ }
+ }
+}
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/gradle/libs.versions.toml b/gradle/libs.versions.toml
index e91f0ac16..190aa1cd2 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -4,17 +4,23 @@ coil = "2.4.0"
coroutines = "1.7.1"
epoxy = "5.1.3"
exoplayer = "2.14.0"
-kotlin = "1.8.0"
+kotlin = "1.9.20"
lifecycle = "2.6.1"
navigation = "2.6.0"
okhttp = "4.11.0"
test = "1.5.0"
+activity-compose = "1.7.0"
+compose-bom = "2023.10.01"
+ui-tooling = "1.5.4"
+appcompat = "1.6.1"
+material = "1.11.0"
+constraintlayout = "2.1.4"
[libraries]
alfresco-auth = "com.alfresco.android:auth:0.8.1-SNAPSHOT"
alfresco-content = "com.alfresco.android:content:0.3.4-SNAPSHOT"
alfresco-contentKtx = "com.alfresco.android:content-ktx:0.3.2-SNAPSHOT"
-alfresco-process = "com.alfresco.android:process:0.1.1-SNAPSHOT"
+alfresco-process = "com.alfresco.android:process:0.1.3-SNAPSHOT"
android-desugar = "com.android.tools:desugar_jdk_libs:2.0.3"
android-gradle = "com.android.tools.build:gradle:8.0.2"
@@ -80,13 +86,11 @@ google-material = "com.google.android.material:material:1.9.0"
google-playservices-location = "com.google.android.gms:play-services-location:21.0.1"
google-playservices-basement = "com.google.android.gms:play-services-basement:18.1.0"
-
google-services-gradle = "com.google.gms:google-services:4.3.15"
gradleVersionsPlugin = "com.github.ben-manes:gradle-versions-plugin:0.47.0"
gradleLicensePlugin = "com.jaredsburrows:gradle-license-plugin:0.9.3"
-
gson = "com.google.code.gson:gson:2.10.1"
itextpdf = "com.itextpdf:itextg:5.5.10"
@@ -99,11 +103,12 @@ junit = "junit:junit:4.13.2"
kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" }
-kotlinx-coroutines-test = "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.2"
+kotlinx-coroutines-test = "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0"
leakcanary = "com.squareup.leakcanary:leakcanary-android:2.7"
mavericks = "com.airbnb.android:mavericks:3.0.3"
+mavericks-compose = "com.airbnb.android:mavericks-compose:3.0.3"
mavericks-testing = "com.airbnb.android:mavericks-testing:3.0.3"
mockito-core = "org.mockito:mockito-core:5.4.0"
@@ -128,5 +133,21 @@ spotless = "com.diffplug.spotless:spotless-plugin-gradle:6.19.0"
subsamplingimageview = "com.davemorrissey.labs:subsampling-scale-image-view:3.10.0"
timber = "com.jakewharton.timber:timber:5.0.1"
+activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" }
+compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
+ui = { group = "androidx.compose.ui", name = "ui" }
+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"
+androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "ui-tooling" }
+appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
+material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
+
zelory-compressor = "id.zelory:compressor:3.0.1"
\ No newline at end of file
diff --git a/listview/src/main/kotlin/com/alfresco/content/listview/ListFragment.kt b/listview/src/main/kotlin/com/alfresco/content/listview/ListFragment.kt
index 55928643d..72d93bed8 100644
--- a/listview/src/main/kotlin/com/alfresco/content/listview/ListFragment.kt
+++ b/listview/src/main/kotlin/com/alfresco/content/listview/ListFragment.kt
@@ -6,6 +6,7 @@ import android.os.Looper
import android.view.View
import android.widget.FrameLayout
import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.RecyclerView
@@ -189,6 +190,7 @@ abstract class ListFragment, S : ListViewState>(layoutID:
private val epoxyController: AsyncEpoxyController by lazy { epoxyController() }
private var delayedBoundary: Boolean = false
private var isViewRequiredMultiSelection = false
+ var bottomMoveButtonLayout: ConstraintLayout? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -201,6 +203,7 @@ abstract class ListFragment, S : ListViewState>(layoutID:
uploadButton = view.findViewById(R.id.upload_button)
moveHereButton = view.findViewById(R.id.move_here_button)
+ bottomMoveButtonLayout = view.findViewById(R.id.bottom_button_layout)
cancelButton = view.findViewById(R.id.cancel_button)
tvUploadingFiles = view.findViewById(R.id.tv_uploading_files)
tvPercentage = view.findViewById(R.id.tv_percentage)
@@ -256,6 +259,7 @@ abstract class ListFragment, S : ListViewState>(layoutID:
if (state.request.complete) {
refreshLayout.isRefreshing = false
}
+
epoxyController.requestModelBuild()
}
diff --git a/listview/src/main/kotlin/com/alfresco/content/listview/ListViewMessage.kt b/listview/src/main/kotlin/com/alfresco/content/listview/ListViewMessage.kt
index 80e91eade..3aac0932e 100644
--- a/listview/src/main/kotlin/com/alfresco/content/listview/ListViewMessage.kt
+++ b/listview/src/main/kotlin/com/alfresco/content/listview/ListViewMessage.kt
@@ -33,4 +33,9 @@ class ListViewMessage @JvmOverloads constructor(
fun setMessage(@StringRes stringRes: Int) {
binding.message.text = resources.getText(stringRes)
}
+
+ @ModelProp
+ fun setMessage(stringRes: String) {
+ binding.message.text = stringRes
+ }
}
diff --git a/listview/src/main/kotlin/com/alfresco/content/listview/tasks/ListViewTaskRow.kt b/listview/src/main/kotlin/com/alfresco/content/listview/tasks/ListViewTaskRow.kt
index bc88bf1eb..c5ca9ad27 100644
--- a/listview/src/main/kotlin/com/alfresco/content/listview/tasks/ListViewTaskRow.kt
+++ b/listview/src/main/kotlin/com/alfresco/content/listview/tasks/ListViewTaskRow.kt
@@ -38,7 +38,7 @@ class ListViewTaskRow @JvmOverloads constructor(
@RequiresApi(Build.VERSION_CODES.O)
@ModelProp
fun setData(entry: TaskEntry) {
- binding.title.text = entry.name
+ binding.title.text = entry.name.ifEmpty { context.getString(R.string.title_no_name) }
val localizedName = context.getLocalizedName(entry.assignee?.name ?: "")
binding.subtitle.visibility = if (localizedName.trim().isNotEmpty()) View.VISIBLE else View.GONE
binding.subtitle.text = localizedName
diff --git a/listview/src/main/res/layout/fragment_move_list.xml b/listview/src/main/res/layout/fragment_move_list.xml
index c530c6265..c1cd74756 100644
--- a/listview/src/main/res/layout/fragment_move_list.xml
+++ b/listview/src/main/res/layout/fragment_move_list.xml
@@ -54,6 +54,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
+ android:visibility="invisible"
android:layout_marginStart="@dimen/list_layout_margin_button"
android:layout_marginTop="@dimen/list_layout_margin_button"
android:layout_marginEnd="@dimen/list_layout_margin_button"
@@ -79,7 +80,6 @@
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_marginStart="14dp"
- android:text="@string/move_button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/cancel_button" />
diff --git a/listview/src/main/res/values/strings.xml b/listview/src/main/res/values/strings.xml
index 1c0fc1ec0..6617e96b1 100644
--- a/listview/src/main/res/values/strings.xml
+++ b/listview/src/main/res/values/strings.xml
@@ -13,5 +13,6 @@
Priority
Assignee
Tasks
- Maximum %d items can be selected
+ Select a maximum of %d items
+ Select
diff --git a/move/src/main/java/com/alfresco/content/move/BrowseMoveFragment.kt b/move/src/main/java/com/alfresco/content/move/BrowseMoveFragment.kt
index 19e95534f..ba1e36998 100644
--- a/move/src/main/java/com/alfresco/content/move/BrowseMoveFragment.kt
+++ b/move/src/main/java/com/alfresco/content/move/BrowseMoveFragment.kt
@@ -45,14 +45,27 @@ class BrowseMoveFragment : ListFragment(R.layo
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
+ if (args.isProcess) {
+ moveHereButton?.text = getString(R.string.select_button)
+ } else {
+ moveHereButton?.text = getString(R.string.move_button)
+ }
+
moveHereButton?.setOnClickListener {
withState(viewModel) { state ->
- val activity = requireActivity()
- val intent = Intent().apply {
- putExtra(MoveResultContract.OUTPUT_KEY, state.nodeId)
+ if (args.isProcess) {
+ state.parent?.let {
+ viewModel.setSearchResult(it)
+ }
+ requireActivity().finish()
+ } else {
+ val activity = requireActivity()
+ val intent = Intent().apply {
+ putExtra(MoveResultContract.OUTPUT_KEY, state.nodeId)
+ }
+ activity.setResult(Activity.RESULT_OK, intent)
+ activity.finish()
}
- activity.setResult(Activity.RESULT_OK, intent)
- activity.finish()
}
}
@@ -68,23 +81,33 @@ class BrowseMoveFragment : ListFragment(R.layo
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
- inflater.inflate(R.menu.menu_browse_extension, menu)
+ if (args.isProcess) {
+ inflater.inflate(R.menu.menu_browse_folder, menu)
+ } else {
+ inflater.inflate(R.menu.menu_browse_extension, menu)
+ }
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.search -> {
withState(viewModel) { state ->
- findNavController().navigateToContextualSearch(args.id ?: "", args.title ?: "", true, state.moveId)
+ if (args.isProcess) {
+ findNavController().navigateToContextualSearch(args.id ?: "", args.title ?: "", args.isProcess)
+ } else {
+ findNavController().navigateToContextualSearch(args.id ?: "", args.title ?: "", true, state.moveId)
+ }
}
true
}
+
R.id.new_folder -> {
withState(viewModel) {
viewModel.createFolder(it)
}
true
}
+
else -> super.onOptionsItemSelected(item)
}
}
@@ -99,6 +122,12 @@ class BrowseMoveFragment : ListFragment(R.layo
if (state.path == getString(R.string.nav_path_move)) {
super.disableRefreshLayout()
}
+
+ if (state.title != null) {
+ bottomMoveButtonLayout?.visibility = View.VISIBLE
+ } else {
+ bottomMoveButtonLayout?.visibility = View.INVISIBLE
+ }
super.invalidate()
}
@@ -108,7 +137,7 @@ class BrowseMoveFragment : ListFragment(R.layo
override fun onItemClicked(entry: Entry) {
if (!entry.isFolder) return
withState(viewModel) { state ->
- findNavController().navigateToFolder(entry, state.moveId)
+ findNavController().navigateToFolder(entry, state.moveId, isProcess = args.isProcess)
}
}
}
diff --git a/move/src/main/java/com/alfresco/content/move/MoveFragment.kt b/move/src/main/java/com/alfresco/content/move/MoveFragment.kt
index 299d4f979..1db155082 100644
--- a/move/src/main/java/com/alfresco/content/move/MoveFragment.kt
+++ b/move/src/main/java/com/alfresco/content/move/MoveFragment.kt
@@ -48,8 +48,12 @@ class MoveFragment : Fragment(), MavericksView {
override fun onStart() {
super.onStart()
val nodeId = viewModel.getMyFilesNodeId()
- args.entryObj?.let {
- findNavController().navigateToMoveParent(nodeId, it.id, getString(R.string.browse_menu_personal))
+
+ val entryObj = args.entryObj
+ if (args.entryObj != null) {
+ entryObj?.id?.let { findNavController().navigateToMoveParent(nodeId, it, getString(R.string.browse_menu_personal)) }
+ } else {
+ findNavController().navigateToMoveParent(nodeId, "", getString(R.string.browse_menu_personal), true)
}
}
diff --git a/process-app/.gitignore b/process-app/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/process-app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/process-app/build.gradle b/process-app/build.gradle
new file mode 100644
index 000000000..2d1a0000e
--- /dev/null
+++ b/process-app/build.gradle
@@ -0,0 +1,88 @@
+plugins {
+ id 'com.android.library'
+ id 'org.jetbrains.kotlin.android'
+ id 'kotlin-kapt'
+}
+
+android {
+ namespace 'com.alfresco.content.process'
+ compileSdk 33
+
+ defaultConfig {
+ minSdk 26
+ targetSdk 33
+ vectorDrawables {
+ useSupportLibrary true
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
+ }
+ kotlinOptions {
+ jvmTarget = '17'
+ }
+
+ buildFeatures {
+ compose true
+ viewBinding true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.5.4"
+ }
+ packagingOptions {
+ resources {
+ excludes += '/META-INF/{AL2.0,LGPL2.1}'
+ }
+ }
+}
+
+dependencies {
+
+ implementation project(':base')
+ 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 project(':search')
+
+ implementation libs.alfresco.process
+ implementation libs.androidx.core
+ implementation libs.androidx.appcompat
+ implementation libs.androidx.lifecycle.runtime
+ implementation libs.activity.compose
+ implementation platform(libs.compose.bom)
+ implementation libs.ui
+ implementation libs.activity.compose
+ implementation libs.ui.graphics
+ implementation libs.ui.compose.viewbinding
+ implementation libs.ui.tooling.preview
+ implementation libs.material3
+
+ implementation libs.navigation.compose
+ implementation libs.mavericks.compose
+ 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
new file mode 100644
index 000000000..a1c319b56
--- /dev/null
+++ b/process-app/src/main/AndroidManifest.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/AmountInputField.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/AmountInputField.kt
new file mode 100644
index 000000000..01dc713fc
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/AmountInputField.kt
@@ -0,0 +1,70 @@
+package com.alfresco.content.process.ui.components
+
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.tooling.preview.Preview
+import com.alfresco.content.data.payloads.FieldsData
+import com.alfresco.content.process.ui.utils.inputField
+
+@Composable
+fun AmountInputField(
+ textFieldValue: String? = null,
+ onValueChanged: (String) -> Unit = { },
+ fieldsData: FieldsData = FieldsData(),
+ errorData: Pair = Pair(false, ""),
+) {
+ val keyboardOptions = KeyboardOptions.Default.copy(
+ imeAction = ImeAction.Next,
+ keyboardType = KeyboardType.Number,
+ )
+
+ val leadingIcon: @Composable () -> Unit = when {
+ !fieldsData.currency.isNullOrEmpty() -> {
+ {
+ Text(
+ text = fieldsData.currency ?: "$",
+ color = MaterialTheme.colorScheme.onPrimary,
+ )
+ }
+ }
+
+ else -> {
+ {
+ Text(
+ text = "$",
+ color = MaterialTheme.colorScheme.onPrimary,
+ )
+ }
+ }
+ }
+
+ InputFieldWithLeading(
+ colors = OutlinedTextFieldDefaults.colors(
+ unfocusedBorderColor = MaterialTheme.colorScheme.onSurfaceVariant,
+ unfocusedLabelColor = MaterialTheme.colorScheme.onPrimary,
+ unfocusedPlaceholderColor = MaterialTheme.colorScheme.onPrimary,
+ unfocusedTextColor = MaterialTheme.colorScheme.onPrimary,
+ ),
+ modifier = Modifier.inputField(),
+ maxLines = 1,
+ textFieldValue = textFieldValue,
+ onValueChanged = onValueChanged,
+ fieldsData = fieldsData,
+ keyboardOptions = keyboardOptions,
+ leadingIcon = leadingIcon,
+ isError = errorData.first,
+ errorMessage = errorData.second,
+ )
+}
+
+@Preview
+@Composable
+fun AmountInputFieldPreview() {
+ AmountInputField()
+}
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..e703d9a31
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/AttachFilesField.kt
@@ -0,0 +1,97 @@
+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.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 com.alfresco.content.data.Entry
+import com.alfresco.content.data.payloads.FieldsData
+import com.alfresco.content.process.R
+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(),
+ onUserTap: (Boolean) -> Unit = { },
+ errorData: Pair = Pair(false, ""),
+) {
+ 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)
+ }
+
+ 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 = {
+ onUserTap(true)
+ }) {
+ Icon(
+ imageVector = Icons.Default.Attachment,
+ tint = AlfrescoBlue300,
+ contentDescription = "",
+ )
+ }
+ }
+ Text(
+ text = contentValue,
+ style = TextStyle(
+ color = MaterialTheme.colorScheme.onPrimary,
+ 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/AttachFolderField.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/AttachFolderField.kt
new file mode 100644
index 000000000..b8f0f6d87
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/AttachFolderField.kt
@@ -0,0 +1,124 @@
+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.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Attachment
+import androidx.compose.material.icons.filled.Clear
+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 androidx.navigation.compose.rememberNavController
+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.theme.AlfrescoBlue300
+import com.alfresco.content.process.ui.theme.AlfrescoError
+
+@Composable
+fun AttachFolderField(
+ fieldsData: FieldsData = FieldsData(),
+ onUserTap: (Boolean) -> Unit = { },
+ onResetFolder: (Boolean) -> Unit = { },
+ navController: NavController,
+ errorData: Pair = Pair(false, ""),
+) {
+ val labelWithAsterisk = buildAnnotatedString {
+ append(fieldsData.name)
+ if (fieldsData.required) {
+ withStyle(style = SpanStyle(color = AlfrescoError)) {
+ append(" *") // Adding a red asterisk for mandatory fields
+ }
+ }
+ }
+
+ val contentValue = (fieldsData.value as? Entry)?.name ?: stringResource(id = R.string.no_attached_folder)
+
+ 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 = {
+ onUserTap(true)
+ }) {
+ Icon(
+ imageVector = Icons.Default.Attachment,
+ tint = AlfrescoBlue300,
+ contentDescription = "",
+ )
+ }
+ }
+
+ Row(
+ horizontalArrangement = Arrangement.SpaceBetween,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = 4.dp, end = 12.dp, top = 0.dp),
+ ) {
+ Text(
+ modifier = Modifier
+ .padding(top = 0.dp)
+ .align(alignment = Alignment.CenterVertically),
+ text = contentValue,
+ style = TextStyle(
+ color = MaterialTheme.colorScheme.onPrimary,
+ fontSize = 12.sp,
+ ),
+ )
+ if (fieldsData.value != null) {
+ IconButton(
+ onClick = {
+ onResetFolder(true)
+ },
+ modifier = Modifier
+ .size(20.dp)
+ .padding(top = 0.dp),
+ ) {
+ Icon(
+ imageVector = Icons.Default.Clear,
+ tint = AlfrescoBlue300,
+ contentDescription = "",
+ )
+ }
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+fun AttachFolderFieldPreview() {
+ AttachFolderField(navController = rememberNavController())
+}
diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/CheckboxField.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/CheckboxField.kt
new file mode 100644
index 000000000..7412fd56e
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/CheckboxField.kt
@@ -0,0 +1,176 @@
+package com.alfresco.content.process.ui.components
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.ClickableText
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+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.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.clearAndSetSemantics
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.unit.dp
+import com.alfresco.content.component.ComponentBuilder
+import com.alfresco.content.component.ComponentData
+import com.alfresco.content.component.ComponentType
+import com.alfresco.content.data.payloads.FieldsData
+import com.alfresco.content.process.R
+import com.alfresco.content.process.ui.theme.AlfrescoBlue300
+import com.alfresco.content.process.ui.theme.AlfrescoError
+
+@Composable
+fun CheckBoxField(
+ title: String = "",
+ checkedValue: Boolean = false,
+ onCheckChanged: (Boolean) -> Unit = { },
+ fieldsData: FieldsData = FieldsData(),
+ errorData: Pair = Pair(false, ""),
+) {
+ val context = LocalContext.current
+
+ val minimumLineLength = 2 // Change this to your desired value
+
+ var showReadMoreButtonState by remember { mutableStateOf(false) }
+
+ var visibleText by remember { mutableStateOf("") }
+ val spaceAsteric = " *"
+ val readMore = stringResource(id = R.string.suffix_view_all)
+ val suffix = spaceAsteric + readMore
+ var lineCount = 1
+
+ val labelWithAsterisk = customLabel(visibleText, showReadMoreButtonState, fieldsData, spaceAsteric, readMore)
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth(),
+ horizontalAlignment = Alignment.Start,
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ ) {
+ Checkbox(
+ modifier = Modifier
+ .align(alignment = Alignment.Top),
+ checked = checkedValue,
+ onCheckedChange = { isChecked ->
+ onCheckChanged(isChecked)
+ },
+ )
+ ClickableText(
+ maxLines = minimumLineLength,
+ text = labelWithAsterisk,
+ style = TextStyle(
+ color = MaterialTheme.colorScheme.onSurface,
+ ),
+ onClick = {
+ labelWithAsterisk.getStringAnnotations(it, it).firstOrNull()?.let {
+ ComponentBuilder(
+ context,
+ ComponentData(
+ name = title,
+ query = "",
+ value = fieldsData.name,
+ selector = ComponentType.VIEW_TEXT.value,
+ ),
+ )
+ .onApply { name, query, _ ->
+ }
+ .onReset { name, query, _ ->
+ }
+ .onCancel {
+ }
+ .show()
+ }
+ },
+ modifier = Modifier
+ .padding(end = 4.dp, top = if (lineCount == 1) 0.dp else 6.dp)
+ .align(alignment = if (lineCount == 1) Alignment.CenterVertically else Alignment.Top)
+ .clearAndSetSemantics { },
+ onTextLayout = { textLayoutResult: TextLayoutResult ->
+ lineCount = textLayoutResult.lineCount
+ if (textLayoutResult.lineCount > minimumLineLength - 1 && !showReadMoreButtonState) {
+ val endIndex = textLayoutResult.getLineEnd(minimumLineLength - 1)
+ visibleText = with(labelWithAsterisk) {
+ this.substring(0, endIndex = endIndex - suffix.length)
+ }
+ showReadMoreButtonState = true
+ }
+ },
+ )
+ }
+
+ if (errorData.first) {
+ Text(
+ text = errorData.second,
+ color = AlfrescoError,
+ modifier = Modifier
+ .padding(horizontal = 16.dp, vertical = 0.dp), // Adjust padding as needed
+ style = MaterialTheme.typography.titleSmall,
+ textAlign = TextAlign.Start,
+ )
+ }
+ }
+}
+
+@Composable
+private fun customLabel(visibleText: String, showReadMoreButtonState: Boolean, fieldsData: FieldsData, spaceAsteric: String, readMore: String) =
+ if (visibleText.isNotEmpty() && showReadMoreButtonState) {
+ buildAnnotatedString {
+ val labelReadMore = (visibleText) + spaceAsteric + readMore
+
+ val startIndexReadMore = labelReadMore.indexOf(readMore)
+ val endIndexReadMore = startIndexReadMore + readMore.length
+
+ val startIndexAsteric = labelReadMore.indexOf(spaceAsteric)
+ val endIndexAsteric = startIndexAsteric + spaceAsteric.length
+
+ append(labelReadMore)
+
+ if (fieldsData.required) {
+ addStyle(
+ style = SpanStyle(color = AlfrescoError),
+ start = startIndexAsteric,
+ end = endIndexAsteric,
+ )
+ }
+ addStyle(
+ style = SpanStyle(color = AlfrescoBlue300),
+ start = startIndexReadMore + 1,
+ end = endIndexReadMore,
+ )
+
+ addStringAnnotation(
+ "tagMinified",
+ readMore,
+ start = startIndexReadMore,
+ end = endIndexReadMore,
+ )
+ }
+ } else {
+ buildAnnotatedString {
+ append(fieldsData.name)
+ if (fieldsData.required) {
+ withStyle(style = SpanStyle(color = AlfrescoError)) {
+ append(" *") // Adding a red asterisk for mandatory fields
+ }
+ }
+ }
+ }
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
new file mode 100644
index 000000000..8a5da1fd5
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/ComposeTopBar.kt
@@ -0,0 +1,32 @@
+import android.app.Activity
+import androidx.compose.foundation.layout.Column
+import androidx.compose.material3.Divider
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+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.composeviews.BackButton
+import com.alfresco.content.process.ui.theme.SeparateColorGrayLT
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ComposeTopBar() {
+ val context = LocalContext.current
+ Column {
+ TopAppBar(
+ title = {
+ Text(
+ text = stringResource(id = R.string.action_start_workflow),
+ )
+ },
+ navigationIcon = {
+ BackButton(onClick = { (context as Activity).finish() })
+ },
+ )
+ Divider(color = SeparateColorGrayLT, thickness = 1.dp)
+ }
+}
diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/CustomLinearProgressIndicator.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/CustomLinearProgressIndicator.kt
new file mode 100644
index 000000000..63f696e00
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/CustomLinearProgressIndicator.kt
@@ -0,0 +1,28 @@
+package com.alfresco.content.process.ui.components
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.LinearProgressIndicator
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun CustomLinearProgressIndicator(padding: PaddingValues) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(2.dp)
+ .padding(padding), // Adjust padding as needed
+ ) {
+ LinearProgressIndicator(
+ color = Color.Blue, // Set your desired color here
+ modifier = Modifier.align(Alignment.TopCenter),
+ )
+ }
+}
diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/DateTimeField.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/DateTimeField.kt
new file mode 100644
index 000000000..3f8bac95a
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/DateTimeField.kt
@@ -0,0 +1,87 @@
+package com.alfresco.content.process.ui.components
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.tooling.preview.Preview
+import com.alfresco.content.DATE_FORMAT_1
+import com.alfresco.content.DATE_FORMAT_2_1
+import com.alfresco.content.component.DatePickerBuilder
+import com.alfresco.content.data.payloads.FieldType
+import com.alfresco.content.data.payloads.FieldsData
+import com.alfresco.content.getLocalFormattedDate
+import com.alfresco.content.process.ui.utils.inputField
+import com.alfresco.content.updateDateFormat
+
+@Composable
+fun DateTimeField(
+ dateTimeValue: String = "",
+ onValueChanged: (String) -> Unit = {},
+ fieldsData: FieldsData = FieldsData(),
+ errorData: Pair = Pair(false, ""),
+) {
+ val keyboardOptions = KeyboardOptions.Default.copy(
+ imeAction = ImeAction.Next,
+ keyboardType = KeyboardType.Text,
+ )
+
+ val context = LocalContext.current
+
+ var dateTime = dateTimeValue
+
+ when (fieldsData.type.lowercase()) {
+ FieldType.DATE.value() -> {
+ val date = fieldsData.getDate(DATE_FORMAT_1, DATE_FORMAT_2_1)
+ if (date.first.isNotEmpty()) {
+ val dateFormat = updateDateFormat(fieldsData.params?.field?.dateDisplayFormat) ?: DATE_FORMAT_2_1
+ dateTime = date.first.getLocalFormattedDate(date.second, dateFormat)
+ }
+ }
+ }
+
+ InputField(
+ colors = OutlinedTextFieldDefaults.colors(
+ disabledBorderColor = MaterialTheme.colorScheme.onSurfaceVariant,
+ disabledTextColor = MaterialTheme.colorScheme.onPrimary,
+ disabledPlaceholderColor = MaterialTheme.colorScheme.onPrimary,
+ disabledLabelColor = MaterialTheme.colorScheme.onPrimary,
+ ),
+ modifier = Modifier
+ .inputField()
+ .clickable {
+ DatePickerBuilder(
+ context = context,
+ fromDate = "",
+ isFrom = true,
+ isFutureDate = true,
+ dateFormat = DATE_FORMAT_2_1,
+ fieldsData = fieldsData,
+ )
+ .onSuccess { date ->
+ onValueChanged(date)
+ }
+ .onFailure {}
+ .show()
+ },
+ maxLines = 1,
+ textFieldValue = dateTime,
+ onValueChanged = onValueChanged,
+ fieldsData = fieldsData,
+ keyboardOptions = keyboardOptions,
+ isError = errorData.first,
+ errorMessage = errorData.second,
+ isEnabled = false,
+ )
+}
+
+@Preview
+@Composable
+fun DateTimeFieldPreview() {
+ DateTimeField()
+}
diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/DropdownField.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/DropdownField.kt
new file mode 100644
index 000000000..2d92aa806
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/DropdownField.kt
@@ -0,0 +1,74 @@
+package com.alfresco.content.process.ui.components
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.tooling.preview.Preview
+import com.alfresco.content.component.ComponentBuilder
+import com.alfresco.content.component.ComponentData
+import com.alfresco.content.data.payloads.FieldsData
+import com.alfresco.content.process.ui.utils.inputField
+
+@Composable
+fun DropdownField(
+ nameText: String = "",
+ queryText: String = "",
+ onValueChanged: (Pair) -> Unit = { },
+ fieldsData: FieldsData = FieldsData(),
+ errorData: Pair = Pair(false, ""),
+) {
+ val keyboardOptions = KeyboardOptions.Default.copy(
+ imeAction = ImeAction.Next,
+ keyboardType = KeyboardType.Text,
+ )
+
+ val context = LocalContext.current
+
+ InputField(
+ colors = OutlinedTextFieldDefaults.colors(
+ disabledBorderColor = MaterialTheme.colorScheme.onSurfaceVariant,
+ disabledTextColor = MaterialTheme.colorScheme.onPrimary,
+ disabledPlaceholderColor = MaterialTheme.colorScheme.onPrimary,
+ disabledLabelColor = MaterialTheme.colorScheme.onPrimary,
+ ),
+ modifier = Modifier
+ .inputField()
+ .clickable {
+ val componentData = ComponentData.with(
+ fieldsData,
+ nameText,
+ queryText,
+ )
+ ComponentBuilder(context, componentData)
+ .onApply { name, query, _ ->
+ onValueChanged(Pair(name, query))
+ }
+ .onReset { name, query, _ ->
+ onValueChanged(Pair(fieldsData.placeHolder ?: "", ""))
+ }
+ .onCancel {
+ onValueChanged(Pair(nameText, queryText))
+ }
+ .show()
+ },
+ maxLines = 1,
+ textFieldValue = nameText,
+ fieldsData = fieldsData,
+ keyboardOptions = keyboardOptions,
+ isError = errorData.first,
+ errorMessage = errorData.second,
+ isEnabled = false,
+ )
+}
+
+@Preview
+@Composable
+fun DropdownFieldPreview() {
+ DropdownField()
+}
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
new file mode 100644
index 000000000..40364e059
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/FloatingActionButton.kt
@@ -0,0 +1,72 @@
+package com.alfresco.content.process.ui.components
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.PlaylistAdd
+import androidx.compose.material3.ExtendedFloatingActionButton
+import androidx.compose.material3.FloatingActionButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.airbnb.mvrx.compose.collectAsState
+import com.alfresco.content.component.ComponentBuilder
+import com.alfresco.content.component.ComponentData
+import com.alfresco.content.data.OptionsModel
+import com.alfresco.content.process.R
+import com.alfresco.content.process.ui.fragments.FormViewModel
+import com.alfresco.content.process.ui.fragments.ProcessFragment
+import com.alfresco.content.process.ui.utils.getContentList
+
+@Composable
+fun FloatingActionButton(outcomes: List, fragment: ProcessFragment, viewModel: FormViewModel) {
+ val context = LocalContext.current
+ val state by viewModel.collectAsState()
+
+ ExtendedFloatingActionButton(
+ onClick = {
+ if (state.enabledOutcomes) {
+ val componentData = ComponentData.with(
+ outcomes,
+ "",
+ "",
+ )
+ ComponentBuilder(context, componentData)
+ .onApply { name, query, _ ->
+
+ val contentList = getContentList(state)
+
+ if (contentList.isNotEmpty()) {
+ viewModel.optionsModel = OptionsModel(id = query, name = name)
+ fragment.confirmContentQueuePrompt()
+ } else {
+ viewModel.performOutcomes(
+ OptionsModel(
+ id = query.ifEmpty { name },
+ name = name,
+ ),
+ )
+ }
+ }
+ .onReset { name, query, _ ->
+ }
+ .onCancel {
+ }
+ .show()
+ }
+ },
+ containerColor = if (state.enabledOutcomes) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
+ icon = { Icon(Icons.Filled.PlaylistAdd, stringResource(id = R.string.accessibility_process_actions), tint = Color.White) },
+ text = { Text(text = stringResource(id = R.string.title_actions), color = Color.White) },
+ elevation = FloatingActionButtonDefaults.elevation(
+ defaultElevation = 0.dp,
+ pressedElevation = 0.dp,
+ focusedElevation = 0.dp,
+ hoveredElevation = 0.dp,
+ ),
+ )
+}
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
new file mode 100644
index 000000000..29745b255
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/FormViewModelExtension.kt
@@ -0,0 +1,17 @@
+package com.alfresco.content.process.ui.components
+
+import com.alfresco.content.process.ui.fragments.FormViewModel
+import com.alfresco.content.process.ui.models.UpdateProcessData
+import com.alfresco.content.process.ui.models.UpdateTasksData
+import com.alfresco.events.EventBus
+import kotlinx.coroutines.launch
+
+/**
+ * update the list of workflow if new entry created
+ */
+fun FormViewModel.updateProcessList() {
+ viewModelScope.launch {
+ EventBus.default.send(UpdateTasksData(isRefresh = true))
+ EventBus.default.send(UpdateProcessData(isRefresh = true))
+ }
+}
diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/HyperLinkField.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/HyperLinkField.kt
new file mode 100644
index 000000000..167d0e903
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/HyperLinkField.kt
@@ -0,0 +1,111 @@
+package com.alfresco.content.process.ui.components
+
+import android.content.Intent
+import android.provider.MediaStore.Audio.AudioColumns.TITLE_KEY
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Link
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalUriHandler
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.tooling.preview.Preview
+import com.alfresco.content.common.SharedURLParser
+import com.alfresco.content.common.SharedURLParser.Companion.ID_KEY
+import com.alfresco.content.common.SharedURLParser.Companion.MODE_KEY
+import com.alfresco.content.data.payloads.FieldsData
+import com.alfresco.content.process.R
+import com.alfresco.content.process.ui.utils.inputField
+import com.alfresco.content.process.ui.utils.trailingIconColor
+import com.alfresco.content.viewer.ViewerActivity
+import kotlinx.coroutines.launch
+import java.net.URL
+
+@Composable
+fun HyperLinkField(
+ fieldsData: FieldsData = FieldsData(),
+ snackbarHostState: SnackbarHostState,
+) {
+ val uriHandler = LocalUriHandler.current
+ val context = LocalContext.current
+ val scope = rememberCoroutineScope()
+
+ val keyboardOptions = KeyboardOptions.Default.copy(
+ imeAction = ImeAction.Next,
+ keyboardType = KeyboardType.Text,
+ )
+
+ val leadingIcon: @Composable () -> Unit = {
+ Icon(
+ imageVector = Icons.Default.Link,
+ contentDescription = stringResource(R.string.accessibility_link_icon),
+ tint = trailingIconColor(),
+ )
+ }
+
+ InputFieldWithLeading(
+ modifier = Modifier
+ .inputField()
+ .clickable {
+ val url = fieldsData.hyperlinkUrl ?: ""
+ if (isValidUrl(url)) {
+ val urlData =
+ SharedURLParser().getEntryIdFromShareURL(url, true)
+ if (urlData.second.isNotEmpty()) {
+ context.startActivity(
+ Intent(context, ViewerActivity::class.java)
+ .putExtra(ID_KEY, urlData.second)
+ .putExtra(
+ MODE_KEY,
+ if (urlData.first) SharedURLParser.VALUE_SHARE else SharedURLParser.VALUE_REMOTE,
+ )
+ .putExtra(TITLE_KEY, "Preview"),
+ )
+ } else {
+ uriHandler.openUri(url)
+ }
+ } else {
+ scope.launch {
+ val message = context.getString(R.string.error_hyperlink_invalid_url, fieldsData.name)
+ snackbarHostState.showSnackbar(message)
+ }
+ }
+ },
+ colors = OutlinedTextFieldDefaults.colors(
+ disabledBorderColor = MaterialTheme.colorScheme.onSurfaceVariant,
+ disabledTextColor = MaterialTheme.colorScheme.onPrimary,
+ disabledPlaceholderColor = MaterialTheme.colorScheme.onPrimary,
+ disabledLabelColor = MaterialTheme.colorScheme.onPrimary,
+ ),
+ maxLines = 1,
+ textFieldValue = fieldsData.displayText,
+ fieldsData = fieldsData,
+ keyboardOptions = keyboardOptions,
+ leadingIcon = leadingIcon,
+ isEnabled = false,
+ )
+}
+
+fun isValidUrl(url: String): Boolean {
+ return try {
+ URL(url).toURI()
+ true
+ } catch (e: Exception) {
+ false
+ }
+}
+
+@Preview
+@Composable
+fun HyperLinkFieldPreview() {
+ HyperLinkField(snackbarHostState = SnackbarHostState())
+}
diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/InputChip.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/InputChip.kt
new file mode 100644
index 000000000..23a3ddda3
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/InputChip.kt
@@ -0,0 +1,112 @@
+package com.alfresco.content.process.ui.components
+
+import android.content.Context
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Cancel
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.InputChip
+import androidx.compose.material3.InputChipDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.alfresco.content.data.UserGroupDetails
+import com.alfresco.content.getLocalizedName
+import com.alfresco.content.process.R
+import com.alfresco.content.process.ui.theme.SeparateColorGrayDT
+import com.alfresco.content.process.ui.theme.SeparateColorGrayLT
+import com.alfresco.content.process.ui.theme.chipBackgroundColorGrayDT
+import com.alfresco.content.process.ui.theme.chipBackgroundColorGrayLT
+import com.alfresco.content.process.ui.theme.chipColorGrayDT
+import com.alfresco.content.process.ui.theme.chipColorGrayLT
+import com.alfresco.content.process.ui.theme.isNightMode
+import com.alfresco.content.process.ui.utils.trailingIconColor
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun InputChip(
+ context: Context,
+ userDetail: UserGroupDetails,
+ onValueChanged: (UserGroupDetails?) -> Unit = { },
+) {
+ val isNightMode = isNightMode()
+ InputChip(
+ modifier = Modifier.padding(vertical = 8.dp),
+ trailingIcon = {
+ IconButton(
+ onClick = {
+ onValueChanged(null)
+ },
+ ) {
+ Icon(
+ imageVector = Icons.Default.Cancel,
+ contentDescription = stringResource(R.string.accessibility_clear_text),
+ tint = trailingIconColor(),
+ )
+ }
+ },
+ onClick = {
+ },
+ label = {
+ if (userDetail.groupName.isNotEmpty()) {
+ Text(context.getLocalizedName(userDetail.groupName))
+ } else {
+ Text(context.getLocalizedName(userDetail.name))
+ }
+ },
+ selected = true,
+ shape = RoundedCornerShape(24.dp),
+ border = InputChipDefaults.inputChipBorder(
+ selectedBorderWidth = 0.dp,
+
+ ),
+ colors = getInputChipColors(),
+ leadingIcon = {
+ Text(
+ color = if (isNightMode) SeparateColorGrayDT else SeparateColorGrayLT,
+ modifier = Modifier
+ .padding(16.dp)
+ .drawBehind {
+ drawCircle(
+ color = if (isNightMode) chipColorGrayDT else chipColorGrayLT,
+ radius = this.size.maxDimension,
+ )
+ },
+ text = context.getLocalizedName(userDetail.nameInitial),
+ fontSize = 12.sp,
+ )
+ },
+ )
+}
+
+@Preview
+@Composable
+fun InputChipPreview() {
+ InputChip(LocalContext.current, UserGroupDetails())
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun getInputChipColors() = if (isNightMode()) {
+ InputChipDefaults.inputChipColors(
+ labelColor = SeparateColorGrayDT,
+ selectedLabelColor = SeparateColorGrayDT,
+ selectedLeadingIconColor = SeparateColorGrayDT,
+ selectedContainerColor = chipBackgroundColorGrayDT,
+ )
+} else
+ InputChipDefaults.inputChipColors(
+ labelColor = SeparateColorGrayLT,
+ selectedLabelColor = SeparateColorGrayLT,
+ selectedLeadingIconColor = SeparateColorGrayLT,
+ selectedContainerColor = chipBackgroundColorGrayLT,
+ )
diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/InputField.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/InputField.kt
new file mode 100644
index 000000000..9c3231e4d
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/InputField.kt
@@ -0,0 +1,229 @@
+package com.alfresco.content.process.ui.components
+
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextFieldColors
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.unit.dp
+import com.alfresco.content.data.payloads.FieldsData
+import com.alfresco.content.process.ui.theme.AlfrescoError
+
+@Composable
+fun InputField(
+ modifier: Modifier = Modifier,
+ colors: TextFieldColors = OutlinedTextFieldDefaults.colors(),
+ maxLines: Int = 1,
+ textFieldValue: String? = null,
+ onValueChanged: (String) -> Unit = { },
+ fieldsData: FieldsData = FieldsData(),
+ keyboardOptions: KeyboardOptions,
+ isError: Boolean = false,
+ errorMessage: String = "",
+ isEnabled: Boolean = true,
+ dateFormat: String? = null,
+) {
+ var selectionState by remember { mutableIntStateOf(0) }
+ // State to keep track of focus state
+ var focusState by remember { mutableStateOf(false) }
+
+ val keyboardActions = KeyboardActions(
+ onDone = {
+ // Handle the action when the "Done" button on the keyboard is pressed
+ },
+ )
+
+ val adjustedModifier = if (maxLines > 1) {
+ modifier.height(120.dp)
+ } else {
+ modifier
+ }
+
+ val labelWithAsterisk = buildAnnotatedString {
+ append(fieldsData.name)
+ if (fieldsData.required) {
+ withStyle(style = SpanStyle(color = AlfrescoError)) {
+ append(" *") // Adding a red asterisk for mandatory fields
+ }
+ }
+ if (dateFormat != null) {
+ append(" ($dateFormat)")
+ }
+ }
+
+ OutlinedTextField(
+ colors = colors,
+ enabled = isEnabled,
+ value = textFieldValue ?: "", // Initial value of the text field
+ onValueChange = { newValue ->
+ val newText = if (fieldsData.maxLength > 0) {
+ if (newValue.length <= fieldsData.maxLength) {
+ newValue
+ } else {
+ newValue.take(fieldsData.maxLength)
+ }
+ } else {
+ newValue
+ }
+ // Calculate the selection range based on the cursor position
+ val cursorPosition = if (selectionState > newText.length) newText.length else selectionState
+
+ if (textFieldValue != newText) {
+ // Set the cursor position after updating the text
+ onValueChanged(newText)
+ selectionState = cursorPosition
+ }
+ },
+ modifier = adjustedModifier.onFocusChanged {
+ focusState = it.isFocused
+ },
+ label = {
+ Text(
+ text = labelWithAsterisk,
+ modifier = Modifier.padding(end = 4.dp),
+ )
+ }, // Label for the text field
+ placeholder = { Text(fieldsData.placeHolder ?: "") }, // Placeholder text
+ maxLines = maxLines, // Set the maximum number of lines to the specified value
+ keyboardOptions = keyboardOptions, // Set keyboard type
+ keyboardActions = keyboardActions,
+ isError = isError,
+ trailingIcon = {
+ TrailingInputField(
+ focusState = focusState,
+ textValue = textFieldValue,
+ errorMessage = errorMessage,
+ isError = isError,
+ fieldsData = fieldsData,
+ onValueChanged = onValueChanged,
+ )
+ },
+ supportingText = {
+ if (focusState) {
+ Text(
+ text = errorMessage,
+ color = AlfrescoError,
+ textAlign = TextAlign.Start,
+ overflow = TextOverflow.Clip,
+ )
+ }
+ },
+ )
+}
+
+@Composable
+fun InputFieldWithLeading(
+ modifier: Modifier = Modifier,
+ colors: TextFieldColors = OutlinedTextFieldDefaults.colors(),
+ maxLines: Int = 1,
+ textFieldValue: String? = null,
+ onValueChanged: (String) -> Unit = { },
+ fieldsData: FieldsData = FieldsData(),
+ keyboardOptions: KeyboardOptions,
+ leadingIcon: @Composable () -> Unit = {},
+ isError: Boolean = false,
+ errorMessage: String = "",
+ isEnabled: Boolean = true,
+) {
+ var selectionState by remember { mutableIntStateOf(0) }
+ // State to keep track of focus state
+ var focusState by remember { mutableStateOf(false) }
+
+ val keyboardActions = KeyboardActions(
+ onDone = {
+ // Handle the action when the "Done" button on the keyboard is pressed
+ },
+ )
+
+ val adjustedModifier = if (maxLines > 1) {
+ modifier.height(120.dp)
+ } else {
+ modifier
+ }
+
+ val labelWithAsterisk = buildAnnotatedString {
+ append(fieldsData.name)
+ if (fieldsData.required) {
+ withStyle(style = SpanStyle(color = AlfrescoError)) {
+ append(" *") // Adding a red asterisk for mandatory fields
+ }
+ }
+ }
+
+ OutlinedTextField(
+ colors = colors,
+ enabled = isEnabled,
+ value = textFieldValue ?: "", // Initial value of the text field
+ onValueChange = { newValue ->
+ val newText = if (fieldsData.maxLength > 0) {
+ if (newValue.length <= fieldsData.maxLength) {
+ newValue
+ } else {
+ newValue.take(fieldsData.maxLength)
+ }
+ } else {
+ newValue
+ }
+ // Calculate the selection range based on the cursor position
+ val cursorPosition = if (selectionState > newText.length) newText.length else selectionState
+
+ if (textFieldValue != newText) {
+ // Set the cursor position after updating the text
+ onValueChanged(newText)
+ selectionState = cursorPosition
+ }
+ },
+ modifier = adjustedModifier
+ .onFocusChanged {
+ focusState = it.isFocused
+ },
+ label = {
+ Text(
+ text = labelWithAsterisk,
+ modifier = Modifier.padding(end = 4.dp),
+ )
+ }, // Label for the text field
+ placeholder = { Text(fieldsData.placeHolder ?: "") }, // Placeholder text
+ maxLines = maxLines, // Set the maximum number of lines to the specified value
+ keyboardOptions = keyboardOptions, // Set keyboard type
+ keyboardActions = keyboardActions,
+ isError = isError,
+ leadingIcon = leadingIcon,
+ trailingIcon = {
+ TrailingInputField(
+ focusState = focusState,
+ textValue = textFieldValue,
+ errorMessage = errorMessage,
+ isError = isError,
+ fieldsData = fieldsData,
+ onValueChanged = onValueChanged,
+ )
+ },
+ supportingText = {
+ if (focusState) {
+ Text(
+ text = errorMessage,
+ color = AlfrescoError,
+ textAlign = TextAlign.Start,
+ overflow = TextOverflow.Clip,
+ )
+ }
+ },
+ )
+}
diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/IntegerInputField.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/IntegerInputField.kt
new file mode 100644
index 000000000..94a122536
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/IntegerInputField.kt
@@ -0,0 +1,48 @@
+package com.alfresco.content.process.ui.components
+
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.tooling.preview.Preview
+import com.alfresco.content.data.payloads.FieldsData
+import com.alfresco.content.process.ui.utils.inputField
+
+@Composable
+fun IntegerInputField(
+ textFieldValue: String? = null,
+ onValueChanged: (String) -> Unit = { },
+ fieldsData: FieldsData = FieldsData(),
+ errorData: Pair = Pair(false, ""),
+) {
+ val keyboardOptions = KeyboardOptions.Default.copy(
+ imeAction = ImeAction.Next,
+ keyboardType = KeyboardType.Number,
+ )
+
+ InputField(
+ colors = OutlinedTextFieldDefaults.colors(
+ unfocusedBorderColor = MaterialTheme.colorScheme.onSurfaceVariant,
+ unfocusedLabelColor = MaterialTheme.colorScheme.onPrimary,
+ unfocusedPlaceholderColor = MaterialTheme.colorScheme.onPrimary,
+ unfocusedTextColor = MaterialTheme.colorScheme.onPrimary,
+ ),
+ modifier = Modifier.inputField(),
+ maxLines = 1,
+ textFieldValue = textFieldValue,
+ onValueChanged = onValueChanged,
+ fieldsData = fieldsData,
+ keyboardOptions = keyboardOptions,
+ isError = errorData.first,
+ errorMessage = errorData.second,
+ )
+}
+
+@Preview
+@Composable
+fun IntegerInputFieldPreview() {
+ IntegerInputField()
+}
diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/MultiLineInputField.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/MultiLineInputField.kt
new file mode 100644
index 000000000..520dac775
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/MultiLineInputField.kt
@@ -0,0 +1,46 @@
+package com.alfresco.content.process.ui.components
+
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.tooling.preview.Preview
+import com.alfresco.content.data.payloads.FieldsData
+import com.alfresco.content.process.ui.utils.inputField
+
+@Composable
+fun MultiLineInputField(
+ textFieldValue: String? = null,
+ onValueChanged: (String) -> Unit = { },
+ fieldsData: FieldsData = FieldsData(),
+ errorData: Pair = Pair(false, ""),
+) {
+ val keyboardOptions = KeyboardOptions.Default.copy(
+ keyboardType = KeyboardType.Text,
+ )
+
+ InputField(
+ colors = OutlinedTextFieldDefaults.colors(
+ unfocusedBorderColor = MaterialTheme.colorScheme.onSurfaceVariant,
+ unfocusedLabelColor = MaterialTheme.colorScheme.onPrimary,
+ unfocusedPlaceholderColor = MaterialTheme.colorScheme.onPrimary,
+ unfocusedTextColor = MaterialTheme.colorScheme.onPrimary,
+ ),
+ modifier = Modifier.inputField(),
+ maxLines = 5,
+ textFieldValue = textFieldValue,
+ onValueChanged = onValueChanged,
+ fieldsData = fieldsData,
+ keyboardOptions = keyboardOptions,
+ isError = errorData.first,
+ errorMessage = errorData.second,
+ )
+}
+
+@Preview
+@Composable
+fun MultiLineInputFieldPreview() {
+ SingleLineInputField()
+}
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
new file mode 100644
index 000000000..ba39e4f23
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/Outcomes.kt
@@ -0,0 +1,47 @@
+package com.alfresco.content.process.ui.components
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+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.OptionsModel
+import com.alfresco.content.process.ui.fragments.FormViewModel
+import com.alfresco.content.process.ui.fragments.ProcessFragment
+import com.alfresco.content.process.ui.utils.getContentList
+
+@Composable
+fun Outcomes(outcomes: List, viewModel: FormViewModel, fragment: ProcessFragment) {
+ val state by viewModel.collectAsState()
+ outcomes.forEach {
+ Button(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 12.dp, vertical = 4.dp),
+ onClick = {
+ val contentList = getContentList(state)
+
+ if (contentList.isNotEmpty()) {
+ viewModel.optionsModel = it
+ fragment.confirmContentQueuePrompt()
+ } else {
+ viewModel.performOutcomes(it)
+ }
+ },
+ shape = RoundedCornerShape(6.dp),
+ enabled = state.enabledOutcomes,
+ colors = ButtonDefaults.buttonColors(
+ contentColor = Color.White,
+ ),
+ ) {
+ Text(it.name)
+ }
+ }
+}
diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/PeopleField.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/PeopleField.kt
new file mode 100644
index 000000000..55c474d40
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/PeopleField.kt
@@ -0,0 +1,92 @@
+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.material3.Icon
+import androidx.compose.material3.IconButton
+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.painterResource
+import androidx.compose.ui.text.SpanStyle
+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 com.alfresco.content.component.searchusergroup.SearchUserGroupComponentBuilder
+import com.alfresco.content.data.ProcessEntry
+import com.alfresco.content.data.UserGroupDetails
+import com.alfresco.content.data.payloads.FieldsData
+import com.alfresco.content.process.R
+import com.alfresco.content.process.ui.theme.AlfrescoBlue300
+import com.alfresco.content.process.ui.theme.AlfrescoError
+
+@Composable
+fun PeopleField(
+ userDetail: UserGroupDetails? = null,
+ onAssigneeSelected: (UserGroupDetails?) -> Unit = {},
+ fieldsData: FieldsData = FieldsData(),
+ processEntry: ProcessEntry = ProcessEntry(),
+ onValueChanged: (UserGroupDetails?) -> Unit = { },
+ errorData: Pair = Pair(false, ""),
+) {
+ val labelWithAsterisk = buildAnnotatedString {
+ append(fieldsData.name)
+ if (fieldsData.required) {
+ withStyle(style = SpanStyle(color = AlfrescoError)) {
+ append(" *") // Adding a red asterisk for mandatory fields
+ }
+ }
+ }
+
+ 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 = {
+ SearchUserGroupComponentBuilder(context, processEntry)
+ .onApply { userDetails ->
+ onAssigneeSelected(userDetails)
+ }
+ .onCancel {
+ onAssigneeSelected(null)
+ }
+ .show()
+ }) {
+ Icon(
+ painterResource(R.drawable.ic_edit_blue),
+ tint = AlfrescoBlue300,
+ contentDescription = "",
+ )
+ }
+ }
+ if (userDetail != null) {
+ InputChip(context, userDetail, onValueChanged)
+ }
+ }
+}
+
+@Preview
+@Composable
+fun PeopleFieldPreview() {
+ PeopleField()
+}
diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/RadioButtonField.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/RadioButtonField.kt
new file mode 100644
index 000000000..644a7bd9b
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/RadioButtonField.kt
@@ -0,0 +1,87 @@
+package com.alfresco.content.process.ui.components
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.RadioButton
+import androidx.compose.material3.Text
+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.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.unit.dp
+import com.alfresco.content.data.payloads.FieldsData
+import com.alfresco.content.process.R
+import com.alfresco.content.process.ui.theme.AlfrescoError
+
+@Composable
+fun RadioButtonField(
+ selectedOption: String = "",
+ selectedQuery: String = "",
+ onSelectedOption: (Pair) -> Unit = {},
+ fieldsData: FieldsData = FieldsData(),
+) {
+ val labelWithAsterisk = buildAnnotatedString {
+ append(fieldsData.name)
+ if (fieldsData.required) {
+ withStyle(style = SpanStyle(color = AlfrescoError)) {
+ append(" *") // Adding a red asterisk for mandatory fields
+ }
+ }
+ }
+
+ var showError by remember { mutableStateOf(false) }
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = 24.dp),
+ horizontalAlignment = Alignment.Start,
+ ) {
+ Column(
+ horizontalAlignment = Alignment.Start,
+ modifier = Modifier
+ .fillMaxWidth(),
+ ) {
+ Text(
+ text = labelWithAsterisk,
+ modifier = Modifier.padding(end = 4.dp),
+ )
+ fieldsData.options.forEach { option ->
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ RadioButton(
+ selected = (option.id == selectedQuery),
+ onClick = {
+ onSelectedOption(Pair(option.name, option.id))
+ },
+ )
+ Text(
+ text = option.name,
+ style = MaterialTheme.typography.bodyMedium,
+ )
+ }
+ }
+ }
+
+ if (showError) {
+ Text(
+ text = stringResource(R.string.error_required_field),
+ color = AlfrescoError,
+ modifier = Modifier
+ .padding(start = 16.dp, top = 0.dp), // Adjust padding as needed
+ style = MaterialTheme.typography.titleSmall,
+ textAlign = TextAlign.Start,
+ )
+ }
+ }
+}
diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/ReadOnlyField.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/ReadOnlyField.kt
new file mode 100644
index 000000000..860dcbcdd
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/ReadOnlyField.kt
@@ -0,0 +1,116 @@
+package com.alfresco.content.process.ui.components
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.tooling.preview.Preview
+import com.alfresco.content.DATE_FORMAT_2_1
+import com.alfresco.content.DATE_FORMAT_3
+import com.alfresco.content.DATE_FORMAT_8
+import com.alfresco.content.data.payloads.FieldType
+import com.alfresco.content.data.payloads.FieldsData
+import com.alfresco.content.getLocalFormattedDate
+import com.alfresco.content.getLocalFormattedDate1
+import com.alfresco.content.getLocalizedName
+import com.alfresco.content.process.R
+import com.alfresco.content.process.ui.fragments.FormViewModel
+import com.alfresco.content.process.ui.utils.inputField
+import com.alfresco.content.updateDateFormat
+
+@Composable
+fun ReadOnlyField(
+ viewModel: FormViewModel? = null,
+ fieldsData: FieldsData = FieldsData(),
+ onUserTap: (Boolean) -> Unit = {},
+) {
+ val keyboardOptions = KeyboardOptions.Default.copy(
+ imeAction = ImeAction.Next,
+ keyboardType = KeyboardType.Text,
+ )
+
+ var dateFormat: String? = null
+
+ val textValue = when (fieldsData.params?.field?.type?.lowercase()) {
+ FieldType.PEOPLE.value(), FieldType.FUNCTIONAL_GROUP.value() -> {
+ LocalContext.current.getLocalizedName(fieldsData.getUserGroupDetails(viewModel?.getAPSUser())?.name ?: "")
+ }
+
+ FieldType.UPLOAD.value() -> {
+ val contents = fieldsData.getContentList()
+ if (contents.isEmpty()) {
+ stringResource(id = R.string.no_attachments)
+ } else {
+ stringResource(id = R.string.text_multiple_attachment, contents.size)
+ }
+ }
+
+ FieldType.DATE.value() -> {
+ val date = fieldsData.value as? String ?: ""
+ if (date.isNotEmpty()) {
+ dateFormat = updateDateFormat(fieldsData.params?.field?.dateDisplayFormat) ?: DATE_FORMAT_2_1
+ date.getLocalFormattedDate(DATE_FORMAT_3, dateFormat)
+ } else {
+ date
+ }
+ }
+
+ FieldType.DATETIME.value() -> {
+ val dateTime = fieldsData.value as? String ?: ""
+ if (dateTime.isNotEmpty()) {
+ dateFormat = updateDateFormat(fieldsData.params?.field?.dateDisplayFormat) ?: DATE_FORMAT_8
+ dateTime.getLocalFormattedDate1(DATE_FORMAT_3, dateFormat)
+ } else {
+ dateTime
+ }
+ }
+
+ else -> {
+ when (fieldsData.value) {
+ is Double -> {
+ (fieldsData.value as? Double ?: 0).toInt().toString()
+ }
+
+ is Int -> {
+ (fieldsData.value as? Int ?: 0).toString()
+ }
+
+ else -> {
+ fieldsData.value as? String ?: ""
+ }
+ }
+ }
+ }
+
+ InputField(
+ colors = OutlinedTextFieldDefaults.colors(
+ disabledBorderColor = MaterialTheme.colorScheme.onSurfaceVariant,
+ disabledTextColor = MaterialTheme.colorScheme.onPrimary,
+ disabledPlaceholderColor = MaterialTheme.colorScheme.onPrimary,
+ disabledLabelColor = MaterialTheme.colorScheme.onPrimary,
+ ),
+ modifier = Modifier
+ .inputField()
+ .clickable {
+ onUserTap(true)
+ },
+ maxLines = 1,
+ textFieldValue = textValue,
+ fieldsData = fieldsData,
+ keyboardOptions = keyboardOptions,
+ isEnabled = false,
+ dateFormat = dateFormat,
+ )
+}
+
+@Preview
+@Composable
+fun ReadOnlyFieldPreview() {
+ ReadOnlyField()
+}
diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/SearchBar.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/SearchBar.kt
new file mode 100644
index 000000000..109be72ec
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/SearchBar.kt
@@ -0,0 +1,104 @@
+package com.alfresco.content.process.ui.components
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+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.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.unit.dp
+
+@OptIn(ExperimentalMaterial3Api::class)
+@ExperimentalAnimationApi
+@ExperimentalComposeUiApi
+@Composable
+fun SearchBar(
+ searchText: String,
+ placeholderText: String = "",
+ onSearchTextChanged: (String) -> Unit = {},
+ onClearClick: () -> Unit = {},
+ onNavigateBack: () -> Unit = {},
+) {
+ var showClearButton by remember { mutableStateOf(false) }
+ val keyboardController = LocalSoftwareKeyboardController.current
+ val focusRequester = remember { FocusRequester() }
+
+ TopAppBar(title = { Text("") }, navigationIcon = {
+ IconButton(onClick = { onNavigateBack() }) {
+ Icon(
+ imageVector = Icons.Filled.ArrowBack,
+ modifier = Modifier,
+ contentDescription = "",
+ )
+ }
+ }, actions = {
+ OutlinedTextField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 2.dp)
+ .onFocusChanged { focusState ->
+ showClearButton = (focusState.isFocused)
+ }
+ .focusRequester(focusRequester),
+ value = searchText,
+ onValueChange = onSearchTextChanged,
+ placeholder = {
+ Text(text = placeholderText)
+ },
+ colors = TextFieldDefaults.textFieldColors(
+ focusedIndicatorColor = Color.Transparent,
+ unfocusedIndicatorColor = Color.Transparent,
+ ),
+ trailingIcon = {
+ AnimatedVisibility(
+ visible = showClearButton,
+ enter = fadeIn(),
+ exit = fadeOut(),
+ ) {
+ IconButton(onClick = { onClearClick() }) {
+ Icon(
+ imageVector = Icons.Filled.Close,
+ contentDescription = "",
+ )
+ }
+ }
+ },
+ maxLines = 1,
+ singleLine = true,
+ keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done),
+ keyboardActions = KeyboardActions(onDone = {
+ keyboardController?.hide()
+ }),
+ )
+ })
+
+ LaunchedEffect(Unit) {
+ focusRequester.requestFocus()
+ }
+}
diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/SingleLineInputField.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/SingleLineInputField.kt
new file mode 100644
index 000000000..84e7dc37a
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/SingleLineInputField.kt
@@ -0,0 +1,51 @@
+package com.alfresco.content.process.ui.components
+
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.tooling.preview.Preview
+import com.alfresco.content.data.payloads.FieldsData
+import com.alfresco.content.process.ui.utils.inputField
+
+@Composable
+fun SingleLineInputField(
+ textFieldValue: String? = null,
+ onValueChanged: (String) -> Unit = { },
+ fieldsData: FieldsData = FieldsData(),
+ errorData: Pair = Pair(false, ""),
+) {
+ val keyboardOptions = KeyboardOptions.Default.copy(
+ imeAction = ImeAction.Next,
+ keyboardType = KeyboardType.Text,
+ )
+
+ InputField(
+ colors = OutlinedTextFieldDefaults.colors(
+ unfocusedBorderColor = MaterialTheme.colorScheme.onSurfaceVariant,
+ unfocusedLabelColor = MaterialTheme.colorScheme.onPrimary,
+ unfocusedPlaceholderColor = MaterialTheme.colorScheme.onPrimary,
+ unfocusedTextColor = MaterialTheme.colorScheme.onPrimary,
+ ),
+ modifier = Modifier
+ .inputField()
+ .semantics(mergeDescendants = true) {},
+ maxLines = 1,
+ textFieldValue = textFieldValue,
+ onValueChanged = onValueChanged,
+ fieldsData = fieldsData,
+ keyboardOptions = keyboardOptions,
+ isError = errorData.first,
+ errorMessage = errorData.second,
+ )
+}
+
+@Preview
+@Composable
+fun SingleLineInputFieldPreview() {
+ SingleLineInputField()
+}
diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/TextLayoutHandler.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/TextLayoutHandler.kt
new file mode 100644
index 000000000..bef2f1f43
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/TextLayoutHandler.kt
@@ -0,0 +1,20 @@
+package com.alfresco.content.process.ui.components
+
+import androidx.compose.ui.text.TextLayoutResult
+
+class TextLayoutHandler(
+ private val minimumLineLength: Int,
+ private val onEllipsisChanged: (Boolean) -> Unit,
+) {
+ fun handleTextLayout(textLayoutResult: TextLayoutResult) {
+ if (textLayoutResult.lineCount > minimumLineLength - 1) {
+ if (textLayoutResult.isLineEllipsized(minimumLineLength - 1)) {
+ onEllipsisChanged(true)
+ } else {
+ onEllipsisChanged(false)
+ }
+ } else {
+ onEllipsisChanged(false)
+ }
+ }
+}
diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/TrailingInputField.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/TrailingInputField.kt
new file mode 100644
index 000000000..ee378d436
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/components/TrailingInputField.kt
@@ -0,0 +1,68 @@
+package com.alfresco.content.process.ui.components
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.CalendarToday
+import androidx.compose.material.icons.filled.Cancel
+import androidx.compose.material.icons.filled.Error
+import androidx.compose.material.icons.filled.KeyboardArrowDown
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import com.alfresco.content.data.payloads.FieldType
+import com.alfresco.content.data.payloads.FieldsData
+import com.alfresco.content.process.R
+import com.alfresco.content.process.ui.utils.trailingIconColor
+
+@Composable
+fun TrailingInputField(
+ focusState: Boolean = false,
+ textValue: String? = null,
+ errorMessage: String = "",
+ isError: Boolean = false,
+ fieldsData: FieldsData = FieldsData(),
+ onValueChanged: (String) -> Unit = { },
+) {
+ when (fieldsData.type) {
+ FieldType.DATETIME.value(), FieldType.DATE.value() -> {
+ Icon(
+ imageVector = Icons.Default.CalendarToday,
+ contentDescription = null,
+ tint = trailingIconColor(),
+ )
+ }
+
+ FieldType.DROPDOWN.value(), FieldType.RADIO_BUTTONS.value() -> {
+ Icon(
+ imageVector = Icons.Default.KeyboardArrowDown,
+ contentDescription = null,
+ tint = trailingIconColor(),
+ )
+ }
+
+ else -> {
+ if (focusState && !textValue.isNullOrEmpty()) {
+ if (isError) {
+ Icon(
+ imageVector = Icons.Default.Error,
+ contentDescription = errorMessage,
+ tint = MaterialTheme.colorScheme.error,
+ )
+ } else {
+ IconButton(
+ onClick = {
+ onValueChanged("")
+ },
+ ) {
+ Icon(
+ imageVector = Icons.Default.Cancel,
+ contentDescription = stringResource(R.string.accessibility_clear_text),
+ tint = trailingIconColor(),
+ )
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/composeviews/FormDetailScreen.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/composeviews/FormDetailScreen.kt
new file mode 100644
index 000000000..2a2733e72
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/composeviews/FormDetailScreen.kt
@@ -0,0 +1,108 @@
+package com.alfresco.content.process.ui.composeviews
+
+import android.annotation.SuppressLint
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.input.key.KeyEventType
+import androidx.compose.ui.input.key.key
+import androidx.compose.ui.input.key.onKeyEvent
+import androidx.compose.ui.input.key.type
+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.airbnb.mvrx.compose.collectAsState
+import com.alfresco.content.data.OptionsModel
+import com.alfresco.content.data.TaskRepository
+import com.alfresco.content.process.ui.components.Outcomes
+import com.alfresco.content.process.ui.fragments.FormViewModel
+import com.alfresco.content.process.ui.fragments.FormViewState
+import com.alfresco.content.process.ui.fragments.ProcessFragment
+
+@SuppressLint("MutableCollectionMutableState")
+@Composable
+fun FormDetailScreen(viewModel: FormViewModel, outcomes: List, navController: NavController, fragment: ProcessFragment, snackbarHostState: SnackbarHostState) {
+ val keyboardController = LocalSoftwareKeyboardController.current
+ val state by viewModel.collectAsState()
+ val focusManager = LocalFocusManager.current
+
+ val interactionSource = remember { MutableInteractionSource() }
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable(
+ interactionSource = interactionSource,
+ indication = null,
+ ) {
+ focusManager.clearFocus()
+ keyboardController?.hide()
+ }
+ .onKeyEvent { event ->
+ if (event.type == KeyEventType.KeyUp && event.key == Key.Enter) {
+ true
+ } else {
+ false
+ }
+ },
+ ) {
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .weight(1f, false),
+ verticalArrangement = Arrangement.Top,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ items(
+ key = {
+ it.id
+ },
+ items = state.formFields,
+ ) { field ->
+ FormScrollContent(field, viewModel, state, navController, snackbarHostState)
+ }
+ }
+
+ if (outcomes.isNotEmpty()) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .align(alignment = Alignment.CenterHorizontally),
+ ) {
+ Outcomes(outcomes = outcomes, viewModel, fragment)
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+fun PreviewProcessDetailScreen() {
+ val state = FormViewState()
+ FormDetailScreen(
+ FormViewModel(
+ state,
+ LocalContext.current,
+ TaskRepository(),
+ ),
+ emptyList(),
+ rememberNavController(),
+ ProcessFragment(),
+ SnackbarHostState(),
+ )
+}
diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/composeviews/FormScreen.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/composeviews/FormScreen.kt
new file mode 100644
index 000000000..e7276e29b
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/composeviews/FormScreen.kt
@@ -0,0 +1,151 @@
+package com.alfresco.content.process.ui.composeviews
+
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.material3.FabPosition
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.res.stringResource
+import androidx.navigation.NavController
+import com.airbnb.mvrx.compose.collectAsState
+import com.alfresco.content.data.DefaultOutcomesID
+import com.alfresco.content.data.OptionsModel
+import com.alfresco.content.process.R
+import com.alfresco.content.process.ui.components.FloatingActionButton
+import com.alfresco.content.process.ui.fragments.FormViewModel
+import com.alfresco.content.process.ui.fragments.FormViewState
+import com.alfresco.content.process.ui.fragments.ProcessFragment
+
+@Composable
+fun FormScreen(navController: NavController, viewModel: FormViewModel, fragment: ProcessFragment) {
+ val snackbarHostState = remember { SnackbarHostState() }
+
+ val state by viewModel.collectAsState()
+
+ val customOutcomes = when {
+ state.formFields.isEmpty() -> emptyList()
+ state.processOutcomes.isEmpty() -> defaultOutcomes(state)
+ state.parent.taskEntry.memberOfCandidateGroup == true -> pooledOutcomes(state, viewModel)
+ else -> customOutcomes(state)
+ }
+
+ when {
+ customOutcomes.size < 3 -> {
+ Scaffold(
+ snackbarHost = { SnackbarHost(snackbarHostState) },
+ ) { padding ->
+ val colorScheme = MaterialTheme.colorScheme
+ Surface(
+ modifier = androidx.compose.ui.Modifier
+ .padding(padding)
+ .statusBarsPadding(),
+ color = colorScheme.background,
+ contentColor = colorScheme.onBackground,
+ ) {
+ FormDetailScreen(viewModel, customOutcomes, navController, fragment, snackbarHostState)
+ }
+ }
+ }
+
+ else -> {
+ Scaffold(
+ snackbarHost = { SnackbarHost(snackbarHostState) },
+ floatingActionButton = { FloatingActionButton(customOutcomes, fragment, viewModel) },
+ floatingActionButtonPosition = FabPosition.End,
+ ) { padding ->
+ val colorScheme = MaterialTheme.colorScheme
+ Surface(
+ modifier = androidx.compose.ui.Modifier
+ .padding(padding)
+ .statusBarsPadding(),
+ color = colorScheme.background,
+ contentColor = colorScheme.onBackground,
+ ) {
+ FormDetailScreen(viewModel, emptyList(), navController, fragment, snackbarHostState)
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun defaultOutcomes(state: FormViewState): List {
+ if (state.parent.taskEntry.endDate != null) {
+ return emptyList()
+ }
+
+ return when (state.parent.processInstanceId) {
+ null -> {
+ listOf(
+ OptionsModel(
+ id = DefaultOutcomesID.DEFAULT_START_WORKFLOW.value(),
+ name = stringResource(id = R.string.action_start_workflow),
+ ),
+ )
+ }
+
+ else -> {
+ listOf(
+ OptionsModel(
+ id = DefaultOutcomesID.DEFAULT_SAVE.value(),
+ name = stringResource(id = R.string.action_text_save),
+ ),
+ OptionsModel(
+ id = DefaultOutcomesID.DEFAULT_COMPLETE.value(),
+ name = stringResource(id = R.string.text_complete),
+ ),
+ )
+ }
+ }
+}
+
+@Composable
+private fun customOutcomes(state: FormViewState): List {
+ return if (state.parent.processInstanceId == null) {
+ state.processOutcomes
+ } else {
+ listOf(
+ OptionsModel(
+ id = DefaultOutcomesID.DEFAULT_SAVE.value(),
+ name = stringResource(id = R.string.action_text_save),
+ ),
+ ) + state.processOutcomes
+ }
+}
+
+@Composable
+private fun pooledOutcomes(state: FormViewState, viewModel: FormViewModel): List {
+ val dataObj = state.parent.taskEntry
+
+ when {
+ dataObj.assignee?.id == null || dataObj.assignee?.id == 0 -> {
+ return listOf(
+ OptionsModel(
+ id = DefaultOutcomesID.DEFAULT_CLAIM.value(),
+ name = stringResource(id = R.string.action_menu_claim),
+ ),
+ )
+ }
+
+ viewModel.isAssigneeAndLoggedInSame(dataObj.assignee) -> {
+ return listOf(
+ OptionsModel(
+ id = DefaultOutcomesID.DEFAULT_RELEASE.value(),
+ name = stringResource(id = R.string.action_menu_release),
+ ),
+ OptionsModel(
+ id = DefaultOutcomesID.DEFAULT_SAVE.value(),
+ name = stringResource(id = R.string.action_text_save),
+ ),
+ ) + state.processOutcomes
+ }
+
+ else -> return emptyList()
+ }
+}
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
new file mode 100644
index 000000000..cf7e2af90
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/composeviews/FormScrollContent.kt
@@ -0,0 +1,272 @@
+package com.alfresco.content.process.ui.composeviews
+
+import android.content.Intent
+import android.os.Bundle
+import androidx.compose.material3.SnackbarHostState
+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.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.navigation.NavController
+import com.airbnb.mvrx.Mavericks
+import com.alfresco.content.data.ProcessEntry
+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
+import com.alfresco.content.process.ui.components.AttachFolderField
+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
+import com.alfresco.content.process.ui.utils.actionsReadOnlyField
+import com.alfresco.content.process.ui.utils.amountInputError
+import com.alfresco.content.process.ui.utils.booleanInputError
+import com.alfresco.content.process.ui.utils.dateTimeInputError
+import com.alfresco.content.process.ui.utils.dropDownRadioInputError
+import com.alfresco.content.process.ui.utils.folderInputError
+import com.alfresco.content.process.ui.utils.integerInputError
+import com.alfresco.content.process.ui.utils.multiLineInputError
+import com.alfresco.content.process.ui.utils.singleLineInputError
+import com.alfresco.content.process.ui.utils.userGroupInputError
+
+@Composable
+fun FormScrollContent(field: FieldsData, viewModel: FormViewModel, state: FormViewState, navController: NavController, snackbarHostState: SnackbarHostState) {
+ val context = LocalContext.current
+ when (field.type) {
+ FieldType.TEXT.value() -> {
+ var textFieldValue by remember { mutableStateOf(field.value as? String ?: "") }
+ var errorData by remember { mutableStateOf(Pair(false, "")) }
+
+ SingleLineInputField(
+ textFieldValue = textFieldValue,
+ onValueChanged = { newText ->
+ textFieldValue = newText
+ errorData = singleLineInputError(newText, field, context)
+ viewModel.updateFieldValue(field.id, newText, errorData)
+ },
+ errorData = errorData,
+ fieldsData = field,
+ )
+ }
+
+ FieldType.MULTI_LINE_TEXT.value() -> {
+ var textFieldValue by remember { mutableStateOf(field.value as? String ?: "") }
+ var errorData by remember { mutableStateOf(Pair(false, "")) }
+
+ MultiLineInputField(
+ textFieldValue = textFieldValue,
+ onValueChanged = { newText ->
+ textFieldValue = newText
+ errorData = multiLineInputError(newText, field, context)
+ viewModel.updateFieldValue(field.id, newText, errorData)
+ },
+ errorData = errorData,
+ fieldsData = field,
+ )
+ }
+
+ FieldType.INTEGER.value() -> {
+ var textFieldValue by remember { mutableStateOf(field.value as? String ?: "") }
+ var errorData by remember { mutableStateOf(field.errorData) }
+
+ IntegerInputField(
+ textFieldValue = textFieldValue,
+ onValueChanged = { newText ->
+ textFieldValue = newText
+ errorData = integerInputError(newText, field, context)
+ viewModel.updateFieldValue(field.id, newText, errorData)
+ },
+ errorData = errorData,
+ fieldsData = field,
+ )
+ }
+
+ FieldType.AMOUNT.value() -> {
+ var textFieldValue by remember { mutableStateOf(field.value as? String ?: "") }
+ var errorData by remember { mutableStateOf(Pair(false, "")) }
+
+ AmountInputField(
+ textFieldValue = textFieldValue,
+ onValueChanged = { newText ->
+ textFieldValue = newText
+ errorData = amountInputError(textFieldValue, field, context)
+ viewModel.updateFieldValue(field.id, newText, errorData)
+ },
+ errorData = errorData,
+ fieldsData = field,
+ )
+ }
+
+ FieldType.BOOLEAN.value() -> {
+ var checkedValue by remember { mutableStateOf(field.value as? Boolean ?: false) }
+ var errorData by remember { mutableStateOf(Pair(false, "")) }
+
+ CheckBoxField(
+ title = stringResource(id = R.string.title_workflow),
+ checkedValue = checkedValue,
+ onCheckChanged = { newChecked ->
+ checkedValue = newChecked
+ errorData = booleanInputError(newChecked, field, context)
+ viewModel.updateFieldValue(field.id, newChecked, errorData)
+ },
+ errorData = errorData,
+ fieldsData = field,
+ )
+ }
+
+ FieldType.DATE.value() -> {
+ var textFieldValue by remember { mutableStateOf(field.value as? String ?: "") }
+ var errorData by remember { mutableStateOf(Pair(false, "")) }
+
+ DateTimeField(
+ dateTimeValue = textFieldValue,
+ onValueChanged = { newText ->
+ textFieldValue = newText
+ errorData = dateTimeInputError(newText, field, context)
+ viewModel.updateFieldValue(field.id, newText, errorData)
+ },
+ errorData = errorData,
+ fieldsData = field,
+ )
+ }
+
+ FieldType.DATETIME.value() -> {
+ var textFieldValue by remember { mutableStateOf(field.value as? String ?: "") }
+ var errorData by remember { mutableStateOf(Pair(false, "")) }
+
+ DateTimeField(
+ dateTimeValue = textFieldValue,
+ onValueChanged = { newText ->
+ textFieldValue = newText
+ errorData = dateTimeInputError(newText, field, context)
+ viewModel.updateFieldValue(field.id, newText, errorData)
+ },
+ errorData = errorData,
+ fieldsData = field,
+ )
+ }
+
+ FieldType.DROPDOWN.value(), FieldType.RADIO_BUTTONS.value() -> {
+ var textFieldValue by remember { mutableStateOf(field.value as? String ?: "") }
+ var textFieldQuery by remember { mutableStateOf(field.options.find { it.name == textFieldValue }?.id ?: "") }
+ var errorData by remember { mutableStateOf(Pair(false, "")) }
+
+ DropdownField(
+ nameText = textFieldValue,
+ queryText = textFieldQuery,
+ onValueChanged = { (newText, newQuery) ->
+ textFieldValue = newText
+ textFieldQuery = newQuery
+ errorData = dropDownRadioInputError(newText, field, context)
+ viewModel.updateFieldValue(field.id, newText, errorData)
+ },
+
+ errorData = errorData,
+ fieldsData = field,
+ )
+ }
+
+ FieldType.READONLY_TEXT.value(), FieldType.READONLY.value() -> {
+ ReadOnlyField(
+ viewModel = viewModel,
+ fieldsData = field,
+ onUserTap = {
+ actionsReadOnlyField(it, field, navController, state, context)
+ },
+ )
+ }
+
+ FieldType.PEOPLE.value(), FieldType.FUNCTIONAL_GROUP.value() -> {
+ var userDetailValue by remember { mutableStateOf(field.getUserGroupDetails(viewModel.getAPSUser())) }
+ var errorData by remember { mutableStateOf(Pair(false, "")) }
+
+ PeopleField(
+ userDetail = userDetailValue,
+ onAssigneeSelected = { userDetails ->
+ userDetailValue = userDetails
+ errorData = userGroupInputError(userDetails, field, context)
+ viewModel.updateFieldValue(field.id, userDetails, errorData)
+ },
+ fieldsData = field,
+ errorData = errorData,
+ processEntry = ProcessEntry.withProcess(state.parent, field.type),
+ onValueChanged = { userDetails ->
+ userDetailValue = userDetails
+ errorData = userGroupInputError(userDetails, field, context)
+ viewModel.updateFieldValue(field.id, userDetails, errorData)
+ },
+ )
+ }
+
+ FieldType.HYPERLINK.value() -> {
+ HyperLinkField(
+ field,
+ snackbarHostState,
+ )
+ }
+
+ FieldType.UPLOAD.value() -> {
+ val listContents = field.getContentList(state.parent.processDefinitionId)
+
+ AttachFilesField(
+ contents = listContents,
+ fieldsData = field,
+ 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,
+ )
+ }
+ },
+ )
+ }
+
+ FieldType.SELECT_FOLDER.value() -> {
+ AttachFolderField(
+ fieldsData = field,
+ navController = navController,
+ onUserTap = {
+ if (it) {
+ viewModel.selectedField = field
+ val intent = Intent(
+ context,
+ Class.forName("com.alfresco.content.app.activity.MoveActivity"),
+ )
+ context.startActivity(intent)
+ }
+ },
+ onResetFolder = {
+ if (it) {
+ val errorData = folderInputError(null, field, context)
+ viewModel.updateFieldValue(field.id, null, errorData)
+ }
+ },
+ )
+ }
+ }
+}
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..4e6e7f202
--- /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_attach_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..feb1c1fb5
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/epoxy/ListViewAttachmentRow.kt
@@ -0,0 +1,115 @@
+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)
+ private var isProcessInstance: Boolean = false
+
+ /**
+ * 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
+ }
+
+ @ModelProp
+ fun setProcessData(isProcessInstance: Boolean) {
+ this.isProcessInstance = isProcessInstance
+ }
+
+ 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 &&
+ // If process is created
+ !isProcessInstance
+
+ /**
+ * 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/FormViewModel.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/FormViewModel.kt
new file mode 100644
index 000000000..8c55ace68
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/FormViewModel.kt
@@ -0,0 +1,596 @@
+package com.alfresco.content.process.ui.fragments
+
+import android.content.Context
+import com.airbnb.mvrx.Async
+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.Uninitialized
+import com.airbnb.mvrx.ViewModelContext
+import com.alfresco.content.DATE_FORMAT_1
+import com.alfresco.content.DATE_FORMAT_2_1
+import com.alfresco.content.DATE_FORMAT_4_1
+import com.alfresco.content.DATE_FORMAT_5
+import com.alfresco.content.common.EntryListener
+import com.alfresco.content.data.APIEvent
+import com.alfresco.content.data.AnalyticsManager
+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.ResponseAccountInfo
+import com.alfresco.content.data.ResponseListForm
+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.data.payloads.LinkContentPayload
+import com.alfresco.content.data.payloads.convertModelToMapValues
+import com.alfresco.content.getFormattedDate
+import com.alfresco.coroutines.asFlow
+import com.alfresco.events.on
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+
+class FormViewModel(
+ val state: FormViewState,
+ val context: Context,
+ private val repository: TaskRepository,
+) : MavericksViewModel(state) {
+
+ private var observeUploadsJob: Job? = null
+ var selectedField: FieldsData? = null
+ private var entryListener: EntryListener? = null
+ var optionsModel: OptionsModel? = null
+ var onLinkContentToProcess: ((Pair) -> Unit)? = null
+ private var successLinkContent = false
+
+ init {
+
+ OfflineRepository().removeCompletedUploadsProcess()
+
+ if (state.parent.processInstanceId != null) {
+ getTaskDetails()
+ } else {
+ singleProcessDefinition(state.parent.id)
+ }
+
+ viewModelScope.on {
+ it.entry?.let { entry ->
+ entryListener?.onAttachFolder(entry)
+ }
+ }
+
+ viewModelScope.on {
+ it.field?.let { field ->
+ entryListener?.onAttachFiles(field)
+ }
+ }
+ }
+
+ fun observeUploads(state: FormViewState) {
+ val parentId = state.parent.id.ifEmpty { state.parent.processDefinitionId } ?: ""
+
+ val repo = OfflineRepository()
+ OfflineRepository().removeCompletedUploadsProcess(parentId)
+ observeUploadsJob?.cancel()
+ observeUploadsJob = repo.observeProcessUploads(parentId, UploadServerType.UPLOAD_TO_PROCESS)
+ .execute {
+ if (it is Success) {
+ withState { newState ->
+ val listFields = newState.formFields.filter { fieldsData -> fieldsData.type == FieldType.UPLOAD.value() }
+ listFields.forEach { field ->
+ val listContents = it().filter { content -> content.observerID == field.id }
+ val isError = field.required && listContents.isEmpty() && listContents.all { content -> !content.isUpload }
+ updateFieldValue(field.id, listContents, Pair(isError, ""))
+ }
+ }
+
+ this
+ } else {
+ this
+ }
+ }
+ }
+
+ /**
+ * returns the current logged in APS user profile data
+ */
+ fun getAPSUser() = repository.getAPSUser()
+
+ 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(requestForm = Loading())
+ is Fail -> {
+ it.error.printStackTrace()
+ copy(requestForm = Fail(it.error))
+ }
+
+ is Success -> {
+ val fields = it().fields.flatMap { listData -> listData.fields }
+
+ val updatedState = copy(
+ parent = processEntry,
+ formFields = fields,
+ processOutcomes = it().outcomes,
+ requestForm = Success(it()),
+ )
+
+ val hasAllRequiredData = hasFieldValidData(fields)
+ updateStateData(hasAllRequiredData, fields)
+
+ updatedState.copy(requestForm = Success(it()))
+ }
+
+ else -> {
+ this
+ }
+ }
+ }
+ }
+ }
+
+ fun fetchUserProfile() {
+ if (repository.isAcsAndApsSameUser()) {
+ return
+ }
+ viewModelScope.launch {
+ // Fetch APS user profile data
+ repository::getProcessUserProfile.execute {
+ when (it) {
+ is Loading -> copy(requestProfile = Loading())
+ is Fail -> copy(requestProfile = Fail(it.error))
+ is Success -> {
+ val response = it()
+ repository.saveProcessUserDetails(response)
+ copy(requestProfile = Success(response))
+ }
+
+ else -> {
+ this
+ }
+ }
+ }
+ }
+ }
+
+ fun fetchAccountInfo() = withState { state ->
+ viewModelScope.launch {
+ repository::getAccountInfo.execute {
+ when (it) {
+ is Loading -> copy(requestAccountInfo = Loading())
+ is Fail -> copy(requestAccountInfo = Fail(it.error))
+ is Success -> {
+ val response = it()
+ repository.saveSourceName(response.listAccounts.first())
+ updateAccountInfo(response).copy(requestAccountInfo = Success(response))
+ }
+
+ else -> {
+ this
+ }
+ }
+ }
+ }
+ }
+
+ private fun getTaskDetails() = withState { state ->
+ viewModelScope.launch {
+ // Fetch tasks detail data
+ repository::getTaskDetails.asFlow(
+ state.parent.taskEntry.id,
+ ).execute {
+ when (it) {
+ is Loading -> copy(request = Loading())
+ is Fail -> copy(request = Fail(it.error))
+ is Success -> {
+ val updateState = update(it())
+ getTaskForms(updateState.parent)
+ updateState.copy(request = Success(it()))
+ }
+
+ else -> {
+ this
+ }
+ }
+ }
+ }
+ }
+
+ internal fun isAssigneeAndLoggedInSame(assignee: UserGroupDetails?) = getAPSUser().id == assignee?.id
+
+ internal fun isStartedByAndLoggedInSame(initiatorId: String?) = getAPSUser().id.toString() == initiatorId
+
+ fun linkContentToProcess(state: FormViewState, entry: Entry, sourceName: String, field: FieldsData?) =
+ viewModelScope.launch {
+ repository::linkADWContentToProcess
+ .asFlow(LinkContentPayload.with(entry, sourceName))
+ .execute {
+ when (it) {
+ is Loading -> copy(requestContent = Loading())
+ is Fail -> copy(requestContent = Fail(it.error))
+ is Success -> {
+ if (!successLinkContent) {
+ successLinkContent = true
+ val updateEntry = Entry.with(
+ data = entry,
+ parentId = state.parent.id,
+ observerID = field?.id ?: "",
+ )
+
+ OfflineRepository().update(updateEntry)
+
+ onLinkContentToProcess?.invoke(Pair(updateEntry, field))
+ }
+
+ copy(requestContent = Success(it()))
+ }
+
+ else -> this
+ }
+ }
+ }
+
+ private fun getTaskForms(processEntry: ProcessEntry) = withState { state ->
+ viewModelScope.launch {
+ repository::getTaskForm.asFlow(processEntry.taskEntry.id).execute {
+ when (it) {
+ is Loading -> copy(requestForm = Loading())
+ is Fail -> {
+ it.error.printStackTrace()
+ copy(requestForm = Fail(it.error))
+ }
+
+ is Success -> {
+ val fields = it().fields.flatMap { listData -> listData.fields }
+
+ val updatedState = copy(
+ parent = processEntry,
+ formFields = fields,
+ processOutcomes = it().outcomes,
+ requestForm = Success(it()),
+ )
+
+ val hasAllRequiredData = hasFieldValidData(fields)
+ updateStateData(hasAllRequiredData, fields)
+
+ updatedState
+ }
+
+ else -> {
+ this
+ }
+ }
+ }
+ }
+ }
+
+ fun updateFieldValue(fieldId: String, newValue: Any?, errorData: Pair) = withState { state ->
+ val updatedFieldList: MutableList = mutableListOf()
+
+ state.formFields.forEach { 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
+ }
+
+ (updatedValue is List<*>) && updatedValue.isEmpty() -> {
+ updatedValue = null
+ }
+ }
+ updatedFieldList.add(FieldsData.withUpdateField(field, updatedValue, errorData))
+ } else {
+ updatedFieldList.add(field)
+ }
+ }
+
+ val hasAllRequiredData = hasFieldValidData(updatedFieldList)
+
+ updateStateData(hasAllRequiredData, updatedFieldList)
+ }
+
+ fun performOutcomes(optionsModel: OptionsModel) {
+ when (optionsModel.id) {
+ DefaultOutcomesID.DEFAULT_START_WORKFLOW.value() -> startWorkflow()
+ DefaultOutcomesID.DEFAULT_COMPLETE.value() -> completeTask()
+ DefaultOutcomesID.DEFAULT_SAVE.value() -> saveForm()
+ DefaultOutcomesID.DEFAULT_CLAIM.value() -> claimTask()
+ DefaultOutcomesID.DEFAULT_RELEASE.value() -> releaseTask()
+ else -> actionOutcome(optionsModel.outcome)
+ }
+ }
+
+ /**
+ * execute API to claim the task
+ */
+ private fun claimTask() = withState { state ->
+ requireNotNull(state.parent)
+ viewModelScope.launch {
+ repository::claimTask.asFlow(state.parent.taskEntry.id).execute {
+ when (it) {
+ is Loading -> copy(requestClaimRelease = Loading())
+ is Fail -> {
+ copy(requestClaimRelease = Fail(it.error))
+ }
+
+ is Success -> {
+ copy(requestClaimRelease = Success(it()))
+ }
+
+ else -> {
+ this
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * execute API to release the task
+ */
+ private fun releaseTask() = withState { state ->
+ requireNotNull(state.parent)
+ viewModelScope.launch {
+ repository::releaseTask.asFlow(state.parent.taskEntry.id).execute {
+ when (it) {
+ is Loading -> copy(requestClaimRelease = Loading())
+ is Fail -> {
+ copy(requestClaimRelease = Fail(it.error))
+ }
+
+ is Success -> {
+ copy(requestClaimRelease = Success(it()))
+ }
+
+ else -> {
+ this
+ }
+ }
+ }
+ }
+ }
+
+ private fun completeTask() = withState { state ->
+ viewModelScope.launch {
+ repository::actionCompleteOutcome.asFlow(state.parent.taskEntry.id, convertFieldsToValues(state.formFields)).execute {
+ when (it) {
+ is Loading -> copy(requestOutcomes = Loading())
+ is Fail -> {
+ copy(requestOutcomes = Fail(it.error))
+ }
+
+ is Success -> {
+ copy(requestOutcomes = Success(it()))
+ }
+
+ else -> {
+ this
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * execute the save-form api
+ */
+ private fun saveForm() = withState { state ->
+ requireNotNull(state.parent)
+ viewModelScope.launch {
+ repository::saveForm.asFlow(
+ state.parent.taskEntry.id,
+ convertFieldsToValues(
+ state.formFields
+ .filter { it.type !in listOf(FieldType.READONLY.value(), FieldType.READONLY_TEXT.value()) },
+ ),
+ ).execute {
+ when (it) {
+ is Loading -> copy(requestSaveForm = Loading())
+ is Fail -> {
+ copy(requestSaveForm = Fail(it.error))
+ }
+
+ is Success -> {
+ copy(requestSaveForm = Success(it()))
+ }
+
+ else -> {
+ this
+ }
+ }
+ }
+ }
+ }
+
+ private fun startWorkflow() = withState { state ->
+ viewModelScope.launch {
+ repository::startWorkflow.asFlow(state.parent, "", convertFieldsToValues(state.formFields)).execute {
+ when (it) {
+ is Loading -> copy(requestStartWorkflow = Loading())
+ is Fail -> {
+ AnalyticsManager().apiTracker(APIEvent.StartWorkflow, false)
+ copy(requestStartWorkflow = Fail(it.error))
+ }
+
+ is Success -> {
+ AnalyticsManager().apiTracker(APIEvent.StartWorkflow, true)
+ copy(requestStartWorkflow = Success(it()))
+ }
+
+ else -> this
+ }
+ }
+ }
+ }
+
+ /**
+ * execute the outcome api
+ */
+ private fun actionOutcome(outcome: String) = withState { state ->
+ requireNotNull(state.parent)
+ viewModelScope.launch {
+ repository::actionOutcomes.asFlow(
+ outcome,
+ state.parent.taskEntry,
+ convertFieldsToValues(
+ state.formFields
+ .filter { it.type !in listOf(FieldType.READONLY.value(), FieldType.READONLY_TEXT.value()) },
+ ),
+ ).execute {
+ when (it) {
+ is Loading -> copy(requestOutcomes = Loading())
+ is Fail -> {
+ AnalyticsManager().apiTracker(APIEvent.Outcomes, false, outcome = outcome)
+ copy(requestOutcomes = Fail(it.error))
+ }
+
+ is Success -> {
+ AnalyticsManager().apiTracker(APIEvent.Outcomes, true, outcome = outcome)
+ copy(requestOutcomes = Success(it()))
+ }
+
+ else -> {
+ this
+ }
+ }
+ }
+ }
+ }
+
+ private fun convertFieldsToValues(fields: List): Map {
+ val values = mutableMapOf()
+
+ fields.forEach { field ->
+ when (field.type) {
+ FieldType.PEOPLE.value(), FieldType.FUNCTIONAL_GROUP.value() -> {
+ when {
+ field.value != null -> {
+ values[field.id] = convertModelToMapValues(field.getUserGroupDetails(getAPSUser()))
+ }
+
+ else -> {
+ values[field.id] = null
+ }
+ }
+ }
+
+ FieldType.DATETIME.value() -> {
+ val convertedDate = (field.value as? String)?.getFormattedDate(DATE_FORMAT_4_1, DATE_FORMAT_5)
+ values[field.id] = convertedDate
+ }
+
+ FieldType.DATE.value() -> {
+ val date = field.getDate(DATE_FORMAT_1, DATE_FORMAT_2_1)
+ val convertedDate = date.first.takeIf { it.isNotEmpty() }?.getFormattedDate(date.second, DATE_FORMAT_5) ?: ""
+ values[field.id] = convertedDate
+ }
+
+ FieldType.RADIO_BUTTONS.value(), FieldType.DROPDOWN.value() -> {
+ values[field.id] = convertModelToMapValues(field)
+ }
+
+ FieldType.UPLOAD.value() -> {
+ val listContents = (field.value as? List<*>)?.mapNotNull { it as? Entry } ?: emptyList()
+ values[field.id] = listContents.joinToString(separator = ",") { content -> content.id }
+ }
+
+ FieldType.SELECT_FOLDER.value() -> {
+ val selectedFolder = (field.value as? Entry)?.id ?: ""
+ values[field.id] = selectedFolder
+ }
+
+ else -> {
+ values[field.id] = field.value
+ }
+ }
+ }
+
+ return values
+ }
+
+ private fun updateStateData(enabledOutcomes: Boolean, fields: List) {
+ setState { copy(enabledOutcomes = enabledOutcomes, formFields = fields) }
+ }
+
+ private fun hasFieldValidData(fields: List): Boolean {
+ val hasValidDataInRequiredFields = !fields.filter { it.required }.any { (it.value == null || it.errorData.first) }
+ val hasValidDataInDropDownRequiredFields = fields.filter { it.required && it.options.isNotEmpty() }.let { list ->
+ list.isEmpty() || list.any { field -> field.options.any { option -> option.name == field.value && option.id != "empty" } }
+ }
+ val hasValidDataInOtherFields = !fields.filter { !it.required }.any { it.errorData.first }
+ return (hasValidDataInRequiredFields && hasValidDataInOtherFields && hasValidDataInDropDownRequiredFields)
+ }
+
+ fun setListener(listener: EntryListener) {
+ entryListener = listener
+ }
+
+ fun getContents(state: FormViewState, fieldId: String) = OfflineRepository().fetchProcessEntries(parentId = state.parent.id.ifEmpty { state.parent.processDefinitionId } ?: "", observerId = fieldId)
+
+ fun resetRequestState(request: Async<*>) {
+ when (request.invoke()) {
+ is ResponseListForm -> {
+ setState { copy(requestForm = Uninitialized) }
+ }
+
+ is ResponseAccountInfo -> {
+ setState { copy(requestAccountInfo = Uninitialized) }
+ }
+
+ is Entry -> {
+ setState { copy(requestContent = Uninitialized) }
+ }
+ }
+ }
+
+ 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..f05b493c9
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/FormViewState.kt
@@ -0,0 +1,61 @@
+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.AccountInfoData
+import com.alfresco.content.data.Entry
+import com.alfresco.content.data.OptionsModel
+import com.alfresco.content.data.ProcessEntry
+import com.alfresco.content.data.ResponseAccountInfo
+import com.alfresco.content.data.ResponseFormVariables
+import com.alfresco.content.data.ResponseListForm
+import com.alfresco.content.data.ResponseListProcessDefinition
+import com.alfresco.content.data.TaskEntry
+import com.alfresco.content.data.payloads.FieldsData
+import com.alfresco.process.models.ProfileData
+import retrofit2.Response
+
+data class FormViewState(
+ val parent: ProcessEntry = ProcessEntry(),
+ val formFields: List = emptyList(),
+ val processOutcomes: List = emptyList(),
+ val enabledOutcomes: Boolean = false,
+ val listAccountInfo: List = emptyList(),
+ val requestForm: Async = Uninitialized,
+ val requestProcessDefinition: Async = Uninitialized,
+ val requestStartWorkflow: Async = Uninitialized,
+ val requestOutcomes: Async> = Uninitialized,
+ val requestSaveForm: Async> = Uninitialized,
+ val requestFormVariables: Async = Uninitialized,
+ val requestContent: Async = Uninitialized,
+ val requestAccountInfo: Async = Uninitialized,
+ val requestProfile: Async = Uninitialized,
+ val requestClaimRelease: Async> = Uninitialized,
+ val request: Async = Uninitialized,
+) : MavericksState {
+ constructor(target: ProcessEntry) : this(parent = target)
+
+ /**
+ * update the taskDetailObj params after getting the response from server.
+ */
+ fun update(response: TaskEntry?): FormViewState {
+ if (response == null) return this
+
+ val processEntry = ProcessEntry.with(response)
+
+ return copy(parent = processEntry)
+ }
+
+ /**
+ * update the single process definition entry
+ */
+ fun updateSingleProcessDefinition(response: ResponseListProcessDefinition): FormViewState {
+ val processEntry = ProcessEntry.with(response.listProcessDefinitions.first(), parent)
+ return copy(parent = processEntry)
+ }
+
+ fun updateAccountInfo(it: ResponseAccountInfo): FormViewState {
+ return copy(listAccountInfo = it.listAccounts)
+ }
+}
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
new file mode 100644
index 000000000..6c5852967
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/ProcessAttachFilesFragment.kt
@@ -0,0 +1,176 @@
+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.fragmentViewModel
+import com.airbnb.mvrx.withState
+import com.alfresco.content.GetMultipleContents
+import com.alfresco.content.actions.ActionOpenWith
+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
+import com.alfresco.content.data.UploadServerType
+import com.alfresco.content.data.payloads.FieldType
+import com.alfresco.content.listview.listViewMessage
+import com.alfresco.content.mimetype.MimeType
+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: ProcessAttachFilesViewModel by fragmentViewModel()
+ private lateinit var binding: FragmentAttachFilesBinding
+ private val epoxyController: AsyncEpoxyController by lazy { epoxyController() }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View {
+ binding = FragmentAttachFilesBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ AnalyticsManager().screenViewEvent(PageView.AttachedFiles)
+
+ viewModel.setListener(this)
+
+ binding.refreshLayout.isEnabled = false
+
+ 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(entry: Entry) {
+ viewModel.deleteAttachment(entry)
+ }
+
+ override fun invalidate() = withState(viewModel) { state ->
+ val handler = Handler(Looper.getMainLooper())
+ binding.refreshLayout.isRefreshing = false
+ binding.loading.isVisible = false
+
+ handler.post {
+ if (isAdded) {
+ if (state.listContents.isNotEmpty()) {
+ binding.tvNoOfAttachments.visibility = View.VISIBLE
+ val filesHeader = StringBuilder()
+ filesHeader.append(getString(R.string.text_multiple_attachment, state.listContents.size)).apply {
+ if (!state.isReadOnlyField) {
+ this.append("\n")
+ .append(getString(R.string.process_max_file_size, GetMultipleContents.MAX_FILE_SIZE_10))
+ }
+ }
+
+ binding.tvNoOfAttachments.text = filesHeader
+ } else {
+ binding.tvNoOfAttachments.visibility = View.GONE
+ }
+ }
+ }
+
+ binding.fabAddAttachments.visibility = if (hasContentAddButton(state)) View.VISIBLE else View.GONE
+
+ binding.fabAddAttachments.setOnClickListener {
+ showCreateSheet(state, viewModel.parentId)
+ }
+ epoxyController.requestModelBuild()
+ }
+
+ private fun hasContentAddButton(state: ProcessAttachFilesViewState): Boolean {
+ val field = state.parent.field
+ if (field.type == FieldType.READONLY.value() || field.type == FieldType.READONLY_TEXT.value()) {
+ return false
+ }
+ return !(field.params?.multiple == false && state.listContents.isNotEmpty())
+ }
+
+ private fun epoxyController() = simpleController(viewModel) { state ->
+
+ if (state.listContents.isEmpty()) {
+ val args = viewModel.emptyMessageArgs(state)
+ listViewMessage {
+ id("empty_message")
+ iconRes(args.first)
+ title(args.second)
+ message(args.third)
+ }
+ } else {
+ state.listContents.forEach { obj ->
+ listViewAttachmentRow {
+ id(stableId(obj))
+ data(obj)
+ processData(state.isProcessInstance && state.isReadOnlyField)
+ clickListener { model, _, _, _ ->
+ if (state.isProcessInstance) {
+ onItemClicked(model.data())
+ }
+ }
+ deleteContentClickListener { model, _, _, _ -> onConfirmDelete(model.data()) }
+ }
+ }
+ }
+ }
+
+ private fun onItemClicked(contentEntry: Entry) {
+ if (!contentEntry.isUpload) {
+ val entry = Entry.convertContentEntryToEntry(
+ contentEntry,
+ MimeType.isDocFile(contentEntry.mimeType),
+ UploadServerType.UPLOAD_TO_TASK,
+ )
+ if (!contentEntry.source.isNullOrEmpty()) {
+ remoteViewerIntent(entry)
+ } else
+ viewModel.executePreview(ActionOpenWith(entry))
+ } else {
+ localViewerIntent(contentEntry)
+ }
+ }
+
+ override fun onEntryCreated(entry: ParentEntry) {
+ if (isAdded) {
+ 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..bc93eebc7
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/ProcessAttachFilesViewModel.kt
@@ -0,0 +1,114 @@
+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.GetMultipleContents
+import com.alfresco.content.actions.Action
+import com.alfresco.content.actions.ActionOpenWith
+import com.alfresco.content.common.EntryListener
+import com.alfresco.content.data.Entry
+import com.alfresco.content.data.OfflineRepository
+import com.alfresco.content.data.TaskRepository
+import com.alfresco.content.data.UploadServerType
+import com.alfresco.content.data.payloads.FieldType
+import com.alfresco.content.process.R
+import com.alfresco.events.on
+import kotlinx.coroutines.Job
+import java.io.File
+
+class ProcessAttachFilesViewModel(
+ val state: ProcessAttachFilesViewState,
+ val context: Context,
+ private val repository: TaskRepository,
+) : MavericksViewModel(state) {
+
+ private var observeUploadsJob: Job? = null
+ var parentId: String = ""
+ private var entryListener: EntryListener? = null
+
+ init {
+
+ viewModelScope.on {
+ if (!it.entry.path.isNullOrEmpty()) {
+ entryListener?.onEntryCreated(it.entry)
+ }
+ }
+
+ val field = state.parent.field
+
+ when (field.type) {
+ FieldType.READONLY.value(), FieldType.READONLY_TEXT.value() -> {
+ state.parent.process
+ setState { copy(listContents = field.getContentList(state.parent.process.processDefinitionId), baseEntries = field.getContentList(state.parent.process.processDefinitionId)) }
+ }
+
+ else -> {
+// setState { copy(listContents = field.getContentList(state.parent.process.processDefinitionId), baseEntries = field.getContentList(state.parent.process.processDefinitionId)) }
+ observeUploads(state)
+ }
+ }
+ }
+
+ /**
+ * returns the current logged in APS user profile data
+ */
+ fun getAPSUser() = repository.getAPSUser()
+
+ /**
+ * delete content locally
+ */
+ fun deleteAttachment(entry: Entry) = stateFlow.execute {
+ OfflineRepository().remove(entry)
+ deleteUploads(entry.id)
+ }
+
+ private fun observeUploads(state: ProcessAttachFilesViewState) {
+ val process = state.parent.process
+
+ parentId = process.id.ifEmpty { process.processDefinitionId } ?: ""
+
+ val repo = OfflineRepository()
+
+ observeUploadsJob?.cancel()
+ observeUploadsJob = repo.observeProcessUploads(parentId, UploadServerType.UPLOAD_TO_PROCESS)
+ .execute {
+ if (it is Success) {
+ updateUploads(state.parent.field.id, it())
+ } else {
+ this
+ }
+ }
+ }
+
+ fun emptyMessageArgs(state: ProcessAttachFilesViewState) =
+ when {
+ else ->
+ Triple(R.drawable.ic_empty_files, R.string.no_attached_files, context.getString(R.string.file_empty_message, GetMultipleContents.MAX_FILE_SIZE_10))
+ }
+
+ /**
+ * execute "open with" action to download the content data
+ */
+ fun executePreview(action: Action) {
+ val entry = action.entry as Entry
+ val file = File(repository.session.contentDir, entry.fileName)
+ if (!entry.isDocFile && repository.session.isFileExists(file) && file.length() != 0L) {
+ entryListener?.onEntryCreated(Entry.updateDownloadEntry(entry, file.path))
+ } else action.execute(context, kotlinx.coroutines.GlobalScope)
+ }
+
+ fun setListener(listener: EntryListener) {
+ this.entryListener = listener
+ }
+
+ 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..ead51ec4d
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/ProcessAttachFilesViewState.kt
@@ -0,0 +1,82 @@
+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.FieldType
+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)
+
+ val isProcessInstance: Boolean
+ get() = when (parent.process.processInstanceId) {
+ null -> false
+ else -> true
+ }
+
+ val isReadOnlyField: Boolean
+ get() = when (parent.field.type) {
+ FieldType.READONLY.value(), FieldType.READONLY_TEXT.value() -> true
+ else -> false
+ }
+
+ /**
+ * delete content locally and update UI
+ */
+ fun deleteUploads(contentId: String): ProcessAttachFilesViewState {
+ val listBaseEntries = baseEntries.filter { it.observerID == parent.field.id }.filter { it.id != contentId }
+ val listUploads = uploads.filter { it.observerID == parent.field.id }.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
new file mode 100644
index 000000000..5302d0d2b
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/ProcessBaseFragment.kt
@@ -0,0 +1,86 @@
+package com.alfresco.content.process.ui.fragments
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.View
+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
+
+/**
+ * Marked as BaseDetailFragment class
+ */
+abstract class ProcessBaseFragment : Fragment(), DeleteContentListener {
+
+ lateinit var listener: DeleteContentListener
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ listener = this
+ }
+
+ internal fun showCreateSheet(state: ProcessAttachFilesViewState, observerID: String) {
+ AnalyticsManager().taskEvent(EventName.UploadProcessAttachment)
+ val field = state.parent.field
+ CreateActionsSheet.with(Entry.defaultWorkflowEntry(observerID, field.id, field.params?.multiple ?: false)).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(entry: Entry)
+}
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
new file mode 100644
index 000000000..50cd74db7
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/fragments/ProcessFragment.kt
@@ -0,0 +1,284 @@
+package com.alfresco.content.process.ui.fragments
+
+import android.annotation.SuppressLint
+import android.content.Intent
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.core.view.isVisible
+import androidx.fragment.app.Fragment
+import androidx.navigation.findNavController
+import com.airbnb.mvrx.Fail
+import com.airbnb.mvrx.Loading
+import com.airbnb.mvrx.Mavericks
+import com.airbnb.mvrx.MavericksView
+import com.airbnb.mvrx.Success
+import com.airbnb.mvrx.activityViewModel
+import com.airbnb.mvrx.withState
+import com.alfresco.content.common.EntryListener
+import com.alfresco.content.data.Entry
+import com.alfresco.content.data.OfflineRepository
+import com.alfresco.content.data.ParentEntry
+import com.alfresco.content.data.payloads.FieldType
+import com.alfresco.content.data.payloads.FieldsData
+import com.alfresco.content.hideSoftInput
+import com.alfresco.content.process.R
+import com.alfresco.content.process.databinding.FragmentProcessBinding
+import com.alfresco.content.process.ui.components.updateProcessList
+import com.alfresco.content.process.ui.composeviews.FormScreen
+import com.alfresco.content.process.ui.theme.AlfrescoBaseTheme
+import com.alfresco.kotlin.FilenameComparator
+import com.alfresco.list.merge
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.android.material.snackbar.Snackbar
+import java.lang.ref.WeakReference
+
+class ProcessFragment : Fragment(), MavericksView, EntryListener {
+
+ val viewModel: FormViewModel by activityViewModel()
+ lateinit var binding: FragmentProcessBinding
+ private var viewLayout: View? = null
+ private var menu: Menu? = null
+ private var isExecuted = false
+ private var confirmContentQueueDialog = WeakReference(null)
+ private var oldSnackbar: Snackbar? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setHasOptionsMenu(true)
+ }
+
+ private fun showSnackBar(message: String) {
+ val snackbar = Snackbar.make(binding.flComposeParent, message, Snackbar.LENGTH_SHORT)
+ if (oldSnackbar == null || oldSnackbar?.isShownOrQueued == false) {
+ oldSnackbar?.dismiss()
+ snackbar.show()
+ oldSnackbar = snackbar
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View {
+ binding = FragmentProcessBinding.inflate(inflater, container, false)
+ viewLayout = binding.root
+ return viewLayout as View
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ inflater.inflate(R.menu.menu_process, menu)
+ super.onCreateOptionsMenu(menu, inflater)
+ }
+
+ override fun onPrepareOptionsMenu(menu: Menu) {
+ this.menu = menu
+ super.onPrepareOptionsMenu(menu)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return when (item.itemId) {
+ R.id.action_info -> {
+ withState(viewModel) { state ->
+ val entry = state.parent.taskEntry
+ val intent = Intent(
+ requireActivity(),
+ Class.forName("com.alfresco.content.app.activity.TaskViewerActivity"),
+ ).apply {
+ putExtra(Mavericks.KEY_ARG, entry)
+ }
+ startActivity(intent)
+ }
+ true
+ }
+
+ else -> super.onOptionsItemSelected(item)
+ }
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ viewModel.setListener(this)
+
+ val supportActionBar = (requireActivity() as AppCompatActivity).supportActionBar
+ supportActionBar?.setDisplayShowHomeEnabled(true)
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+
+ withState(viewModel) {
+ if (it.parent.processInstanceId != null) {
+ if (it.parent.processInstanceId != null) {
+ supportActionBar?.title = it.parent.taskEntry.name.ifEmpty { getString(R.string.title_no_name) }
+ } else {
+ supportActionBar?.title = getString(R.string.title_workflow)
+ }
+ }
+ }
+
+ supportActionBar?.setHomeActionContentDescription(getString(R.string.label_navigation_back))
+
+ binding.composeView.apply {
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ setContent {
+ AlfrescoBaseTheme {
+ FormScreen(
+ navController = findNavController(),
+ viewModel = viewModel,
+ this@ProcessFragment,
+ )
+ }
+ }
+ }
+
+ binding.flComposeParent.setOnTouchListener { _, event ->
+ if (event.action == MotionEvent.ACTION_DOWN) {
+ hideSoftInput()
+ }
+ false
+ }
+
+ viewModel.onLinkContentToProcess = {
+ withState(viewModel) { state ->
+ it.second?.apply {
+ val listContents = listOf(it.first)
+ val isError = this.required && listContents.isEmpty()
+ viewModel.updateFieldValue(this.id, listContents, Pair(isError, ""))
+ }
+ }
+ }
+ }
+
+ override fun invalidate() = withState(viewModel) { state ->
+ binding.loading.isVisible = state.requestForm is Loading || state.requestStartWorkflow is Loading ||
+ state.requestSaveForm is Loading || state.requestOutcomes is Loading || state.requestProfile is Loading ||
+ state.requestAccountInfo is Loading || state.requestContent is Loading
+
+ handleError(state)
+ when {
+ state.requestStartWorkflow is Success || state.requestSaveForm is Success ||
+ state.requestOutcomes is Success || state.requestClaimRelease is Success -> {
+ viewModel.updateProcessList()
+ requireActivity().finish()
+ }
+
+ state.requestForm is Success -> {
+ val hasUploadField = state.formFields.any { it.type == FieldType.UPLOAD.value() }
+
+ if (state.parent.defaultEntries.isNotEmpty()) {
+ if (hasUploadField) {
+ viewModel.fetchUserProfile()
+ viewModel.fetchAccountInfo()
+ } else {
+ showSnackBar(getString(R.string.error_no_upload_fields))
+ }
+ }
+
+ if (hasUploadField) {
+ viewModel.observeUploads(state)
+ val fields = state.formFields
+ fields.forEach { field ->
+ if (field.type == FieldType.UPLOAD.value()) {
+ field.getContentList(state.parent.processDefinitionId).forEach(OfflineRepository()::addServerEntry)
+ }
+ }
+ }
+
+ viewModel.resetRequestState(state.requestForm)
+ }
+
+ state.requestAccountInfo is Success -> {
+ val uploadingFields = state.formFields.filter { it.type == FieldType.UPLOAD.value() }
+ val field = uploadingFields.find { it.params?.multiple == false } ?: uploadingFields.firstOrNull()
+ val sourceName = state.listAccountInfo.firstOrNull()?.sourceName ?: ""
+
+ if (!isExecuted) {
+ isExecuted = true
+ state.parent.defaultEntries.map { entry ->
+ viewModel.linkContentToProcess(state, entry, sourceName, field)
+ }
+ }
+ viewModel.resetRequestState(state.requestAccountInfo)
+ }
+
+ state.requestContent is Success -> {
+ viewModel.resetRequestState(state.requestContent)
+ }
+ }
+ menu?.findItem(R.id.action_info)?.isVisible = state.parent.processInstanceId != null
+ }
+
+ private fun handleError(state: FormViewState) {
+ when {
+ state.requestStartWorkflow is Fail<*> || state.requestForm is Fail<*> ||
+ state.requestSaveForm is Fail<*> || state.requestProfile is Fail<*> || state.request is Fail<*> ||
+ state.requestOutcomes is Fail<*> || state.requestContent is Fail<*> || state.requestProcessDefinition is Fail<*> ||
+ state.requestClaimRelease is Fail<*> || state.requestFormVariables is Fail<*> || state.requestAccountInfo is Fail<*> -> {
+ showSnackBar(getString(R.string.error_process_failure))
+ }
+ }
+ }
+
+ override fun onAttachFolder(entry: ParentEntry) = withState(viewModel) {
+ if (isAdded && viewModel.selectedField?.type == FieldType.SELECT_FOLDER.value()) {
+ viewModel.updateFieldValue(
+ viewModel.selectedField?.id ?: "",
+ entry as? Entry,
+ Pair(false, ""),
+ )
+ viewModel.selectedField = null
+ }
+ }
+
+ override fun onAttachFiles(field: FieldsData) = withState(viewModel) { state ->
+ if (isAdded && field.type == FieldType.UPLOAD.value()) {
+ val listContents = mergeInUploads(field.getContentList(state.parent.processDefinitionId), viewModel.getContents(state, field.id))
+ val isError = field.required && listContents.isEmpty()
+
+ viewModel.updateFieldValue(field.id, listContents, Pair(isError, ""))
+
+ viewModel.selectedField = null
+ }
+ }
+
+ private fun mergeInUploads(base: List, uploads: List): List {
+ if (uploads.isEmpty()) {
+ return emptyList()
+ }
+
+ return merge(base, uploads, includeRemainingRight = true) { left: Entry, right: Entry ->
+ FilenameComparator.compare(left.name, right.name)
+ }.distinctBy { it.id.ifEmpty { it.boxId } }
+ }
+
+ /**
+ * It will prompt if user trying to start workflow and if any of content file is in uploaded
+ */
+ fun confirmContentQueuePrompt() {
+ val oldDialog = confirmContentQueueDialog.get()
+ if (oldDialog != null && oldDialog.isShowing) return
+ val dialog = MaterialAlertDialogBuilder(requireContext())
+ .setCancelable(false)
+ .setTitle(getString(R.string.title_content_in_queue))
+ .setMessage(getString(R.string.message_content_in_queue))
+ .setNegativeButton(getString(R.string.dialog_negative_button_task), null)
+ .setPositiveButton(getString(R.string.dialog_positive_button_task)) { _, _ ->
+ viewModel.optionsModel?.let {
+ viewModel.performOutcomes(
+ it,
+ )
+ }
+ }
+ .show()
+ confirmContentQueueDialog = WeakReference(dialog)
+ }
+}
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) {
+ }
+}
diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/models/UpdateProcessData.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/models/UpdateProcessData.kt
new file mode 100644
index 000000000..fed16d61b
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/models/UpdateProcessData.kt
@@ -0,0 +1,11 @@
+package com.alfresco.content.process.ui.models
+
+/**
+ * Mark as UpdateProcessData data class
+ */
+data class UpdateProcessData(val isRefresh: Boolean)
+
+/**
+ * Mark as UpdateTasksData data class
+ */
+data class UpdateTasksData(val isRefresh: Boolean)
diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/theme/Color.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/theme/Color.kt
new file mode 100644
index 000000000..2b70e4f09
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/theme/Color.kt
@@ -0,0 +1,34 @@
+package com.alfresco.content.process.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val AlfrescoBlue900 = Color(0xFF1A43A9)
+val AlfrescoBlue700 = Color(0xFF1F74DB)
+val AlfrescoBlue300 = Color(0xFF6EACFF)
+
+val AlfrescoGray900 = Color(0xFF212328)
+
+val AlfrescoGray90015 = Color(0x26212328)
+val AlfrescoGray90030 = Color(0x4D212328)
+val AlfrescoGray90060 = Color(0x99212328)
+val AlfrescoGray90070 = Color(0xB3212328)
+val AlfrescoGray90024 = Color(0x3D212328)
+val AlfrescoGray100 = Color(0xFFF5F5F5)
+
+val AlfrescoError = Color(0xFFB8082A)
+
+// Define your specific colors
+val designDefaultDarkBackgroundColor = Color(0xFF121212)
+
+val Transparent = Color.Transparent
+val White = Color.White
+val White15 = Color(0x26FFFFFF)
+val White60 = Color(0x99FFFFFF)
+val ActionModeColor = AlfrescoBlue700 // Using AlfrescoBlue700 as action mode color
+val SeparateColorGrayLT = Color(0xFF212121)
+val chipColorGrayLT = Color(0x1F212121)
+val chipBackgroundColorGrayLT = Color(0x0D212121)
+
+val SeparateColorGrayDT = Color(0xFFFFFFFF)
+val chipColorGrayDT = Color(0x1FFFFFFF)
+val chipBackgroundColorGrayDT = Color(0x0DFFFFFF)
diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/theme/Theme.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/theme/Theme.kt
new file mode 100644
index 000000000..71ff34f69
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/theme/Theme.kt
@@ -0,0 +1,58 @@
+package com.alfresco.content.process.ui.theme
+
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+
+private val DarkColorScheme = darkColorScheme(
+ primary = AlfrescoBlue700,
+ onSurface = White60,
+ onPrimary = White60,
+ onSurfaceVariant = White60,
+ onBackground = Color.White,
+ background = designDefaultDarkBackgroundColor,
+ error = AlfrescoError,
+)
+private val LightColorScheme = lightColorScheme(
+ primary = AlfrescoBlue700,
+ onPrimary = AlfrescoGray90070,
+ onSurface = AlfrescoGray900,
+ onSurfaceVariant = AlfrescoGray90015,
+ outline = AlfrescoGray90015,
+ error = AlfrescoError,
+)
+
+@Composable
+fun AlfrescoBaseTheme(
+ darkTheme: Boolean = isNightMode(),
+ // Dynamic color is available on Android 12+
+ dynamicColor: Boolean = true,
+ content: @Composable () -> Unit,
+) {
+ val colorScheme = when {
+ darkTheme -> DarkColorScheme.copy(
+ secondary = MaterialTheme.colorScheme.primary, //
+ )
+
+ else -> LightColorScheme.copy(
+ secondary = MaterialTheme.colorScheme.primary, //
+ )
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = Typography,
+ content = content,
+ )
+}
+
+@Composable
+fun isNightMode() = when (AppCompatDelegate.getDefaultNightMode()) {
+ AppCompatDelegate.MODE_NIGHT_NO -> false
+ AppCompatDelegate.MODE_NIGHT_YES -> true
+ else -> isSystemInDarkTheme()
+}
diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/theme/Type.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/theme/Type.kt
new file mode 100644
index 000000000..8f4fec6ca
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/theme/Type.kt
@@ -0,0 +1,34 @@
+package com.alfresco.content.process.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+ bodyLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp,
+ ),
+ /* Other default text styles to override
+ titleLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp
+ ),
+ labelSmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ )
+ */
+)
diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/utils/ActionsReadOnlyField.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/utils/ActionsReadOnlyField.kt
new file mode 100644
index 000000000..bc5fbfd53
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/utils/ActionsReadOnlyField.kt
@@ -0,0 +1,61 @@
+package com.alfresco.content.process.ui.utils
+
+import android.content.Context
+import android.os.Bundle
+import androidx.navigation.NavController
+import com.airbnb.mvrx.Mavericks
+import com.alfresco.content.component.ComponentBuilder
+import com.alfresco.content.component.ComponentData
+import com.alfresco.content.component.ComponentType
+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.fragments.FormViewState
+
+fun actionsReadOnlyField(
+ isTapped: Boolean,
+ field: FieldsData,
+ navController: NavController,
+ state: FormViewState,
+ context: Context,
+) {
+ when (field.params?.field?.type?.lowercase()) {
+ FieldType.UPLOAD.value() -> {
+ if (isTapped && field.value is List<*> && (field.value as List<*>).isNotEmpty()) {
+ 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,
+ )
+ }
+ }
+
+ FieldType.TEXT.value(), FieldType.MULTI_LINE_TEXT.value() -> {
+ ComponentBuilder(
+ context,
+ ComponentData(
+ name = field.name,
+ query = "",
+ value = field.value as? String ?: "",
+ selector = ComponentType.VIEW_TEXT.value,
+ ),
+ )
+ .onApply { name, query, _ ->
+ }
+ .onReset { name, query, _ ->
+ }
+ .onCancel {
+ }
+ .show()
+ }
+ }
+}
diff --git a/process-app/src/main/kotlin/com/alfresco/content/process/ui/utils/Utils.kt b/process-app/src/main/kotlin/com/alfresco/content/process/ui/utils/Utils.kt
new file mode 100644
index 000000000..54ced530b
--- /dev/null
+++ b/process-app/src/main/kotlin/com/alfresco/content/process/ui/utils/Utils.kt
@@ -0,0 +1,168 @@
+package com.alfresco.content.process.ui.utils
+
+import android.annotation.SuppressLint
+import android.content.Context
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.alfresco.content.data.Entry
+import com.alfresco.content.data.OfflineStatus
+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.R
+import com.alfresco.content.process.ui.fragments.FormViewState
+
+@Composable
+fun trailingIconColor() = MaterialTheme.colorScheme.onPrimary
+
+fun Modifier.inputField() =
+ this
+ .fillMaxWidth()
+ .padding(start = 16.dp, end = 16.dp, top = 12.dp) // Add padding or other modifiers as needed
+
+fun integerInputError(value: String?, fieldsData: FieldsData, context: Context): Pair {
+ var errorData = Pair(false, "")
+
+ if (!value.isNullOrEmpty()) {
+ val minValue = fieldsData.minValue?.toLong() ?: 0
+ val maxValue = fieldsData.maxValue?.toLong() ?: 0
+
+ if (value.toLong() < minValue) {
+ errorData = Pair(true, context.getString(R.string.error_min_value, minValue))
+ }
+
+ if (value.toLong() > maxValue) {
+ errorData = Pair(true, context.getString(R.string.error_max_value, maxValue))
+ }
+ }
+
+ return errorData
+}
+
+fun singleLineInputError(value: String?, fieldsData: FieldsData, context: Context): Pair {
+ var isError = false
+ if (!value.isNullOrEmpty()) {
+ isError = (value.length < fieldsData.minLength)
+ }
+
+ val errorMessage = if (isError) {
+ context.getString(R.string.error_min_length, fieldsData.minLength)
+ } else {
+ ""
+ }
+ return Pair(isError, errorMessage)
+}
+
+fun multiLineInputError(value: String?, fieldsData: FieldsData, context: Context): Pair {
+ var isError = false
+ if (!value.isNullOrEmpty()) {
+ isError = (value.length < fieldsData.minLength)
+ }
+
+ val errorMessage = if (isError) {
+ context.getString(R.string.error_min_length, fieldsData.minLength)
+ } else {
+ ""
+ }
+ return Pair(isError, errorMessage)
+}
+
+fun booleanInputError(value: Boolean, fieldsData: FieldsData, context: Context): Pair {
+ var isError = false
+ if (fieldsData.required) {
+ isError = !value
+ }
+
+ val errorMessage = if (isError) {
+ context.getString(R.string.error_required_field)
+ } else {
+ ""
+ }
+ return Pair(isError, errorMessage)
+}
+
+fun amountInputError(value: String?, fieldsData: FieldsData, context: Context): Pair {
+ val errorData = Pair(false, "")
+
+ if (value.isNullOrEmpty()) {
+ return errorData
+ }
+
+ if (value.toFloatOrNull() == null) {
+ return Pair(true, context.getString(R.string.error_invalid_format))
+ }
+
+ val minValue = fieldsData.minValue?.toFloat() ?: 0f
+ val maxValue = fieldsData.maxValue?.toFloat() ?: 0f
+
+ if (value.toFloat() < minValue) {
+ return Pair(true, context.getString(R.string.error_min_value, minValue.toInt()))
+ }
+
+ if (value.toFloat() > maxValue) {
+ return Pair(true, context.getString(R.string.error_max_value, maxValue.toInt()))
+ }
+
+ return errorData
+}
+
+@SuppressLint("StringFormatInvalid")
+fun dateTimeInputError(value: String?, fieldsData: FieldsData, context: Context): Pair {
+ var isError = false
+
+ if (!value.isNullOrEmpty()) {
+ isError = (value.length < fieldsData.minLength)
+ }
+
+ val errorMessage = if (isError) {
+ context.getString(R.string.error_required_field, fieldsData.minLength)
+ } else {
+ ""
+ }
+ return Pair(isError, errorMessage)
+}
+
+@SuppressLint("StringFormatInvalid")
+fun dropDownRadioInputError(value: String?, fieldsData: FieldsData, context: Context): Pair {
+ var isError = false
+
+ if (!value.isNullOrEmpty()) {
+ isError = (value.length < fieldsData.minLength)
+ }
+
+ val errorMessage = if (isError) {
+ context.getString(R.string.error_required_field, fieldsData.minLength)
+ } else {
+ ""
+ }
+ return Pair(isError, errorMessage)
+}
+
+fun userGroupInputError(value: UserGroupDetails?, fieldsData: FieldsData, context: Context): Pair {
+ val isError = (fieldsData.required && value == null)
+
+ val errorMessage = ""
+
+ return Pair(isError, errorMessage)
+}
+
+fun folderInputError(value: Entry?, fieldsData: FieldsData, context: Context): Pair {
+ val isError = (fieldsData.required && value == null)
+
+ val errorMessage = ""
+
+ return Pair(isError, errorMessage)
+}
+
+fun getContentList(state: FormViewState): List {
+ val uploadList = state.formFields.filter { it.type == FieldType.UPLOAD.value() }
+
+ return uploadList.flatMap { it.getContentList(state.parent.processDefinitionId) }.filter {
+ (!it.isUpload && it.offlineStatus == OfflineStatus.SYNCED) ||
+ (it.isUpload && it.offlineStatus == OfflineStatus.UNDEFINED)
+ }
+}
diff --git a/process-app/src/main/res/drawable/ic_add.xml b/process-app/src/main/res/drawable/ic_add.xml
new file mode 100644
index 000000000..4d5e6ac9c
--- /dev/null
+++ b/process-app/src/main/res/drawable/ic_add.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/process-app/src/main/res/drawable/ic_edit_blue.xml b/process-app/src/main/res/drawable/ic_edit_blue.xml
new file mode 100644
index 000000000..2037477dc
--- /dev/null
+++ b/process-app/src/main/res/drawable/ic_edit_blue.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/process-app/src/main/res/drawable/ic_empty_files.xml b/process-app/src/main/res/drawable/ic_empty_files.xml
new file mode 100644
index 000000000..12e5a4491
--- /dev/null
+++ b/process-app/src/main/res/drawable/ic_empty_files.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
diff --git a/process-app/src/main/res/drawable/ic_info.xml b/process-app/src/main/res/drawable/ic_info.xml
new file mode 100644
index 000000000..d99625aca
--- /dev/null
+++ b/process-app/src/main/res/drawable/ic_info.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/process-app/src/main/res/layout/fragment_attach_files.xml b/process-app/src/main/res/layout/fragment_attach_files.xml
new file mode 100644
index 000000000..4388c00a9
--- /dev/null
+++ b/process-app/src/main/res/layout/fragment_attach_files.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/process-app/src/main/res/layout/fragment_process.xml b/process-app/src/main/res/layout/fragment_process.xml
new file mode 100644
index 000000000..f47deaad5
--- /dev/null
+++ b/process-app/src/main/res/layout/fragment_process.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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/menu/menu_process.xml b/process-app/src/main/res/menu/menu_process.xml
new file mode 100644
index 000000000..8203133fa
--- /dev/null
+++ b/process-app/src/main/res/menu/menu_process.xml
@@ -0,0 +1,10 @@
+
+
diff --git a/process-app/src/main/res/navigation/nav_process_paths.xml b/process-app/src/main/res/navigation/nav_process_paths.xml
new file mode 100644
index 000000000..4f60957a3
--- /dev/null
+++ b/process-app/src/main/res/navigation/nav_process_paths.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/process-app/src/main/res/values-de/strings.xml b/process-app/src/main/res/values-de/strings.xml
new file mode 100644
index 000000000..d6d7f2d79
--- /dev/null
+++ b/process-app/src/main/res/values-de/strings.xml
@@ -0,0 +1,14 @@
+
+ Geben Sie mindestens %1$d Zeichen ein
+ Text löschen
+ Symbol für Datum
+ Symbol für Verknüpfung
+ Darf nicht kleiner als %1$d sein
+ Darf nicht größer als %1$d sein
+ Verwenden Sie ein anderes Zahlenformat
+ Dies ist ein Pflichtfeld.
+ Aktionen
+ Schaltfläche „Aktionen verarbeiten“
+ Keine Anhänge
+ Etwas ist schiefgelaufen. Kontaktieren Sie Ihren Administrator für Hilfe.
+
diff --git a/process-app/src/main/res/values-es/strings.xml b/process-app/src/main/res/values-es/strings.xml
new file mode 100644
index 000000000..4f7d4c984
--- /dev/null
+++ b/process-app/src/main/res/values-es/strings.xml
@@ -0,0 +1,14 @@
+
+ Introduzca al menos %1$d caracteres
+ Borrar texto
+ Icono de fecha
+ Icono de enlace
+ No puede ser menor que %1$d
+ No puede ser mayor que %1$d
+ Utilice un formato de número distinto
+ Este campo es obligatorio.
+ Acciones
+ Botón de acciones de procesos
+ Sin adjuntos
+ Ha surgido un error. Contacte a su administrador para obtener ayuda.
+
diff --git a/process-app/src/main/res/values-fr/strings.xml b/process-app/src/main/res/values-fr/strings.xml
new file mode 100644
index 000000000..5754cce49
--- /dev/null
+++ b/process-app/src/main/res/values-fr/strings.xml
@@ -0,0 +1,14 @@
+
+ Veuillez saisir au moins %1$d caractères
+ Effacer le texte
+ Icône de date
+ Icône de lien
+ Ne peut pas être inférieur à %1$d
+ Ne peut pas être supérieur à %1$d
+ Utiliser un format numérique différent
+ Ceci est un champ obligatoire.
+ Actions
+ Bouton d\'actions de traitement
+ Aucune pièce jointe
+ Un problème est survenu. Pour obtenir de l\'aide, contactez votre administrateur Alfresco.
+
diff --git a/process-app/src/main/res/values-it/strings.xml b/process-app/src/main/res/values-it/strings.xml
new file mode 100644
index 000000000..2110011dc
--- /dev/null
+++ b/process-app/src/main/res/values-it/strings.xml
@@ -0,0 +1,14 @@
+
+ Immettere almeno %1$d caratteri
+ Elimina testo
+ Icona data
+ Icona link
+ Non deve essere inferiore a %1$d
+ Non deve essere superiore a %1$d
+ Usare un formato numerico diverso
+ Questo è un campo obbligatorio.
+ Azioni
+ Pulsante Elabora azioni
+ Nessun allegato
+ Si è verificato un problema. Per assistenza, contatta il tuo amministratore.
+
diff --git a/process-app/src/main/res/values-nl/strings.xml b/process-app/src/main/res/values-nl/strings.xml
new file mode 100644
index 000000000..fb35be3c2
--- /dev/null
+++ b/process-app/src/main/res/values-nl/strings.xml
@@ -0,0 +1,14 @@
+
+ Voer minstens %1$d tekens in
+ Tekst wissen
+ Datumpictogram
+ Koppelingpictogram
+ Mag niet minder zijn dan %1$d
+ Mag niet meer zijn dan %1$d
+ Gebruik een andere getalnotatie
+ Dit is een verplicht veld.
+ Acties
+ Knop Procesacties
+ Geen bijlagen
+ Er is iets fout gegaan. Neem contact op met uw beheerder voor hulp.
+
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/ids.xml b/process-app/src/main/res/values/ids.xml
new file mode 100644
index 000000000..3b3cc5ba9
--- /dev/null
+++ b/process-app/src/main/res/values/ids.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/process-app/src/main/res/values/strings.xml b/process-app/src/main/res/values/strings.xml
new file mode 100644
index 000000000..819103d54
--- /dev/null
+++ b/process-app/src/main/res/values/strings.xml
@@ -0,0 +1,22 @@
+
+ Enter at least %1$d characters
+ Clear Text
+ Date Icon
+ Link Icon
+ Can\'t be less than %1$d
+ Can\'t be greater than %1$d
+ Use a different number format
+ This is a required field.
+ Actions
+ Process Actions Button
+ No attachment(s)
+ No Attached Folder
+ %d folder(s)
+ Info
+ Looks like you haven’t\nadded any files yet\n(Max file size: %d MB).
+ Search Folder
+ %1$s has invalid URL
+ Unable to attach the selected content in this form.
+ Please note: Maximum file size for uploads is %d MB.
+ Something went wrong. Contact your administrator for help.
+
diff --git a/process-app/src/main/res/values/themes.xml b/process-app/src/main/res/values/themes.xml
new file mode 100644
index 000000000..8b92e766f
--- /dev/null
+++ b/process-app/src/main/res/values/themes.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/scan/src/main/res/values-de/strings.xml b/scan/src/main/res/values-de/strings.xml
index 386679b34..4cf253e7f 100755
--- a/scan/src/main/res/values-de/strings.xml
+++ b/scan/src/main/res/values-de/strings.xml
@@ -1,35 +1,35 @@
-
-
- Schließen
- Galerie
- Blitzlichtmodus
-
- Foto
-
- @string/scan_failure_permissions
- Um Dokumente zu scannen, aktivieren Sie die Kamera und die Leseberechtigung.
-
- Gescannte Dateien verwerfen?
- Gescannte Dateien werden verworfen, wenn Sie beenden.
- Abbrechen
- Verwerfen
-
-
- Weiter
- Scannen
- Geringer Speicher
- Ihr Handy hat nur geringen Speicherplatz zur Verfügung, daher kann es bei der Bildbearbeitung zu Verzögerungen kommen.
- Scan beibehalten
- Neuaufnahme
- Wird geladen
- Beschreibung
- Dateiname
- Abbrechen
- Erstellen
- Erstellen
- Der Ordnername darf keines der folgenden Zeichen enthalten: ?:\"*|/\<>
- Der Ordnername darf nicht leer sein.
- Vorschau nicht verfügbar
- Speichern
-
-
+
+
+ Schließen
+ Galerie
+ Blitzlichtmodus
+
+ Foto
+
+ @string/scan_failure_permissions
+ Um Dokumente zu scannen, aktivieren Sie die Kamera und die Leseberechtigung.
+
+ Gescannte Dateien verwerfen?
+ Gescannte Dateien werden verworfen, wenn Sie beenden.
+ Abbrechen
+ Verwerfen
+
+
+ Weiter
+ Scannen
+ Geringer Speicher
+ Ihr Handy hat nur geringen Speicherplatz zur Verfügung, daher kann es bei der Bildbearbeitung zu Verzögerungen kommen.
+ Scan beibehalten
+ Neuaufnahme
+ Wird geladen
+ Beschreibung
+ Dateiname
+ Abbrechen
+ Erstellen
+ Erstellen
+ Der Ordnername darf keines der folgenden Zeichen enthalten: ?:\"*|/\<>
+ Der Ordnername darf nicht leer sein.
+ Vorschau nicht verfügbar
+ Speichern
+
+
diff --git a/scan/src/main/res/values-es/strings.xml b/scan/src/main/res/values-es/strings.xml
index db0589a80..ff2ff3ea3 100755
--- a/scan/src/main/res/values-es/strings.xml
+++ b/scan/src/main/res/values-es/strings.xml
@@ -1,35 +1,35 @@
-
-
- Cerrar
- Galería
- Modo flash
-
- Foto
-
- @string/scan_failure_permissions
- Para escanear documentos, active los permisos de cámara y lectura.
-
- ¿Descartar ficheros escaneados?
- Si sale, se descartarán los ficheros escaneados.
- Cancelar
- Descartar
-
-
- Siguiente
- Escaneo en curso
- Baja memoria
- Su móvil tiene poca memoria, es posible que tenga retrasos al editar imágenes.
- Guardar escaneo
- Retomar
- Cargando
- Añadir descripción
- Nombre del fichero
- Cancelar
- Crear
- Crear
- El nombre de la carpeta no puede contener ninguno de los siguientes caracteres: ?: \"*|/\<>
- El nombre de la carpeta no puede estar vacío.
- Vista previa no disponible
- Guardar
-
-
+
+
+ Cerrar
+ Galería
+ Modo flash
+
+ Foto
+
+ @string/scan_failure_permissions
+ Para escanear documentos, active los permisos de cámara y lectura.
+
+ ¿Descartar ficheros escaneados?
+ Si sale, se descartarán los ficheros escaneados.
+ Cancelar
+ Descartar
+
+
+ Siguiente
+ Escaneo en curso
+ Baja memoria
+ Su móvil tiene poca memoria, es posible que tenga retrasos al editar imágenes.
+ Guardar escaneo
+ Retomar
+ Cargando
+ Añadir descripción
+ Nombre del fichero
+ Cancelar
+ Crear
+ Crear
+ El nombre de la carpeta no puede contener ninguno de los siguientes caracteres: ?: \"*|/\<>
+ El nombre de la carpeta no puede estar vacío.
+ Vista previa no disponible
+ Guardar
+
+
diff --git a/scan/src/main/res/values-fr/strings.xml b/scan/src/main/res/values-fr/strings.xml
index 02af4280f..6719e20ff 100755
--- a/scan/src/main/res/values-fr/strings.xml
+++ b/scan/src/main/res/values-fr/strings.xml
@@ -1,35 +1,35 @@
-
-
- Fermer
- Galerie
- Mode flash
-
- Photo
-
- @string/scan_failure_permissions
- Pour scanner les documents, activez l\'appareil photo et les permissions de lecture.
-
- Supprimer le scannage des fichiers
- Le scannage des fichiers sera supprimé si vous quittez.
- Annuler
- Supprimer
-
-
- Suivant
- Scannage
- Mémoire insuffisante
- La mémoire de votre téléphone est faible, vous pouvez ressentir des décalages lors de l\'édition des images.
- Conserver le scan
- Reprendre
- Chargement
- Ajouter une description
- Nom de fichier
- Annuler
- Créer
- Créer
- Le nom du dossier ne peut pas contenir les caractères suivants : ?:\"*|/\<>
- Le nom du dossier ne peut pas être vide.
- Aperçu non disponible
- Enregistrer
-
-
+
+
+ Fermer
+ Galerie
+ Mode flash
+
+ Photo
+
+ @string/scan_failure_permissions
+ Pour scanner les documents, activez l\'appareil photo et les permissions de lecture.
+
+ Supprimer le scannage des fichiers
+ Le scannage des fichiers sera supprimé si vous quittez.
+ Annuler
+ Supprimer
+
+
+ Suivant
+ Scannage
+ Mémoire insuffisante
+ La mémoire de votre téléphone est faible, vous pouvez ressentir des décalages lors de l\'édition des images.
+ Conserver le scan
+ Reprendre
+ Chargement
+ Ajouter une description
+ Nom de fichier
+ Annuler
+ Créer
+ Créer
+ Le nom du dossier ne peut pas contenir les caractères suivants : ?:\"*|/\<>
+ Le nom du dossier ne peut pas être vide.
+ Aperçu non disponible
+ Enregistrer
+
+
diff --git a/scan/src/main/res/values-it/strings.xml b/scan/src/main/res/values-it/strings.xml
index d8bea5f6c..bb8ac9874 100755
--- a/scan/src/main/res/values-it/strings.xml
+++ b/scan/src/main/res/values-it/strings.xml
@@ -1,35 +1,35 @@
-
-
- Chiudi
- Galleria
- Modalità flash
-
- Foto
-
- @string/scan_failure_permissions
- Per acquisire i documenti, attivare la fotocamera e le autorizzazioni di lettura.
-
- Eliminare i file acquisiti?
- Se si esce, i file acquisiti verranno eliminati.
- Annulla
- Elimina
-
-
- Avanti
- Scansione in corso
- Memoria insufficiente
- La memoria del telefono è insufficiente. Potrebbero verificarsi dei ritardi nella modifica delle immagini.
- Conserva la scansione
- Riacquisisci
- Caricamento in corso
- Aggiungi descrizione
- Nome file
- Annulla
- Crea
- Crea
- Il nome della cartella non può contenere i caratteri seguenti: ?:\"*|/\<>
- Il nome della cartella non può essere vuoto.
- Anteprima non disponibile
- Salva
-
-
+
+
+ Chiudi
+ Galleria
+ Modalità flash
+
+ Foto
+
+ @string/scan_failure_permissions
+ Per acquisire i documenti, attivare la fotocamera e le autorizzazioni di lettura.
+
+ Eliminare i file acquisiti?
+ Se si esce, i file acquisiti verranno eliminati.
+ Annulla
+ Elimina
+
+
+ Avanti
+ Scansione in corso
+ Memoria insufficiente
+ La memoria del telefono è insufficiente. Potrebbero verificarsi dei ritardi nella modifica delle immagini.
+ Conserva la scansione
+ Riacquisisci
+ Caricamento in corso
+ Aggiungi descrizione
+ Nome file
+ Annulla
+ Crea
+ Crea
+ Il nome della cartella non può contenere i caratteri seguenti: ?:\"*|/\<>
+ Il nome della cartella non può essere vuoto.
+ Anteprima non disponibile
+ Salva
+
+
diff --git a/scan/src/main/res/values-nl/strings.xml b/scan/src/main/res/values-nl/strings.xml
index 91d152c82..fd6e5b1d8 100755
--- a/scan/src/main/res/values-nl/strings.xml
+++ b/scan/src/main/res/values-nl/strings.xml
@@ -1,35 +1,35 @@
-
-
- Sluiten
- Galerij
- Flitsmodus
-
- Foto
-
- @string/scan_failure_permissions
- Schakel de rechten voor Camera en Lezen in om documenten te scannen.
-
- Scanbestanden verwijderen?
- Scanbestanden worden verwijderd als u afsluit.
- Annuleren
- Verwijderen
-
-
- Volgende
- Scannen
- Onvoldoende geheugen
- Uw telefoon heeft onvoldoende geheugen beschikbaar.Er kunnen vertragingen zijn bij het bewerken van afbeeldingen.
- Scan houden
- Opnieuw
- Laden…
- Beschrijving toevoegen
- Bestandsnaam
- Annuleren
- Maken
- Maken
- Mapnaam mag geen van de volgende tekens bevatten: ?:\"*|/\<>
- Mapnaam mag niet leeg zijn.
- Preview niet beschikbaar
- Opslaan
-
-
+
+
+ Sluiten
+ Galerij
+ Flitsmodus
+
+ Foto
+
+ @string/scan_failure_permissions
+ Schakel de rechten voor Camera en Lezen in om documenten te scannen.
+
+ Scanbestanden verwijderen?
+ Scanbestanden worden verwijderd als u afsluit.
+ Annuleren
+ Verwijderen
+
+
+ Volgende
+ Scannen
+ Onvoldoende geheugen
+ Uw telefoon heeft onvoldoende geheugen beschikbaar.Er kunnen vertragingen zijn bij het bewerken van afbeeldingen.
+ Scan houden
+ Opnieuw
+ Laden…
+ Beschrijving toevoegen
+ Bestandsnaam
+ Annuleren
+ Maken
+ Maken
+ Mapnaam mag geen van de volgende tekens bevatten: ?:\"*|/\<>
+ Mapnaam mag niet leeg zijn.
+ Preview niet beschikbaar
+ Opslaan
+
+
diff --git a/search/src/main/kotlin/com/alfresco/content/search/SearchFragment.kt b/search/src/main/kotlin/com/alfresco/content/search/SearchFragment.kt
index 626b6a83f..232b319a6 100644
--- a/search/src/main/kotlin/com/alfresco/content/search/SearchFragment.kt
+++ b/search/src/main/kotlin/com/alfresco/content/search/SearchFragment.kt
@@ -60,12 +60,14 @@ data class ContextualSearchArgs(
val title: String?,
val moveId: String,
val isExtension: Boolean,
+ val isProcess: Boolean? = null,
) : Parcelable {
companion object {
private const val ID_KEY = "id"
private const val TITLE_KEY = "title"
private const val EXTENSION_KEY = "extension"
private const val MOVE_ID_KEY = "moveId"
+ private const val PROCESS_KEY = "isProcess"
fun with(args: Bundle?): ContextualSearchArgs? {
if (args == null) return null
@@ -74,6 +76,7 @@ data class ContextualSearchArgs(
args.getString(TITLE_KEY, null),
args.getString(MOVE_ID_KEY, ""),
args.getBoolean(EXTENSION_KEY, false),
+ args.getBoolean(PROCESS_KEY, false),
)
}
}
@@ -134,10 +137,15 @@ class SearchFragment : Fragment(), MavericksView {
binding.recyclerViewChips.setController(epoxyController)
withState(viewModel) { state ->
- if (!state.isExtension) {
- setAdvanceSearchFiltersData()
- } else {
+
+ if (state.isProcess == true) {
binding.parentAdvanceSearch.visibility = View.GONE
+ } else {
+ if (!state.isExtension) {
+ setAdvanceSearchFiltersData()
+ } else {
+ binding.parentAdvanceSearch.visibility = View.GONE
+ }
}
}
@@ -147,14 +155,16 @@ class SearchFragment : Fragment(), MavericksView {
private fun setAdvanceSearchFiltersData() {
withState(viewModel) {
- if (viewModel.isShowAdvanceFilterView(it.listSearchFilters)) {
- binding.parentAdvanceSearch.visibility = View.VISIBLE
- binding.chipGroup.visibility = View.GONE
- setupDropDown()
- } else {
- binding.parentAdvanceSearch.visibility = View.GONE
- binding.chipGroup.visibility = View.VISIBLE
- setupChips()
+ if (it.isProcess == null || it.isProcess == false) {
+ if (viewModel.isShowAdvanceFilterView(it.listSearchFilters)) {
+ binding.parentAdvanceSearch.visibility = View.VISIBLE
+ binding.chipGroup.visibility = View.GONE
+ setupDropDown()
+ } else {
+ binding.parentAdvanceSearch.visibility = View.GONE
+ binding.chipGroup.visibility = View.VISIBLE
+ setupChips()
+ }
}
}
}
@@ -528,7 +538,7 @@ class SearchFragment : Fragment(), MavericksView {
resultsFragment.setFilters(advanceSearchFilter, facetData)
}
- fun clearMultiSelection() {
+ private fun clearMultiSelection() {
resultsFragment.clearMultiSelection()
}
}
diff --git a/search/src/main/kotlin/com/alfresco/content/search/SearchResultsFragment.kt b/search/src/main/kotlin/com/alfresco/content/search/SearchResultsFragment.kt
index 8c2b22d75..461e9e609 100644
--- a/search/src/main/kotlin/com/alfresco/content/search/SearchResultsFragment.kt
+++ b/search/src/main/kotlin/com/alfresco/content/search/SearchResultsFragment.kt
@@ -82,15 +82,28 @@ class SearchResultsFragment : ListFragment(
override fun onItemClicked(entry: Entry) {
viewModel.saveSearch()
withState(viewModel) { state ->
- if (!state.isExtension) {
- findNavController().navigateTo(entry)
- } else if (entry.isFolder) {
- if (state.moveId.isNotEmpty()) {
- val parentId = entry.parentPaths.find { it == state.moveId }
- if (parentId.isNullOrEmpty()) {
- findNavController().navigateToFolder(entry, state.moveId)
- } else Toast.makeText(requireContext(), getString(R.string.search_move_warning), Toast.LENGTH_SHORT).show()
- } else findNavController().navigateToExtensionFolder(entry)
+ when {
+ state.isProcess != null -> {
+ if (entry.isFolder) {
+ findNavController().navigateToFolder(entry, isProcess = true)
+ }
+ }
+ else -> {
+ if (!state.isExtension) {
+ findNavController().navigateTo(entry)
+ } else if (entry.isFolder) {
+ when {
+ state.moveId.isNotEmpty() -> {
+ val parentId = entry.parentPaths.find { it == state.moveId }
+ if (parentId.isNullOrEmpty()) {
+ findNavController().navigateToFolder(entry, state.moveId)
+ } else Toast.makeText(requireContext(), getString(R.string.search_move_warning), Toast.LENGTH_SHORT).show()
+ }
+
+ else -> findNavController().navigateToExtensionFolder(entry)
+ }
+ }
+ }
}
}
}
diff --git a/search/src/main/kotlin/com/alfresco/content/search/SearchResultsState.kt b/search/src/main/kotlin/com/alfresco/content/search/SearchResultsState.kt
index 9c0e07ebf..7e2a1fee0 100644
--- a/search/src/main/kotlin/com/alfresco/content/search/SearchResultsState.kt
+++ b/search/src/main/kotlin/com/alfresco/content/search/SearchResultsState.kt
@@ -39,10 +39,17 @@ data class SearchResultsState(
val contextId: String? = null,
val contextTitle: String? = null,
val isExtension: Boolean = false,
+ val isProcess: Boolean? = null,
val moveId: String = "",
) : ListViewState {
- constructor(args: ContextualSearchArgs) : this(contextId = args.id, contextTitle = args.title, isExtension = args.isExtension, moveId = args.moveId)
+ constructor(args: ContextualSearchArgs) : this(
+ contextId = args.id,
+ contextTitle = args.title,
+ isExtension = args.isExtension,
+ moveId = args.moveId,
+ isProcess = args.isProcess,
+ )
val isContextual: Boolean
get() {
diff --git a/search/src/main/kotlin/com/alfresco/content/search/SearchViewModel.kt b/search/src/main/kotlin/com/alfresco/content/search/SearchViewModel.kt
index 9a0e77641..662781767 100644
--- a/search/src/main/kotlin/com/alfresco/content/search/SearchViewModel.kt
+++ b/search/src/main/kotlin/com/alfresco/content/search/SearchViewModel.kt
@@ -98,7 +98,7 @@ class SearchViewModel(
it.terms.length >= MIN_QUERY_LENGTH
}.executeOnLatest({
if (canSearchOverCurrentNetwork()) {
- if (state.isExtension) {
+ if (state.isExtension || state.isProcess == true) {
repository.search(
it.terms,
it.contextId,
@@ -106,22 +106,20 @@ class SearchViewModel(
it.skipCount,
it.maxItems,
)
- } else
- repository.search(
- it.terms,
- it.contextId,
- it.filters,
- it.advanceSearchFilter,
- SearchFacetData(
- searchFacetFields = it.listFacetFields,
- searchFacetQueries = it.listFacetQueries,
- searchFacetIntervals = it.listFacetIntervals,
- ),
- it.skipCount,
- it.maxItems,
- )
- } else
- repository.offlineSearch(it.terms, it.advanceSearchFilter)
+ } else repository.search(
+ it.terms,
+ it.contextId,
+ it.filters,
+ it.advanceSearchFilter,
+ SearchFacetData(
+ searchFacetFields = it.listFacetFields,
+ searchFacetQueries = it.listFacetQueries,
+ searchFacetIntervals = it.listFacetIntervals,
+ ),
+ it.skipCount,
+ it.maxItems,
+ )
+ } else repository.offlineSearch(it.terms, it.advanceSearchFilter)
}) {
if (it is Loading) {
copy(request = it)
@@ -470,6 +468,7 @@ class SearchViewModel(
maxLimitReachedForMultiSelection = false,
)
}
+
override fun resetMaxLimitError() = setState { copy(maxLimitReachedForMultiSelection = false) }
/**
diff --git a/search/src/main/res/layout/list_search_filter_pop_up.xml b/search/src/main/res/layout/list_search_filter_pop_up.xml
deleted file mode 100644
index 45b015759..000000000
--- a/search/src/main/res/layout/list_search_filter_pop_up.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
diff --git a/search/src/main/res/layout/view_date_range_component.xml b/search/src/main/res/layout/view_date_range_component.xml
deleted file mode 100644
index 7dbfc21a3..000000000
--- a/search/src/main/res/layout/view_date_range_component.xml
+++ /dev/null
@@ -1,54 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/search/src/main/res/layout/view_facet_check_list_component.xml b/search/src/main/res/layout/view_facet_check_list_component.xml
deleted file mode 100644
index 6d1970d98..000000000
--- a/search/src/main/res/layout/view_facet_check_list_component.xml
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/search/src/main/res/layout/view_number_range_component.xml b/search/src/main/res/layout/view_number_range_component.xml
deleted file mode 100644
index d6079a5c2..000000000
--- a/search/src/main/res/layout/view_number_range_component.xml
+++ /dev/null
@@ -1,80 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/search/src/main/res/layout/view_radio_list_component.xml b/search/src/main/res/layout/view_radio_list_component.xml
deleted file mode 100644
index 7d87c6b97..000000000
--- a/search/src/main/res/layout/view_radio_list_component.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
-
-
diff --git a/search/src/main/res/layout/view_text_component.xml b/search/src/main/res/layout/view_text_component.xml
deleted file mode 100644
index f489e3d1d..000000000
--- a/search/src/main/res/layout/view_text_component.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/search/src/main/res/values/colors.xml b/search/src/main/res/values/colors.xml
index c938a997f..def906303 100644
--- a/search/src/main/res/values/colors.xml
+++ b/search/src/main/res/values/colors.xml
@@ -1,7 +1,6 @@
- #3D2A7DE1
#212121
#1F212121
#0D212328
diff --git a/settings.gradle b/settings.gradle
index 241663b92..d916ae0e7 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -25,6 +25,9 @@ include ':scan'
include ':DocumentScanner'
include ':component'
include ':app'
+include ':process-app'
+//include ':content'
+//include ':content-ktx'
// Enable Gradle's version catalog support
// Ref: https://docs.gradle.org/current/userguide/platforms.html
diff --git a/viewer-pdf/src/main/kotlin/com/alfresco/content/viewer/pdf/PdfViewerFragment.kt b/viewer-pdf/src/main/kotlin/com/alfresco/content/viewer/pdf/PdfViewerFragment.kt
index 555ba09df..1b29d0a77 100644
--- a/viewer-pdf/src/main/kotlin/com/alfresco/content/viewer/pdf/PdfViewerFragment.kt
+++ b/viewer-pdf/src/main/kotlin/com/alfresco/content/viewer/pdf/PdfViewerFragment.kt
@@ -63,6 +63,7 @@ class PdfViewerFragment : ChildViewerFragment(), MavericksView {
saveFormData = false
domStorageEnabled = true
safeBrowsingEnabled = true
+
}
val assetLoader = makeAssetLoader()