diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c72c25d5f9..26e487afcd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ ownCloud admins and users. * Enhancement - New layout for spaces list: [#4604](https://github.com/owncloud/android/issues/4604) * Enhancement - Add account ID to the user information: [#4605](https://github.com/owncloud/android/issues/4605) * Enhancement - Create a new space: [#4606](https://github.com/owncloud/android/issues/4606) +* Enhancement - Edit a space: [#4607](https://github.com/owncloud/android/issues/4607) * Enhancement - Update test in GitHub Actions: [#4663](https://github.com/owncloud/android/pull/4663) * Enhancement - New workflow to generate a build from "latest" tag on demand: [#4681](https://github.com/owncloud/android/pull/4681) @@ -120,6 +121,14 @@ ownCloud admins and users. https://github.com/owncloud/android/issues/4606 https://github.com/owncloud/android/pull/4683 +* Enhancement - Edit a space: [#4607](https://github.com/owncloud/android/issues/4607) + + A new edit space option has been added to the bottom sheet, available only to + users with the required permissions when the three-dot menu button is tapped. + + https://github.com/owncloud/android/issues/4607 + https://github.com/owncloud/android/pull/4687 + * Enhancement - Update test in GitHub Actions: [#4663](https://github.com/owncloud/android/pull/4663) A new Github Actions workflow has been added, in order to check whether the diff --git a/changelog/unreleased/4687 b/changelog/unreleased/4687 new file mode 100644 index 00000000000..894f738b4a1 --- /dev/null +++ b/changelog/unreleased/4687 @@ -0,0 +1,7 @@ +Enhancement: Edit a space + +A new edit space option has been added to the bottom sheet, available only to users +with the required permissions when the three-dot menu button is tapped. + +https://github.com/owncloud/android/issues/4607 +https://github.com/owncloud/android/pull/4687 diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt index 933c09b47c9..94e55d086f6 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt @@ -89,16 +89,19 @@ import com.owncloud.android.domain.sharing.shares.usecases.EditPublicShareAsyncU import com.owncloud.android.domain.sharing.shares.usecases.GetShareAsLiveDataUseCase import com.owncloud.android.domain.sharing.shares.usecases.GetSharesAsLiveDataUseCase import com.owncloud.android.domain.sharing.shares.usecases.RefreshSharesFromServerAsyncUseCase +import com.owncloud.android.domain.spaces.usecases.CreateSpaceUseCase +import com.owncloud.android.domain.spaces.usecases.EditSpaceUseCase +import com.owncloud.android.domain.spaces.usecases.FilterSpaceMenuOptionsUseCase import com.owncloud.android.domain.spaces.usecases.GetPersonalAndProjectSpacesForAccountUseCase import com.owncloud.android.domain.spaces.usecases.GetPersonalAndProjectSpacesWithSpecialsForAccountAsStreamUseCase import com.owncloud.android.domain.spaces.usecases.GetPersonalSpaceForAccountUseCase import com.owncloud.android.domain.spaces.usecases.GetPersonalSpacesWithSpecialsForAccountAsStreamUseCase import com.owncloud.android.domain.spaces.usecases.GetProjectSpacesWithSpecialsForAccountAsStreamUseCase import com.owncloud.android.domain.spaces.usecases.GetSpaceByIdForAccountUseCase +import com.owncloud.android.domain.spaces.usecases.GetSpacePermissionsAsyncUseCase import com.owncloud.android.domain.spaces.usecases.GetSpaceWithSpecialsByIdForAccountUseCase import com.owncloud.android.domain.spaces.usecases.GetSpacesFromEveryAccountUseCaseAsStream import com.owncloud.android.domain.spaces.usecases.RefreshSpacesFromServerAsyncUseCase -import com.owncloud.android.domain.spaces.usecases.CreateSpaceUseCase import com.owncloud.android.domain.transfers.usecases.ClearSuccessfulTransfersUseCase import com.owncloud.android.domain.transfers.usecases.GetAllTransfersAsStreamUseCase import com.owncloud.android.domain.transfers.usecases.GetAllTransfersUseCase @@ -222,12 +225,15 @@ val useCaseModule = module { // Spaces factoryOf(::CreateSpaceUseCase) + factoryOf(::EditSpaceUseCase) + factoryOf(::FilterSpaceMenuOptionsUseCase) factoryOf(::GetPersonalAndProjectSpacesForAccountUseCase) factoryOf(::GetPersonalAndProjectSpacesWithSpecialsForAccountAsStreamUseCase) factoryOf(::GetPersonalSpaceForAccountUseCase) factoryOf(::GetPersonalSpacesWithSpecialsForAccountAsStreamUseCase) factoryOf(::GetProjectSpacesWithSpecialsForAccountAsStreamUseCase) factoryOf(::GetSpaceByIdForAccountUseCase) + factoryOf(::GetSpacePermissionsAsyncUseCase) factoryOf(::GetSpaceWithSpecialsByIdForAccountUseCase) factoryOf(::GetSpacesFromEveryAccountUseCaseAsStream) factoryOf(::GetWebDavUrlForSpaceUseCase) diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt index 81e43a86123..72bcb6c6040 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt @@ -102,6 +102,6 @@ val viewModelModule = module { get()) } viewModel { ReceiveExternalFilesViewModel(get(), get(), get(), get()) } viewModel { (accountName: String, showPersonalSpace: Boolean) -> - SpacesListViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), accountName, showPersonalSpace) + SpacesListViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), accountName, showPersonalSpace) } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/SpaceMenuOptionExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/SpaceMenuOptionExt.kt new file mode 100644 index 00000000000..892c75cb603 --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/SpaceMenuOptionExt.kt @@ -0,0 +1,34 @@ +/** + * ownCloud Android client application + * + * @author Jorge Aguado Recio + * + * Copyright (C) 2025 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.extensions + +import com.owncloud.android.R +import com.owncloud.android.domain.spaces.model.SpaceMenuOption + +fun SpaceMenuOption.toStringResId() = + when (this) { + SpaceMenuOption.EDIT -> R.string.edit_space + } + +fun SpaceMenuOption.toDrawableResId() = + when (this) { + SpaceMenuOption.EDIT -> R.drawable.ic_pencil + } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/releasenotes/ReleaseNotesViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/releasenotes/ReleaseNotesViewModel.kt index acc58f234f7..31456d6c77b 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/releasenotes/ReleaseNotesViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/releasenotes/ReleaseNotesViewModel.kt @@ -53,6 +53,11 @@ class ReleaseNotesViewModel( subtitle = R.string.release_notes_4_7_0_subtitle_create_new_spaces, type = ReleaseNoteType.ENHANCEMENT ), + ReleaseNote( + title = R.string.release_notes_4_7_0_title_edit_spaces, + subtitle = R.string.release_notes_4_7_0_subtitle_edit_spaces, + type = ReleaseNoteType.ENHANCEMENT + ), ReleaseNote( title = R.string.release_notes_bugfixes_title, subtitle = R.string.release_notes_bugfixes_subtitle, diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListDiffUtil.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListDiffUtil.kt index 532c59f5a0a..68102fafdd3 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListDiffUtil.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListDiffUtil.kt @@ -2,8 +2,9 @@ * ownCloud Android client application * * @author Juan Carlos Garrote Gascón + * @author Jorge Aguado Recio * - * Copyright (C) 2023 ownCloud GmbH. + * Copyright (C) 2025 ownCloud GmbH. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -42,7 +43,7 @@ class SpacesListDiffUtil( val oldItem = oldList[oldItemPosition] val newItem = newList[newItemPosition] - if ((oldItem.name != newItem.name) || (oldItem.description != newItem.description) || + if ((oldItem.name != newItem.name) || (oldItem.description != newItem.description) || (oldItem.quota != newItem.quota) || (oldItem.getSpaceSpecialImage()?.id != newItem.getSpaceSpecialImage()?.id)) { return false } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListFragment.kt index 381cdd516cb..1f1af976183 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListFragment.kt @@ -30,6 +30,7 @@ import android.view.MenuInflater import android.view.View import android.view.ViewGroup import androidx.appcompat.widget.SearchView +import androidx.core.content.res.ResourcesCompat import androidx.core.os.bundleOf import androidx.core.view.isVisible import androidx.fragment.app.Fragment @@ -42,13 +43,17 @@ import com.owncloud.android.databinding.FileOptionsBottomSheetFragmentBinding import com.owncloud.android.databinding.SpacesListFragmentBinding import com.owncloud.android.domain.files.model.FileListOption import com.owncloud.android.domain.spaces.model.OCSpace +import com.owncloud.android.domain.spaces.model.SpaceMenuOption import com.owncloud.android.extensions.collectLatestLifecycleFlow import com.owncloud.android.extensions.showErrorInSnackbar import com.owncloud.android.extensions.showMessageInSnackbar import com.owncloud.android.extensions.toDrawableRes +import com.owncloud.android.extensions.toDrawableResId +import com.owncloud.android.extensions.toStringResId import com.owncloud.android.extensions.toSubtitleStringRes import com.owncloud.android.extensions.toTitleStringRes import com.owncloud.android.presentation.capabilities.CapabilityViewModel +import com.owncloud.android.presentation.common.BottomSheetFragmentItemView import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.presentation.common.UIResult import com.owncloud.android.presentation.spaces.createspace.CreateSpaceDialogFragment @@ -66,6 +71,9 @@ class SpacesListFragment : private val binding get() = _binding!! private var isMultiPersonal = false + private var editSpacesPermission = false + private var editQuotaPermission = false + private lateinit var currentSpace: OCSpace private val spacesListViewModel: SpacesListViewModel by viewModel { parametersOf( @@ -112,7 +120,12 @@ class SpacesListFragment : } binding.fabCreateSpace.setOnClickListener { - val dialog = CreateSpaceDialogFragment.newInstance(requireArguments().getString(BUNDLE_ACCOUNT_NAME), this) + val dialog = CreateSpaceDialogFragment.newInstance( + isEditMode = false, + canEditQuota = false, + currentSpace = null, + listener = this + ) dialog.show(requireActivity().supportFragmentManager, DIALOG_CREATE_SPACE) binding.fabCreateSpace.isFocusable = false } @@ -166,7 +179,11 @@ class SpacesListFragment : when (val uiResult = event.peekContent()) { is UIResult.Success -> { Timber.d("The permissions for $accountName are: ${uiResult.data}") - uiResult.data?.let { binding.fabCreateSpace.isVisible = it.contains(DRIVES_CREATE_ALL_PERMISSION) } + uiResult.data?.let { + binding.fabCreateSpace.isVisible = it.contains(DRIVES_CREATE_ALL_PERMISSION) + editSpacesPermission = it.contains(DRIVES_READ_WRITE_ALL_PERMISSION) + editQuotaPermission = it.contains(DRIVES_READ_WRITE_PROJECT_QUOTA_ALL_PERMISSION) + } } is UIResult.Loading -> { } is UIResult.Error -> { @@ -187,6 +204,20 @@ class SpacesListFragment : } } + collectLatestLifecycleFlow(spacesListViewModel.editSpaceFlow) { event -> + event?.let { + when (val uiResult = event.peekContent()) { + is UIResult.Success -> { showMessageInSnackbar(getString(R.string.edit_space_correctly)) } + is UIResult.Loading -> { } + is UIResult.Error -> { showErrorInSnackbar(R.string.edit_space_failed, uiResult.error) } + } + } + } + + collectLatestLifecycleFlow(spacesListViewModel.menuOptions) { menuOptions -> + showSpaceMenuOptionsDialog(menuOptions) + } + } private fun showOrHideEmptyView(spacesList: List) { @@ -211,6 +242,44 @@ class SpacesListFragment : } override fun onThreeDotButtonClick(ocSpace: OCSpace) { + currentSpace = ocSpace + spacesListViewModel.filterMenuOptions(ocSpace, editSpacesPermission) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + (menu.findItem(R.id.action_search).actionView as SearchView).run { + setOnQueryTextListener(this@SpacesListFragment) + queryHint = resources.getString(R.string.actionbar_search_space) + } + menu.findItem(R.id.action_share_current_folder)?.itemId?.let { menu.removeItem(it) } + } + + override fun onQueryTextSubmit(query: String?): Boolean = false + + override fun onQueryTextChange(newText: String?): Boolean { + newText?.let { spacesListViewModel.updateSearchFilter(it) } + return true + } + + override fun createSpace(spaceName: String, spaceSubtitle: String, spaceQuota: Long) { + spacesListViewModel.createSpace(spaceName, spaceSubtitle, spaceQuota) + } + + override fun editSpace(spaceId: String, spaceName: String, spaceSubtitle: String, spaceQuota: Long?) { + spacesListViewModel.editSpace(spaceId, spaceName, spaceSubtitle, spaceQuota) + } + + fun setSearchListener(searchView: SearchView) { + searchView.setOnQueryTextListener(this) + } + + private fun setTextHintRootToolbar() { + val searchViewRootToolbar = requireActivity().findViewById(R.id.root_toolbar_search_view) + searchViewRootToolbar.queryHint = getString(R.string.actionbar_search_space) + } + + private fun showSpaceMenuOptionsDialog(menuOptions: List) { val spaceOptionsBottomSheetBinding = FileOptionsBottomSheetFragmentBinding.inflate(layoutInflater) val dialog = BottomSheetDialog(requireContext()) dialog.setContentView(spaceOptionsBottomSheetBinding.root) @@ -226,14 +295,18 @@ class SpacesListFragment : thumbnailBottomSheet.setImageResource(R.drawable.ic_menu_space) val spaceNameBottomSheet = spaceOptionsBottomSheetBinding.fileNameBottomSheet - spaceNameBottomSheet.text = ocSpace.name + spaceNameBottomSheet.text = currentSpace.name val spaceSizeBottomSheet = spaceOptionsBottomSheetBinding.fileSizeBottomSheet - spaceSizeBottomSheet.text = DisplayUtils.bytesToHumanReadable(ocSpace.quota?.used ?: 0L, requireContext(), true) + spaceSizeBottomSheet.text = DisplayUtils.bytesToHumanReadable(currentSpace.quota?.used ?: 0L, requireContext(), true) val spaceSeparatorBottomSheet = spaceOptionsBottomSheetBinding.fileSeparatorBottomSheet spaceSeparatorBottomSheet.visibility = View.GONE + menuOptions.forEach { menuOption -> + setMenuOption(menuOption, spaceOptionsBottomSheetBinding, dialog) + } + fileOptionsBottomSheetSingleFileBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { override fun onStateChanged(bottomSheet: View, newState: Int) { if (newState == BottomSheetBehavior.STATE_DRAGGING) { @@ -247,33 +320,27 @@ class SpacesListFragment : dialog.show() } - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - super.onCreateOptionsMenu(menu, inflater) - (menu.findItem(R.id.action_search).actionView as SearchView).run { - setOnQueryTextListener(this@SpacesListFragment) - queryHint = resources.getString(R.string.actionbar_search_space) + private fun setMenuOption(menuOption: SpaceMenuOption, binding: FileOptionsBottomSheetFragmentBinding, dialog: BottomSheetDialog) { + val fileOptionItemView = BottomSheetFragmentItemView(requireContext()) + fileOptionItemView.apply { + itemIcon = ResourcesCompat.getDrawable(resources, menuOption.toDrawableResId(), null) + title = getString(menuOption.toStringResId()) + setOnClickListener { + dialog.dismiss() + when(menuOption) { + SpaceMenuOption.EDIT -> { + val editDialog = CreateSpaceDialogFragment.newInstance( + isEditMode = true, + canEditQuota = editQuotaPermission, + currentSpace = currentSpace, + listener = this@SpacesListFragment + ) + editDialog.show(requireActivity().supportFragmentManager, DIALOG_CREATE_SPACE) + } + } + } } - menu.findItem(R.id.action_share_current_folder)?.itemId?.let { menu.removeItem(it) } - } - - override fun onQueryTextSubmit(query: String?): Boolean = false - - override fun onQueryTextChange(newText: String?): Boolean { - newText?.let { spacesListViewModel.updateSearchFilter(it) } - return true - } - - override fun createSpace(spaceName: String, spaceSubtitle: String, spaceQuota: Long) { - spacesListViewModel.createSpace(spaceName, spaceSubtitle, spaceQuota) - } - - fun setSearchListener(searchView: SearchView) { - searchView.setOnQueryTextListener(this) - } - - private fun setTextHintRootToolbar() { - val searchViewRootToolbar = requireActivity().findViewById(R.id.root_toolbar_search_view) - searchViewRootToolbar.queryHint = getString(R.string.actionbar_search_space) + binding.fileOptionsBottomSheetLayout.addView(fileOptionItemView) } companion object { @@ -282,6 +349,8 @@ class SpacesListFragment : const val BUNDLE_SHOW_PERSONAL_SPACE = "showPersonalSpace" const val BUNDLE_ACCOUNT_NAME = "accountName" const val DRIVES_CREATE_ALL_PERMISSION = "Drives.Create.all" + const val DRIVES_READ_WRITE_ALL_PERMISSION = "Drives.ReadWrite.all" + const val DRIVES_READ_WRITE_PROJECT_QUOTA_ALL_PERMISSION = "Drives.ReadWriteProjectQuota.all" private const val DIALOG_CREATE_SPACE = "DIALOG_CREATE_SPACE" diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListViewModel.kt index cca765f7e39..5eb9b52c9bf 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListViewModel.kt @@ -29,7 +29,10 @@ import com.owncloud.android.domain.files.model.OCFile import com.owncloud.android.domain.files.model.OCFile.Companion.ROOT_PATH import com.owncloud.android.domain.files.usecases.GetFileByRemotePathUseCase import com.owncloud.android.domain.spaces.model.OCSpace +import com.owncloud.android.domain.spaces.model.SpaceMenuOption import com.owncloud.android.domain.spaces.usecases.CreateSpaceUseCase +import com.owncloud.android.domain.spaces.usecases.EditSpaceUseCase +import com.owncloud.android.domain.spaces.usecases.FilterSpaceMenuOptionsUseCase import com.owncloud.android.domain.spaces.usecases.GetPersonalAndProjectSpacesWithSpecialsForAccountAsStreamUseCase import com.owncloud.android.domain.spaces.usecases.GetPersonalSpacesWithSpecialsForAccountAsStreamUseCase import com.owncloud.android.domain.spaces.usecases.GetProjectSpacesWithSpecialsForAccountAsStreamUseCase @@ -57,6 +60,8 @@ class SpacesListViewModel( private val getUserIdAsyncUseCase: GetUserIdAsyncUseCase, private val getUserPermissionsAsyncUseCase: GetUserPermissionsAsyncUseCase, private val createSpaceUseCase: CreateSpaceUseCase, + private val filterSpaceMenuOptionsUseCase: FilterSpaceMenuOptionsUseCase, + private val editSpaceUseCase: EditSpaceUseCase, private val coroutinesDispatcherProvider: CoroutinesDispatcherProvider, private val accountName: String, private val showPersonalSpace: Boolean, @@ -72,9 +77,15 @@ class SpacesListViewModel( private val _userPermissions = MutableStateFlow>>?>(null) val userPermissions: StateFlow>>?> = _userPermissions + private val _menuOptions: MutableSharedFlow> = MutableSharedFlow() + val menuOptions: SharedFlow> = _menuOptions + private val _createSpaceFlow = MutableSharedFlow>?>() val createSpaceFlow: SharedFlow>?> = _createSpaceFlow + private val _editSpaceFlow = MutableSharedFlow>?>() + val editSpaceFlow: SharedFlow>?> = _editSpaceFlow + init { viewModelScope.launch(coroutinesDispatcherProvider.io) { refreshSpacesFromServer() @@ -155,6 +166,29 @@ class SpacesListViewModel( } } + fun editSpace(spaceId: String, spaceName: String, spaceSubtitle: String, spaceQuota: Long?) { + viewModelScope.launch(coroutinesDispatcherProvider.io) { + when (val result = editSpaceUseCase(EditSpaceUseCase.Params(accountName, spaceId, spaceName, spaceSubtitle, spaceQuota))) { + is UseCaseResult.Success -> _editSpaceFlow.emit(Event(UIResult.Success(result.getDataOrNull()))) + is UseCaseResult.Error -> _editSpaceFlow.emit(Event(UIResult.Error(error = result.getThrowableOrNull()))) + } + refreshSpacesFromServerAsyncUseCase(RefreshSpacesFromServerAsyncUseCase.Params(accountName)) + } + } + + fun filterMenuOptions(space: OCSpace, editSpacesPermission: Boolean) { + viewModelScope.launch(coroutinesDispatcherProvider.io) { + val result = filterSpaceMenuOptionsUseCase( + FilterSpaceMenuOptionsUseCase.Params( + accountName = accountName, + space = space, + editSpacesPermission = editSpacesPermission + ) + ) + _menuOptions.emit(result) + } + } + data class SpacesListUiState( val spaces: List, val rootFolderFromSelectedSpace: OCFile? = null, diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/createspace/CreateSpaceDialogFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/createspace/CreateSpaceDialogFragment.kt index 0196cd87fc7..e8eeed30889 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/createspace/CreateSpaceDialogFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/createspace/CreateSpaceDialogFragment.kt @@ -24,10 +24,12 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged import androidx.fragment.app.DialogFragment import com.owncloud.android.R import com.owncloud.android.databinding.CreateSpaceDialogBinding +import com.owncloud.android.domain.spaces.model.OCSpace class CreateSpaceDialogFragment : DialogFragment() { private var _binding: CreateSpaceDialogBinding? = null @@ -44,19 +46,53 @@ class CreateSpaceDialogFragment : DialogFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + val currentSpace = requireArguments().getParcelable(ARG_CURRENT_SPACE) + val isEditMode = requireArguments().getBoolean(ARG_EDIT_SPACE_MODE) + val canEditQuota = requireArguments().getBoolean(ARG_CAN_EDIT_SPACE_QUOTA) binding.apply { cancelCreateSpaceButton.setOnClickListener { dialog?.dismiss() } createSpaceDialogNameValue.doOnTextChanged { name, _, _, _ -> val errorMessage = validateName(name.toString()) updateUI(errorMessage) } + + if (isEditMode) { + createSpaceDialogTitle.text = getString(R.string.edit_space) + createSpaceDialogQuotaSection.isVisible = canEditQuota + + currentSpace?.let { + createSpaceDialogNameValue.setText(it.name) + createSpaceDialogSubtitleValue.setText(it.description) + createSpaceDialogQuotaUnit.setSelection(getQuotaValueForSpinner(it.quota!!.total)) + } + + createSpaceButton.apply { + text = getString(R.string.share_confirm_public_link_button) + contentDescription = getString(R.string.share_confirm_public_link_button) + } + } + createSpaceButton.setOnClickListener { - val spaceQuota = convertToBytes(binding.createSpaceDialogQuotaUnit.selectedItem.toString()) - createSpaceListener.createSpace( - spaceName = binding.createSpaceDialogNameValue.text.toString(), - spaceSubtitle = binding.createSpaceDialogSubtitleValue.text.toString(), - spaceQuota = spaceQuota - ) + val spaceName = createSpaceDialogNameValue.text.toString() + val spaceSubtitle = createSpaceDialogSubtitleValue.text.toString() + val spaceQuota = convertToBytes(createSpaceDialogQuotaUnit.selectedItem.toString()) + + if (isEditMode) { + currentSpace?.let { + createSpaceListener.editSpace( + spaceId = it.id, + spaceName = spaceName, + spaceSubtitle = spaceSubtitle, + spaceQuota = if (canEditQuota) spaceQuota else null + ) + } + } else { + createSpaceListener.createSpace( + spaceName = spaceName, + spaceSubtitle = spaceSubtitle, + spaceQuota = spaceQuota + ) + } dialog?.dismiss() } } @@ -89,17 +125,41 @@ class CreateSpaceDialogFragment : DialogFragment() { return quotaNumber * 1_000_000_000L } + private fun getQuotaValueForSpinner(spaceQuota: Long): Int { + val totalGB = spaceQuota / 1_000_000_000.0 + return when (totalGB) { + 1.0 -> 0 + 2.0 -> 1 + 5.0 -> 2 + 10.0 -> 3 + 50.0 -> 4 + 100.0 -> 5 + 0.0 -> 6 + else -> 0 + } + } + interface CreateSpaceListener { fun createSpace(spaceName: String, spaceSubtitle: String, spaceQuota: Long) + fun editSpace(spaceId: String, spaceName: String, spaceSubtitle: String, spaceQuota: Long?) } companion object { - private const val ARG_ACCOUNT_NAME = "ACCOUNT_NAME" + private const val ARG_EDIT_SPACE_MODE = "EDIT_SPACE_MODE" + private const val ARG_CAN_EDIT_SPACE_QUOTA = "CAN_EDIT_SPACE_QUOTA" + private const val ARG_CURRENT_SPACE = "CURRENT_SPACE" private const val FORBIDDEN_CHARACTERS = """[/\\.:?*"'><|]""" - fun newInstance(accountName: String?, listener: CreateSpaceListener): CreateSpaceDialogFragment { + fun newInstance( + isEditMode: Boolean, + canEditQuota: Boolean, + currentSpace: OCSpace?, + listener: CreateSpaceListener + ): CreateSpaceDialogFragment { val args = Bundle().apply { - putString(ARG_ACCOUNT_NAME, accountName) + putBoolean(ARG_EDIT_SPACE_MODE, isEditMode) + putBoolean(ARG_CAN_EDIT_SPACE_QUOTA, canEditQuota) + putParcelable(ARG_CURRENT_SPACE, currentSpace) } return CreateSpaceDialogFragment().apply { createSpaceListener = listener diff --git a/owncloudApp/src/main/res/layout/create_space_dialog.xml b/owncloudApp/src/main/res/layout/create_space_dialog.xml index f8f8f2196db..67dbaf73999 100644 --- a/owncloudApp/src/main/res/layout/create_space_dialog.xml +++ b/owncloudApp/src/main/res/layout/create_space_dialog.xml @@ -23,6 +23,7 @@ @@ -119,6 +120,7 @@ A new design for Infinite Scale users in the spaces list, with a three-dot button on each space card that opens an options menu when clicked Create project spaces Infinite Scale users with right permissions can now create project spaces directly from the app + Edit project spaces + Infinite Scale users with right permissions can now edit project spaces directly from the app Open in web Open in %1$s (web) @@ -851,6 +853,9 @@ Forbidden characters: / \\ . : ? * " ' > < | Space created correctly Space could not be created + Edit space + Space updated correctly + Space could not be updated forum or contribute in our GitHub repo]]> diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/methods/nonwebdav/PatchMethod.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/methods/nonwebdav/PatchMethod.kt new file mode 100644 index 00000000000..1843e0a6953 --- /dev/null +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/methods/nonwebdav/PatchMethod.kt @@ -0,0 +1,48 @@ +/* ownCloud Android Library is available under MIT license + * Copyright (C) 2025 ownCloud GmbH. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package com.owncloud.android.lib.common.http.methods.nonwebdav + +import okhttp3.OkHttpClient +import okhttp3.RequestBody +import java.io.IOException +import java.net.URL + +/** + * OkHttp patch calls wrapper + * + * @author Jorge Aguado Recio + */ +class PatchMethod( + url: URL, + private val patchRequestBody: RequestBody +) : HttpMethod(url) { + @Throws(IOException::class) + override fun onExecute(okHttpClient: OkHttpClient): Int { + request = request.newBuilder() + .patch(patchRequestBody) + .build() + return super.onExecute(okHttpClient) + } +} diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/EditRemoteSpaceOperation.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/EditRemoteSpaceOperation.kt new file mode 100644 index 00000000000..90c50ea4516 --- /dev/null +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/EditRemoteSpaceOperation.kt @@ -0,0 +1,100 @@ +/** + * ownCloud Android client application + * + * @author Jorge Aguado Recio + * + * Copyright (C) 2025 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + +package com.owncloud.android.lib.resources.spaces + +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.http.HttpConstants +import com.owncloud.android.lib.common.http.HttpConstants.CONTENT_TYPE_JSON +import com.owncloud.android.lib.common.http.methods.nonwebdav.PatchMethod +import com.owncloud.android.lib.common.operations.RemoteOperation +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode +import com.owncloud.android.lib.resources.spaces.responses.SpaceResponse +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody +import org.json.JSONObject +import timber.log.Timber +import java.net.URL + +class EditRemoteSpaceOperation( + private val spaceId: String, + private val spaceName: String, + private val spaceSubtitle: String, + private val spaceQuota: Long? +): RemoteOperation() { + override fun run(client: OwnCloudClient): RemoteOperationResult { + var result: RemoteOperationResult + try { + val moshi = Moshi.Builder().build() + + val uriBuilder = client.baseUri.buildUpon().apply { + appendEncodedPath(GRAPH_API_SPACES_PATH) + appendEncodedPath(spaceId) + } + + val requestBody = JSONObject().apply { + put(SPACE_NAME_BODY_PARAM, spaceName) + spaceQuota?.let { + put(SPACE_QUOTA_BODY_PARAM, JSONObject().apply { + put(SPACE_QUOTA_TOTAL_BODY_PARAM, spaceQuota) + }) + } + put(SPACE_DESCRIPTION_BODY_PARAM, spaceSubtitle) + }.toString().toRequestBody(CONTENT_TYPE_JSON.toMediaType()) + + val patchMethod = PatchMethod(URL(uriBuilder.build().toString()), requestBody) + + val status = client.executeHttpMethod(patchMethod) + + val response = patchMethod.getResponseBodyAsString() + + if (status == HttpConstants.HTTP_OK) { + Timber.d("Successful response: $response") + + val responseAdapter: JsonAdapter = moshi.adapter(SpaceResponse::class.java) + + result = RemoteOperationResult(ResultCode.OK) + result.data = responseAdapter.fromJson(response) + + Timber.d("Update of space completed and parsed to ${result.data}") + } else { + result = RemoteOperationResult(patchMethod) + Timber.e("Failed response while updating the space; status code: $status, response: $response") + } + } catch (e: Exception) { + result = RemoteOperationResult(e) + Timber.e(e, "Exception while updating the space $spaceId") + } + return result + } + + companion object { + private const val GRAPH_API_SPACES_PATH = "graph/v1.0/drives/" + private const val SPACE_NAME_BODY_PARAM = "name" + private const val SPACE_QUOTA_BODY_PARAM = "quota" + private const val SPACE_QUOTA_TOTAL_BODY_PARAM = "total" + private const val SPACE_DESCRIPTION_BODY_PARAM = "description" + } + +} diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/GetRemoteSpacePermissionsOperation.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/GetRemoteSpacePermissionsOperation.kt new file mode 100644 index 00000000000..130eb6bb661 --- /dev/null +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/GetRemoteSpacePermissionsOperation.kt @@ -0,0 +1,85 @@ +/** + * ownCloud Android client application + * + * @author Jorge Aguado Recio + * + * Copyright (C) 2025 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.lib.resources.spaces + +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.http.HttpConstants +import com.owncloud.android.lib.common.http.methods.nonwebdav.GetMethod +import com.owncloud.android.lib.common.operations.RemoteOperation +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode +import com.squareup.moshi.Json +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.JsonClass +import com.squareup.moshi.Moshi +import timber.log.Timber +import java.net.URL + +class GetRemoteSpacePermissionsOperation( + private val spaceId: String +): RemoteOperation>() { + override fun run(client: OwnCloudClient): RemoteOperationResult> { + var result: RemoteOperationResult> + try { + val requestUri = client.baseUri.buildUpon().apply { + appendEncodedPath(GRAPH_API_SPACES_PATH) + appendEncodedPath(spaceId) + appendEncodedPath(SPACE_PERMISSIONS_ENDPOINT) + build() + } + val getMethod = GetMethod(URL(requestUri.toString())) + + val status = client.executeHttpMethod(getMethod) + + val response = getMethod.getResponseBodyAsString() + + if (status == HttpConstants.HTTP_OK) { + Timber.d("Successful response: $response") + + val moshi: Moshi = Moshi.Builder().build() + val adapter: JsonAdapter = moshi.adapter(SpacePermissionsListResponse::class.java) + + result = RemoteOperationResult(ResultCode.OK) + result.data = getMethod.getResponseBodyAsString().let { adapter.fromJson(it)?.permissions ?: emptyList() } + + Timber.d("Get space permissions for user completed and parsed to ${result.data}") + } else { + result = RemoteOperationResult(getMethod) + Timber.e("Failed response while getting space permissions; status code: $status, response: $response") + } + } catch (e: Exception) { + result = RemoteOperationResult(e) + Timber.e(e, "Exception while getting space permissions for user") + } + return result + } + + @JsonClass(generateAdapter = true) + data class SpacePermissionsListResponse( + @Json(name = "@libre.graph.permissions.actions.allowedValues") + val permissions: List + ) + + companion object { + private const val GRAPH_API_SPACES_PATH = "graph/v1beta1/drives/" + private const val SPACE_PERMISSIONS_ENDPOINT = "root/permissions" + } +} diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/services/OCSpacesService.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/services/OCSpacesService.kt index 0bf79b91e59..3a60f427ad2 100644 --- a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/services/OCSpacesService.kt +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/services/OCSpacesService.kt @@ -25,6 +25,8 @@ package com.owncloud.android.lib.resources.spaces.services import com.owncloud.android.lib.common.OwnCloudClient import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.resources.spaces.CreateRemoteSpaceOperation +import com.owncloud.android.lib.resources.spaces.EditRemoteSpaceOperation +import com.owncloud.android.lib.resources.spaces.GetRemoteSpacePermissionsOperation import com.owncloud.android.lib.resources.spaces.GetRemoteSpacesOperation import com.owncloud.android.lib.resources.spaces.responses.SpaceResponse @@ -35,4 +37,10 @@ class OCSpacesService(override val client: OwnCloudClient) : SpacesService { override fun createSpace(spaceName: String, spaceSubtitle: String, spaceQuota: Long): RemoteOperationResult = CreateRemoteSpaceOperation(spaceName, spaceSubtitle, spaceQuota).execute(client) + override fun getSpacePermissions(spaceId: String): RemoteOperationResult> = + GetRemoteSpacePermissionsOperation(spaceId).execute(client) + + override fun editSpace(spaceId: String, spaceName: String, spaceSubtitle: String, spaceQuota: Long?): RemoteOperationResult = + EditRemoteSpaceOperation(spaceId, spaceName, spaceSubtitle, spaceQuota).execute(client) + } diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/services/SpacesService.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/services/SpacesService.kt index 63875260a71..ba1bc0dc268 100644 --- a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/services/SpacesService.kt +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/services/SpacesService.kt @@ -29,4 +29,6 @@ import com.owncloud.android.lib.resources.spaces.responses.SpaceResponse interface SpacesService : Service { fun getSpaces(): RemoteOperationResult> fun createSpace(spaceName: String, spaceSubtitle: String, spaceQuota: Long): RemoteOperationResult + fun getSpacePermissions(spaceId: String): RemoteOperationResult> + fun editSpace(spaceId: String, spaceName: String, spaceSubtitle: String, spaceQuota: Long?): RemoteOperationResult } diff --git a/owncloudData/src/main/java/com/owncloud/android/data/spaces/datasources/RemoteSpacesDataSource.kt b/owncloudData/src/main/java/com/owncloud/android/data/spaces/datasources/RemoteSpacesDataSource.kt index 949940d55c2..eb7a2673104 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/spaces/datasources/RemoteSpacesDataSource.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/spaces/datasources/RemoteSpacesDataSource.kt @@ -25,4 +25,6 @@ import com.owncloud.android.domain.spaces.model.OCSpace interface RemoteSpacesDataSource { fun refreshSpacesForAccount(accountName: String): List fun createSpace(accountName: String, spaceName: String, spaceSubtitle: String, spaceQuota: Long): OCSpace + fun getSpacePermissions(accountName: String, spaceId: String): List + fun editSpace(accountName: String, spaceId: String, spaceName: String, spaceSubtitle: String, spaceQuota: Long?): OCSpace } diff --git a/owncloudData/src/main/java/com/owncloud/android/data/spaces/datasources/implementation/OCRemoteSpacesDataSource.kt b/owncloudData/src/main/java/com/owncloud/android/data/spaces/datasources/implementation/OCRemoteSpacesDataSource.kt index a1b6f719e49..582a58e7273 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/spaces/datasources/implementation/OCRemoteSpacesDataSource.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/spaces/datasources/implementation/OCRemoteSpacesDataSource.kt @@ -53,6 +53,16 @@ class OCRemoteSpacesDataSource( return spaceResponse.toModel(accountName) } + override fun getSpacePermissions(accountName: String, spaceId: String): List = + executeRemoteOperation { clientManager.getSpacesService(accountName).getSpacePermissions(spaceId) } + + override fun editSpace(accountName: String, spaceId: String, spaceName: String, spaceSubtitle: String, spaceQuota: Long?): OCSpace { + val spaceResponse = executeRemoteOperation { + clientManager.getSpacesService(accountName).editSpace(spaceId, spaceName, spaceSubtitle, spaceQuota) + } + return spaceResponse.toModel(accountName) + } + companion object { @VisibleForTesting diff --git a/owncloudData/src/main/java/com/owncloud/android/data/spaces/repository/OCSpacesRepository.kt b/owncloudData/src/main/java/com/owncloud/android/data/spaces/repository/OCSpacesRepository.kt index e7a74779e36..f8b5ebccde1 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/spaces/repository/OCSpacesRepository.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/spaces/repository/OCSpacesRepository.kt @@ -74,6 +74,9 @@ class OCSpacesRepository( override fun getSpaceByIdForAccount(spaceId: String?, accountName: String): OCSpace? = localSpacesDataSource.getSpaceByIdForAccount(spaceId = spaceId, accountName = accountName) + override fun getSpacePermissions(accountName: String, spaceId: String): List = + remoteSpacesDataSource.getSpacePermissions(accountName, spaceId) + override fun getWebDavUrlForSpace(accountName: String, spaceId: String?): String? = localSpacesDataSource.getWebDavUrlForSpace(accountName = accountName, spaceId = spaceId) @@ -81,4 +84,8 @@ class OCSpacesRepository( remoteSpacesDataSource.createSpace(accountName, spaceName, spaceSubtitle, spaceQuota) } + override fun editSpace(accountName: String, spaceId: String, spaceName: String, spaceSubtitle: String, spaceQuota: Long?) { + remoteSpacesDataSource.editSpace(accountName, spaceId, spaceName, spaceSubtitle, spaceQuota) + } + } diff --git a/owncloudData/src/test/java/com/owncloud/android/data/spaces/datasources/implementation/OCRemoteSpacesDataSourceTest.kt b/owncloudData/src/test/java/com/owncloud/android/data/spaces/datasources/implementation/OCRemoteSpacesDataSourceTest.kt index b7263b9bcff..0bbd47f412d 100644 --- a/owncloudData/src/test/java/com/owncloud/android/data/spaces/datasources/implementation/OCRemoteSpacesDataSourceTest.kt +++ b/owncloudData/src/test/java/com/owncloud/android/data/spaces/datasources/implementation/OCRemoteSpacesDataSourceTest.kt @@ -3,8 +3,9 @@ * * @author Aitor Ballesteros Pavón * @author Juan Carlos Garrote Gascón + * @author Jorge Aguado Recio * - * Copyright (C) 2023 ownCloud GmbH. + * Copyright (C) 2025 ownCloud GmbH. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -26,6 +27,7 @@ import com.owncloud.android.data.spaces.datasources.implementation.OCRemoteSpace import com.owncloud.android.lib.resources.spaces.services.OCSpacesService import com.owncloud.android.testutil.OC_ACCOUNT_NAME import com.owncloud.android.testutil.OC_SPACE_PROJECT_WITH_IMAGE +import com.owncloud.android.testutil.SPACE_PERMISSIONS import com.owncloud.android.testutil.SPACE_RESPONSE import com.owncloud.android.utils.createRemoteOperationResultMock import io.mockk.every @@ -96,4 +98,54 @@ class OCRemoteSpacesDataSourceTest { } } + @Test + fun `getSpacePermissions returns a list of String with project space permissions`() { + val getSpacePermissionsResult = createRemoteOperationResultMock(SPACE_PERMISSIONS, isSuccess = true) + + every { + ocSpaceService.getSpacePermissions(OC_SPACE_PROJECT_WITH_IMAGE.id) + } returns getSpacePermissionsResult + + val spacePermissions = ocRemoteSpacesDataSource.getSpacePermissions(OC_ACCOUNT_NAME, OC_SPACE_PROJECT_WITH_IMAGE.id) + assertEquals(SPACE_PERMISSIONS, spacePermissions) + + verify(exactly = 1) { + clientManager.getSpacesService(OC_ACCOUNT_NAME) + ocSpaceService.getSpacePermissions(OC_SPACE_PROJECT_WITH_IMAGE.id) + } + } + + @Test + fun `editSpace updates a project space correctly`() { + val editSpaceOperationResult = createRemoteOperationResultMock(SPACE_RESPONSE, isSuccess = true) + + every { + ocSpaceService.editSpace( + spaceId = OC_SPACE_PROJECT_WITH_IMAGE.id, + spaceName = OC_SPACE_PROJECT_WITH_IMAGE.name, + spaceSubtitle = OC_SPACE_PROJECT_WITH_IMAGE.description!!, + spaceQuota = OC_SPACE_PROJECT_WITH_IMAGE.quota?.total!! + ) + } returns editSpaceOperationResult + + val spaceResult = ocRemoteSpacesDataSource.editSpace( + accountName = OC_ACCOUNT_NAME, + spaceId = OC_SPACE_PROJECT_WITH_IMAGE.id, + spaceName = OC_SPACE_PROJECT_WITH_IMAGE.name, + spaceSubtitle = OC_SPACE_PROJECT_WITH_IMAGE.description!!, + spaceQuota = OC_SPACE_PROJECT_WITH_IMAGE.quota?.total!! + ) + assertEquals(SPACE_RESPONSE.toModel(OC_ACCOUNT_NAME), spaceResult) + + verify(exactly = 1) { + clientManager.getSpacesService(OC_ACCOUNT_NAME) + ocSpaceService.editSpace( + spaceId = OC_SPACE_PROJECT_WITH_IMAGE.id, + spaceName = OC_SPACE_PROJECT_WITH_IMAGE.name, + spaceSubtitle = OC_SPACE_PROJECT_WITH_IMAGE.description!!, + spaceQuota = OC_SPACE_PROJECT_WITH_IMAGE.quota?.total!! + ) + } + } + } diff --git a/owncloudData/src/test/java/com/owncloud/android/data/spaces/repository/OCSpacesRepositoryTest.kt b/owncloudData/src/test/java/com/owncloud/android/data/spaces/repository/OCSpacesRepositoryTest.kt index 1b4bbdfdf17..96f48cbff60 100644 --- a/owncloudData/src/test/java/com/owncloud/android/data/spaces/repository/OCSpacesRepositoryTest.kt +++ b/owncloudData/src/test/java/com/owncloud/android/data/spaces/repository/OCSpacesRepositoryTest.kt @@ -34,6 +34,7 @@ import com.owncloud.android.testutil.OC_SPACE_PROJECT_WITH_IMAGE import com.owncloud.android.testutil.OC_USER_QUOTA_LIMITED import com.owncloud.android.testutil.OC_USER_QUOTA_UNLIMITED import com.owncloud.android.testutil.OC_USER_QUOTA_WITHOUT_PERSONAL +import com.owncloud.android.testutil.SPACE_PERMISSIONS import io.mockk.every import io.mockk.mockk import io.mockk.verify @@ -246,6 +247,20 @@ class OCSpacesRepositoryTest { } } + @Test + fun `getSpacePermissions returns a list of String with space permissions`() { + every { + remoteSpacesDataSource.getSpacePermissions(OC_ACCOUNT_NAME, OC_SPACE_PROJECT_WITH_IMAGE.id) + } returns SPACE_PERMISSIONS + + val spacePermissions = ocSpacesRepository.getSpacePermissions(OC_ACCOUNT_NAME, OC_SPACE_PROJECT_WITH_IMAGE.id) + assertEquals(SPACE_PERMISSIONS, spacePermissions) + + verify(exactly = 1) { + remoteSpacesDataSource.getSpacePermissions(OC_ACCOUNT_NAME, OC_SPACE_PROJECT_WITH_IMAGE.id) + } + } + @Test fun `getWebDavUrlForSpace returns a String of webdav url`() { every { @@ -302,4 +317,35 @@ class OCSpacesRepositoryTest { } } + @Test + fun `editSpace updates a space correctly`() { + every { + remoteSpacesDataSource.editSpace( + accountName = OC_ACCOUNT_NAME, + spaceId = OC_SPACE_PROJECT_WITH_IMAGE.id, + spaceName = OC_SPACE_PROJECT_WITH_IMAGE.name, + spaceSubtitle = OC_SPACE_PROJECT_WITH_IMAGE.description!!, + spaceQuota = OC_SPACE_PROJECT_WITH_IMAGE.quota?.total!! + ) + } returns OC_SPACE_PROJECT_WITH_IMAGE + + ocSpacesRepository.editSpace( + accountName = OC_ACCOUNT_NAME, + spaceId = OC_SPACE_PROJECT_WITH_IMAGE.id, + spaceName = OC_SPACE_PROJECT_WITH_IMAGE.name, + spaceSubtitle = OC_SPACE_PROJECT_WITH_IMAGE.description!!, + spaceQuota = OC_SPACE_PROJECT_WITH_IMAGE.quota?.total!! + ) + + verify (exactly = 1) { + remoteSpacesDataSource.editSpace( + accountName = OC_ACCOUNT_NAME, + spaceId = OC_SPACE_PROJECT_WITH_IMAGE.id, + spaceName = OC_SPACE_PROJECT_WITH_IMAGE.name, + spaceSubtitle = OC_SPACE_PROJECT_WITH_IMAGE.description!!, + spaceQuota = OC_SPACE_PROJECT_WITH_IMAGE.quota?.total!! + ) + } + } + } diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/SpacesRepository.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/SpacesRepository.kt index 36c6d72c21f..5f5482b62e2 100644 --- a/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/SpacesRepository.kt +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/SpacesRepository.kt @@ -33,6 +33,8 @@ interface SpacesRepository { fun getPersonalAndProjectSpacesForAccount(accountName: String): List fun getSpaceWithSpecialsByIdForAccount(spaceId: String?, accountName: String): OCSpace fun getSpaceByIdForAccount(spaceId: String?, accountName: String): OCSpace? + fun getSpacePermissions(accountName: String, spaceId: String): List fun getWebDavUrlForSpace(accountName: String, spaceId: String?): String? fun createSpace(accountName: String, spaceName: String, spaceSubtitle: String, spaceQuota: Long) + fun editSpace(accountName: String, spaceId: String, spaceName: String, spaceSubtitle: String, spaceQuota: Long?) } diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/model/OCSpace.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/model/OCSpace.kt index 35a14ce7c5e..8db903993b9 100644 --- a/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/model/OCSpace.kt +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/model/OCSpace.kt @@ -3,8 +3,9 @@ * * @author Abel García de Prada * @author Juan Carlos Garrote Gascón + * @author Jorge Aguado Recio * - * Copyright (C) 2024 ownCloud GmbH. + * Copyright (C) 2025 ownCloud GmbH. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -21,6 +22,10 @@ package com.owncloud.android.domain.spaces.model +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize data class OCSpace( val accountName: String, val driveAlias: String?, @@ -34,7 +39,7 @@ data class OCSpace( val webUrl: String?, val description: String?, val special: List?, -) { +) : Parcelable { val isPersonal get() = driveType == DRIVE_TYPE_PERSONAL val isProject get() = driveType == DRIVE_TYPE_PROJECT @@ -57,24 +62,28 @@ data class OCSpace( } } +@Parcelize data class SpaceOwner( val user: SpaceUser -) +) : Parcelable +@Parcelize data class SpaceQuota( val remaining: Long?, val state: String?, val total: Long, val used: Long?, -) +) : Parcelable +@Parcelize data class SpaceRoot( val eTag: String?, val id: String, val webDavUrl: String, val deleted: SpaceDeleted?, -) +) : Parcelable +@Parcelize data class SpaceSpecial( val eTag: String, val file: SpaceFile, @@ -84,20 +93,24 @@ data class SpaceSpecial( val size: Int, val specialFolder: SpaceSpecialFolder, val webDavUrl: String -) +) : Parcelable +@Parcelize data class SpaceDeleted( val state: String, -) +) : Parcelable +@Parcelize data class SpaceUser( val id: String -) +) : Parcelable +@Parcelize data class SpaceFile( val mimeType: String -) +) : Parcelable +@Parcelize data class SpaceSpecialFolder( val name: String -) +) : Parcelable diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/model/SpaceMenuOption.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/model/SpaceMenuOption.kt new file mode 100644 index 00000000000..75c73510a0b --- /dev/null +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/model/SpaceMenuOption.kt @@ -0,0 +1,25 @@ +/** + * ownCloud Android client application + * + * @author Jorge Aguado Recio + * + * Copyright (C) 2025 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.domain.spaces.model + +enum class SpaceMenuOption { + EDIT +} diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/usecases/EditSpaceUseCase.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/usecases/EditSpaceUseCase.kt new file mode 100644 index 00000000000..fe297719ac2 --- /dev/null +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/usecases/EditSpaceUseCase.kt @@ -0,0 +1,41 @@ +/** + * ownCloud Android client application + * + * @author Jorge Aguado Recio + * + * Copyright (C) 2025 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.domain.spaces.usecases + +import com.owncloud.android.domain.BaseUseCaseWithResult +import com.owncloud.android.domain.spaces.SpacesRepository + +class EditSpaceUseCase( + private val spacesRepository: SpacesRepository +) : BaseUseCaseWithResult() { + + override fun run(params: Params) = + spacesRepository.editSpace(params.accountName, params.spaceId, params.spaceName, params.spaceSubtitle, params.spaceQuota) + + data class Params( + val accountName: String, + val spaceId: String, + val spaceName: String, + val spaceSubtitle: String, + val spaceQuota: Long? + ) + +} diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/usecases/FilterSpaceMenuOptionsUseCase.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/usecases/FilterSpaceMenuOptionsUseCase.kt new file mode 100644 index 00000000000..39e9172d280 --- /dev/null +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/usecases/FilterSpaceMenuOptionsUseCase.kt @@ -0,0 +1,61 @@ +/** + * ownCloud Android client application + * + * @author Jorge Aguado Recio + * + * Copyright (C) 2025 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.domain.spaces.usecases + +import com.owncloud.android.domain.BaseUseCase +import com.owncloud.android.domain.UseCaseResult +import com.owncloud.android.domain.spaces.model.OCSpace +import com.owncloud.android.domain.spaces.model.SpaceMenuOption + +class FilterSpaceMenuOptionsUseCase( + private val getSpacePermissionsAsyncUseCase: GetSpacePermissionsAsyncUseCase, +) : BaseUseCase, FilterSpaceMenuOptionsUseCase.Params>() { + + override fun run(params: Params): MutableList { + val optionsToShow = mutableListOf() + + val editPermission = if (params.editSpacesPermission) { + true + } else { + when (val spacePermissionsResult = + getSpacePermissionsAsyncUseCase(GetSpacePermissionsAsyncUseCase.Params(params.accountName, params.space.id))) { + is UseCaseResult.Success -> DRIVES_MANAGE_PERMISSION in spacePermissionsResult.data + is UseCaseResult.Error -> false + } + } + + if (editPermission) { + optionsToShow.add(SpaceMenuOption.EDIT) + } + + return optionsToShow + } + + data class Params( + val accountName: String, + val space: OCSpace, + val editSpacesPermission: Boolean + ) + + companion object { + private const val DRIVES_MANAGE_PERMISSION = "libre.graph/driveItem/permissions/update" + } +} diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/usecases/GetSpacePermissionsAsyncUseCase.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/usecases/GetSpacePermissionsAsyncUseCase.kt new file mode 100644 index 00000000000..ba902be1c66 --- /dev/null +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/usecases/GetSpacePermissionsAsyncUseCase.kt @@ -0,0 +1,33 @@ +/** + * ownCloud Android client application + * + * @author Jorge Aguado Recio + * + * Copyright (C) 2025 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.domain.spaces.usecases + +import com.owncloud.android.domain.BaseUseCaseWithResult +import com.owncloud.android.domain.spaces.SpacesRepository + +class GetSpacePermissionsAsyncUseCase( + private val spacesRepository: SpacesRepository +): BaseUseCaseWithResult, GetSpacePermissionsAsyncUseCase.Params>() { + + override fun run(params: Params) = spacesRepository.getSpacePermissions(params.accountName, params.spaceId) + + data class Params(val accountName: String, val spaceId: String) +} diff --git a/owncloudTestUtil/src/main/java/com/owncloud/android/testutil/OCSpace.kt b/owncloudTestUtil/src/main/java/com/owncloud/android/testutil/OCSpace.kt index f5150969a88..c8ac160dec5 100644 --- a/owncloudTestUtil/src/main/java/com/owncloud/android/testutil/OCSpace.kt +++ b/owncloudTestUtil/src/main/java/com/owncloud/android/testutil/OCSpace.kt @@ -302,3 +302,8 @@ val SPACE_RESPONSE = ), special = null, ) + +val SPACE_PERMISSIONS = listOf( + "libre.graph/driveItem/permissions/delete", + "libre.graph/driveItem/permissions/update" +)