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 @@ + + + +