Skip to content

Commit

Permalink
Organization List View (#762)
Browse files Browse the repository at this point in the history
* OrganizationListView

### What's done:
 * Added new user settings window that shows the list of user's organizations
 * Added new icon for organizations
 * Added new endpoint "/organizations/by-user/not-deleted"

 (#731)
  • Loading branch information
sanyavertolet authored May 16, 2022
1 parent 35e0c57 commit 1eadfbd
Show file tree
Hide file tree
Showing 9 changed files with 161 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import org.cqfn.save.backend.service.LnkUserOrganizationService
import org.cqfn.save.backend.service.OrganizationService
import org.cqfn.save.backend.utils.AuthenticationDetails
import org.cqfn.save.domain.Role
import org.cqfn.save.entities.OrganizationStatus
import org.cqfn.save.info.OrganizationInfo
import org.cqfn.save.info.UserInfo
import org.cqfn.save.permission.Permission
import org.cqfn.save.permission.SetRoleRequest
Expand Down Expand Up @@ -193,6 +195,32 @@ class LnkUserOrganizationController(
)
return (exactMatchUsers + prefixUsers).map { it.toUserInfo() }
}

/**
* Get not deleted organizations that are connected with current user.
*
* @param authentication
* @return Map where key is organization name, value is a pair of avatar and role.
*/
@Suppress("TYPE_ALIAS", "UnsafeCallOnNullableType")
@GetMapping("/by-user/not-deleted")
fun getOrganizationWithRoles(
authentication: Authentication,
): List<OrganizationInfo> {
val selfId = (authentication.details as AuthenticationDetails).id
val user = lnkUserOrganizationService.getUserById(selfId)
.orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND) }
return lnkUserOrganizationService
.getOrganizationsAndRolesByUser(user)
.filter { it.organization?.status != OrganizationStatus.DELETED }
.map {
OrganizationInfo(
it.organization!!.name,
mapOf(user.name!! to (it.role ?: Role.NONE)),
it.organization!!.avatar,
)
}
}
companion object {
const val PAGE_SIZE = 5
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,10 @@ interface LnkUserOrganizationRepository : BaseEntityRepository<LnkUserOrganizati
@Param("user_id") userId: Long,
@Param("role") role: String
)

