Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions changelog/unreleased/4687
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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 -> {
Expand All @@ -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<OCSpace>) {
Expand All @@ -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<SearchView>(R.id.root_toolbar_search_view)
searchViewRootToolbar.queryHint = getString(R.string.actionbar_search_space)
}

private fun showSpaceMenuOptionsDialog(menuOptions: List<SpaceMenuOption>) {
val spaceOptionsBottomSheetBinding = FileOptionsBottomSheetFragmentBinding.inflate(layoutInflater)
val dialog = BottomSheetDialog(requireContext())
dialog.setContentView(spaceOptionsBottomSheetBinding.root)
Expand All @@ -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) {
Expand All @@ -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<SearchView>(R.id.root_toolbar_search_view)
searchViewRootToolbar.queryHint = getString(R.string.actionbar_search_space)
binding.fileOptionsBottomSheetLayout.addView(fileOptionItemView)
}

companion object {
Expand All @@ -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"

Expand Down
Loading
Loading