/**
* @param userId
* @return List of [LnkUserOrganization] in which user with [userId] participates
*/
fun findByUserId(userId: Long): List<LnkUserOrganization>
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ class LnkUserOrganizationService(
* @param organization
* @return role of the [user] in [organization]
*/
@Suppress("KDOC_WITHOUT_PARAM_TAG", "UnsafeCallOnNullableType")
@Suppress("UnsafeCallOnNullableType")
fun getRole(user: User, organization: Organization) = lnkUserOrganizationRepository
.findByUserIdAndOrganization(user.id!!, organization)
?.role
Expand All @@ -99,7 +99,7 @@ class LnkUserOrganizationService(
* @param organization
* @return Unit
*/
@Suppress("KDOC_WITHOUT_PARAM_TAG", "UnsafeCallOnNullableType")
@Suppress("UnsafeCallOnNullableType")
fun removeRole(user: User, organization: Organization) = lnkUserOrganizationRepository
.findByUserIdAndOrganization(user.id!!, organization)
?.id
Expand Down Expand Up @@ -148,4 +148,12 @@ class LnkUserOrganizationService(
val selfOrganizationRole = findRoleByUserIdAndOrganization(selfId, organization)
return getHighestRole(selfOrganizationRole, selfGlobalRole)
}

/**
* @param user
* @return [Organization]s that are connected to the [user]
*/
@Suppress("UnsafeCallOnNullableType")
fun getOrganizationsAndRolesByUser(user: User): List<LnkUserOrganization> =
lnkUserOrganizationRepository.findByUserId(user.id!!)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.cqfn.save.info

import org.cqfn.save.domain.Role

import kotlinx.serialization.Serializable

/**
* Represents all data related to the Organization
*
* @property name organization name
* @property userRoles map that matches usernames and roles
* @property avatar avatar of organization
*/
@Serializable
data class OrganizationInfo(
val name: String,
val userRoles: Map<String, Role> = emptyMap(),
val avatar: String? = null,
)
12 changes: 12 additions & 0 deletions save-frontend/src/main/kotlin/org/cqfn/save/frontend/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import org.cqfn.save.frontend.components.errorModalHandler
import org.cqfn.save.frontend.components.topBar
import org.cqfn.save.frontend.components.views.*
import org.cqfn.save.frontend.components.views.usersettingsview.UserSettingsEmailMenuView
import org.cqfn.save.frontend.components.views.usersettingsview.UserSettingsOrganizationsMenuView
import org.cqfn.save.frontend.components.views.usersettingsview.UserSettingsProfileMenuView
import org.cqfn.save.frontend.components.views.usersettingsview.UserSettingsTokenMenuView
import org.cqfn.save.frontend.externals.fontawesome.*
Expand Down Expand Up @@ -189,6 +190,17 @@ class App : ComponentWithScope<PropsWithChildren, AppState>() {
}
}

Route {
attrs {
path = "/:user/settings/organizations"
element = buildElement {
child(UserSettingsOrganizationsMenuView::class) {
attrs.userName = state.userInfo?.name
}
}
}
}

Route {
attrs {
path = "/creation"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ package org.cqfn.save.frontend.components

import org.cqfn.save.domain.Role
import org.cqfn.save.frontend.components.modal.logoutModal
import org.cqfn.save.frontend.externals.fontawesome.faCog
import org.cqfn.save.frontend.externals.fontawesome.faSignOutAlt
import org.cqfn.save.frontend.externals.fontawesome.fontAwesomeIcon
import org.cqfn.save.frontend.externals.fontawesome.*
import org.cqfn.save.info.UserInfo

import csstype.Width
Expand Down Expand Up @@ -208,6 +206,11 @@ fun topBar() = fc<TopBarProps> { props ->
window.location.href = "#/$name/settings/email"
}
}
dropdownEntry(faCity, "My organizations") {
attrs.onClickFunction = {
window.location.href = "#/$name/settings/organizations"
}
}
}
dropdownEntry(faSignOutAlt, "Log out") {
attrs.onClickFunction = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package org.cqfn.save.frontend.components.views.usersettingsview

import org.cqfn.save.domain.Role
import org.cqfn.save.frontend.components.basic.cardComponent
import org.cqfn.save.v1

import react.FC
import react.dom.*
import react.fc

@Suppress("MISSING_KDOC_TOP_LEVEL", "TOO_LONG_FUNCTION")
class UserSettingsOrganizationsMenuView : UserSettingsView() {
override fun renderMenu(): FC<UserSettingsProps> = fc { props ->
child(cardComponent(isBordered = false, hasBg = true) {
div("d-sm-flex align-items-center justify-content-center mb-4 mt-4") {
h1("h3 mb-0 mt-2 text-gray-800") {
+"Organizations"
}
}

ul(classes = "list-group list-group-flush") {
for (organizationInfo in state.selfOrganizationInfos) {
li("list-group-item") {
div("row justify-content-between align-items-center") {
div("align-items-center ml-3") {
img(classes = "avatar avatar-user width-full border color-bg-default rounded-circle") {
attrs.src = organizationInfo.avatar?.let {
"/api/$v1/avatar$it"
} ?: "img/company.svg"
attrs.height = "60"
attrs.width = "60"
}
a(classes = "ml-2", href = "#/${organizationInfo.name}") {
+organizationInfo.name
}
}
div("mr-3") {
val role = state.userInfo?.name?.let {
organizationInfo.userRoles[it]
} ?: Role.NONE
+role.formattedName
}
}
}
}
}
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,10 @@ package org.cqfn.save.frontend.components.views.usersettingsview
import org.cqfn.save.domain.ImageInfo
import org.cqfn.save.frontend.components.basic.InputTypes
import org.cqfn.save.frontend.components.views.AbstractView
import org.cqfn.save.frontend.externals.fontawesome.faEnvelope
import org.cqfn.save.frontend.externals.fontawesome.faKey
import org.cqfn.save.frontend.externals.fontawesome.faUser
import org.cqfn.save.frontend.externals.fontawesome.fontAwesomeIcon
import org.cqfn.save.frontend.externals.fontawesome.*
import org.cqfn.save.frontend.http.getUser
import org.cqfn.save.frontend.utils.*
import org.cqfn.save.info.OrganizationInfo
import org.cqfn.save.info.UserInfo
import org.cqfn.save.utils.AvatarType
import org.cqfn.save.v1
Expand Down Expand Up @@ -47,7 +45,7 @@ external interface UserSettingsProps : PropsWithChildren {
/**
* [State] of project view component
*/
@Suppress("MISSING_KDOC_TOP_LEVEL")
@Suppress("MISSING_KDOC_TOP_LEVEL", "TYPE_ALIAS")
external interface UserSettingsViewState : State {
/**
* Flag to handle uploading a file
Expand All @@ -68,6 +66,11 @@ external interface UserSettingsViewState : State {
* Token for user
*/
var token: String?

/**
* Organizations connected to user
*/
var selfOrganizationInfos: List<OrganizationInfo>
}

@Suppress("MISSING_KDOC_TOP_LEVEL")
Expand All @@ -76,6 +79,7 @@ abstract class UserSettingsView : AbstractView<UserSettingsProps, UserSettingsVi

init {
state.isUploading = false
state.selfOrganizationInfos = emptyList()
}

/**
Expand All @@ -95,11 +99,14 @@ abstract class UserSettingsView : AbstractView<UserSettingsProps, UserSettingsVi
super.componentDidMount()
scope.launch {
val avatar = getAvatar()
val user = props.userName?.let { getUser(it) }
val user = props.userName
?.let { getUser(it) }
val organizationInfos = getOrganizationInfos()
setState {
image = avatar
userInfo = user
userInfo?.let { updateFieldsMap(it) }
selfOrganizationInfos = organizationInfos
}
}
}
Expand Down Expand Up @@ -130,7 +137,7 @@ abstract class UserSettingsView : AbstractView<UserSettingsProps, UserSettingsVi
}
div("mb-0 font-weight-bold text-gray-800") {
form {
div("row g-3 ml-3 mr-3 pb-2 pt-2 border-bottom") {
div("row g-3 ml-3 mr-3 pb-2 pt-2 border-bottom") {
div("col-md-4 pl-0 pr-0") {
label {
input(type = InputType.file) {
Expand Down Expand Up @@ -196,6 +203,15 @@ abstract class UserSettingsView : AbstractView<UserSettingsProps, UserSettingsVi
+"Email management"
}
}
div("mt-2") {
a(classes = "item", href = "#/${props.userName}/settings/organizations") {
fontAwesomeIcon {
attrs.icon = faCity
attrs.className = "fas fa-sm fa-fw mr-2 text-gray-600"
}
+"Organizations"
}
}
}
}
}
Expand Down Expand Up @@ -282,4 +298,11 @@ abstract class UserSettingsView : AbstractView<UserSettingsProps, UserSettingsVi
.unsafeMap {
it.decodeFromJsonString<ImageInfo>()
}

@Suppress("TYPE_ALIAS")
private suspend fun getOrganizationInfos() = get(
"$apiUrl/organizations/by-user/not-deleted",
Headers(),
)
.unsafeMap { it.decodeFromJsonString<List<OrganizationInfo>>() }
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,4 @@ external val faCog: dynamic
external val faHome: dynamic
external val faEnvelope: dynamic
external val faKey: dynamic
external val faCity: dynamic

0 comments on commit 1eadfbd

Please sign in to comment.