Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

4032 - Collaborators/User edit final fixes #4187

Merged
merged 9 commits into from
Dec 18, 2024
38 changes: 21 additions & 17 deletions src/client/components/EditUserForm/EditUserForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ import TextInputField from './TextInputField'
import UserRolePropsFields from './UserRolePropsFields'

type Props = {
targetUser: User
canEditPermissions?: boolean
canEditRoles?: boolean
canEditUser?: boolean
targetUser: User
}

const EditUserForm: React.FC<Props> = (props: Props) => {
const { targetUser, canEditPermissions, canEditRoles, canEditUser } = props
const { canEditPermissions, canEditRoles, canEditUser, targetUser } = props
const dispatch = useAppDispatch()
const assessment = useAssessment()
const countryIso = useCountryIso()
Expand Down Expand Up @@ -81,45 +81,49 @@ const EditUserForm: React.FC<Props> = (props: Props) => {
const enabled = canEditUser
const isAdmin = Users.isAdministrator(user)
const isTargetAdmin = Users.isAdministrator(targetUser)
const showRoleSelector = !Areas.isGlobal(countryIso) && isAdmin && !isTargetAdmin
const isSelf = user.id === targetUser.id
const showRoleSelector =
!Areas.isGlobal(countryIso) &&
!isSelf &&
((isAdmin && !isTargetAdmin) || Users.getRolesAllowedToEdit({ user, cycle, countryIso }).length > 0)

return (
<div className="edit-user__form-container">
<ProfilePicture
enabled={enabled}
onChange={(profilePicture: File) => setProfilePicture(profilePicture)}
userId={targetUser.id}
enabled={enabled}
/>
<TextInputField
enabled={Users.isAdministrator(user)}
mandatory
name="email"
value={targetUser.email}
onChange={changeUser}
validator={Users.validEmailField}
enabled={Users.isAdministrator(user)}
mandatory
value={targetUser.email}
/>
<SelectField
enabled={enabled}
mandatory
name="title"
value={targetUser.props.title}
onChange={changeUserProp}
options={appellationOptions}
enabled={enabled}
mandatory
value={targetUser.props.title}
/>
<TextInputField name="name" value={targetUser.props.name} onChange={changeUserProp} enabled={enabled} mandatory />
<TextInputField enabled={enabled} mandatory name="name" onChange={changeUserProp} value={targetUser.props.name} />
<TextInputField
name="surname"
value={targetUser.props.surname}
onChange={changeUserProp}
enabled={enabled}
mandatory
name="surname"
onChange={changeUserProp}
value={targetUser.props.surname}
/>
{showRoleSelector && <UserCountryRoleSelector user={targetUser} enabled={enabled} />}
{showRoleSelector && <UserCountryRoleSelector enabled={enabled} target={targetUser} />}

{[RoleName.NATIONAL_CORRESPONDENT, RoleName.ALTERNATE_NATIONAL_CORRESPONDENT, RoleName.COLLABORATOR].includes(
userRole?.role
) &&
roleToEdit && <UserRolePropsFields role={roleToEdit} onChange={changeUserRoleProp} enabled={enabled} />}
roleToEdit && <UserRolePropsFields enabled={enabled} onChange={changeUserRoleProp} role={roleToEdit} />}
<div className="edit-user__form-item">
<div className="edit-user__form-label">{t('editUser.mandatoryFields')}</div>
</div>
Expand All @@ -128,7 +132,7 @@ const EditUserForm: React.FC<Props> = (props: Props) => {
)}
{canEditRoles && <CountryRoles user={targetUser} />}

{isAdmin && <DisableUser user={targetUser} changeUser={changeUser} />}
{isAdmin && <DisableUser changeUser={changeUser} user={targetUser} />}
</div>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ const TextInputField: React.FC<Props> = (props) => {
error: !valid,
})}
>
{editorLink && <EditorWYSIWYGLinks onChange={(_value) => onChange(name, _value)} value={value} />}
{editorLink && (
<EditorWYSIWYGLinks disabled={!enabled} onChange={(_value) => onChange(name, _value)} value={value} />
)}

{!editorLink && (
<input
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,26 @@ import { User, Users } from 'meta/user'
import { useCycle } from 'client/store/assessment'
import { useCountryRouteParams } from 'client/hooks/useRouteParams'
import SelectField from 'client/components/EditUserForm/SelectField'
import { useOnChange } from 'client/components/EditUserForm/UserCountryRoleSelector/hooks/useOnChange'
import { useOptions } from 'client/components/EditUserForm/UserCountryRoleSelector/hooks/useOptions'

import { useOnChange } from './hooks/useOnChange'
import { useOptions } from './hooks/useOptions'

type Props = {
enabled: boolean
user: User
target: User
}

const UserCountryRoleSelector: React.FC<Props> = (props: Props) => {
const { enabled, user } = props
const { enabled, target } = props
const { countryIso } = useCountryRouteParams()
const cycle = useCycle()

const userRole = Users.getRole(user, countryIso, cycle)
const userRole = Users.getRole(target, countryIso, cycle)

const options = useOptions()
const onChange = useOnChange(user)
const onChange = useOnChange(target)

return <SelectField onChange={onChange} name="role" options={options} value={userRole.role} enabled={enabled} />
return <SelectField enabled={enabled} name="role" onChange={onChange} options={options} value={userRole.role} />
}

export default UserCountryRoleSelector
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,25 @@ import { useTranslation } from 'react-i18next'

import { RoleName, Users } from 'meta/user'

import { useCycle } from 'client/store/assessment'
import { useUser } from 'client/store/user'
import { useCountryRouteParams } from 'client/hooks/useRouteParams'
import { Option } from 'client/components/Inputs/Select'

type Returned = Array<Option>

export const useOptions = (): Returned => {
const { t } = useTranslation()
const { countryIso } = useCountryRouteParams()
const cycle = useCycle()
const user = useUser()

return useMemo<Returned>(() => {
const options = Object.keys(RoleName).reduce<Returned>((acc, key) => {
const roles = Users.isAdministrator(user)
? Object.keys(RoleName)
: Users.getRolesAllowedToEdit({ user, countryIso, cycle })

const options = roles.reduce<Returned>((acc, key) => {
if (key !== RoleName.ADMINISTRATOR) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems User.getRolesAllowedToEdit also handles the admin user. It returns every role but administrator. So you can use User.getRolesAllowedToEdit instead of Rolename and then the check at line 25 can be removed too

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch 😃. update pushed

acc.push({
label: t(Users.getI18nRoleLabelKey(key)),
Expand All @@ -22,5 +32,5 @@ export const useOptions = (): Returned => {
}, [])

return options
}, [t])
}, [countryIso, cycle, t, user])
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,4 @@
width: 1px;
height: 16px;
}

.btn-message__count {
padding-top: 10px;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Link } from 'react-router-dom'

import { CountryIso } from 'meta/area'
import { Routes } from 'meta/routes'
import { Users } from 'meta/user'
import { Authorizer } from 'meta/user'
import { CountryUserSummaries } from 'meta/user/countryUserSummaries'

import { useCycle } from 'client/store/assessment'
Expand All @@ -27,9 +27,9 @@ const Edit: React.FC<Props> = (props: Props) => {
const currentUser = useUser()
const cycle = useCycle()

const currentUserIsReviewer = Users.isReviewer(currentUser, countryIso, cycle)
const label = t(currentUserIsReviewer ? 'common.view' : 'userManagement.edit')
const iconName = currentUserIsReviewer ? 'icon-eye' : 'pencil'
const isEdit = Authorizer.canEditUser({ cycle, countryIso, user: currentUser, target: user })
const label = t(isEdit ? 'userManagement.edit' : 'common.view')
const iconName = isEdit ? 'pencil' : 'icon-eye'
const className = useButtonClassName({ iconName, label, size, type, className: 'home-user-action-button-edit' })

if (CountryUserSummaries.isInvitation(user, countryIso)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React from 'react'
import React, { useMemo } from 'react'

import { CountryIso } from 'meta/area'
import { UserInvitations, Users } from 'meta/user'
import { Authorizer, UserInvitations, Users } from 'meta/user'
import { CountryUserSummaries } from 'meta/user/countryUserSummaries'

import { useCycle } from 'client/store/assessment'
import { useCanSeeUserActivities, useUser } from 'client/store/user'
import { useCanEditUserActivities } from 'client/store/user/hooks'
import { useCountryRouteParams } from 'client/hooks/useRouteParams'
Expand All @@ -24,6 +25,7 @@ export const useActions = (props: Props): Array<ActionType> => {
const { user } = props

const { countryIso } = useCountryRouteParams<CountryIso>()
const cycle = useCycle()
const currentUser = useUser()
const canCurrentUserEditActivities = useCanEditUserActivities(currentUser)
const canCurrentUserViewActivities = useCanSeeUserActivities(currentUser)
Expand All @@ -33,32 +35,47 @@ export const useActions = (props: Props): Array<ActionType> => {
const isInvitation = CountryUserSummaries.isInvitation(user, countryIso)
const expired = invitation && UserInvitations.isExpired(invitation)

// List of actions available to the user
const actions: Array<ActionType> = []
return useMemo<Array<ActionType>>(() => {
// List of actions available to the user
const actions: Array<ActionType> = []

// Reviewer cannot access invitation actions
if (isInvitation && canCurrentUserEditActivities) {
actions.push({ name: 'resend', Component: Resend })
// Allow copying the link only when the invitation is not expired
if (!expired) {
actions.push({ name: 'copy', Component: CopyLink })
// Reviewer cannot access invitation actions
if (isInvitation && canCurrentUserEditActivities) {
actions.push({ name: 'resend', Component: Resend })
// Allow copying the link only when the invitation is not expired
if (!expired) {
actions.push({ name: 'copy', Component: CopyLink })
}
actions.push({ name: 'remove', Component: Remove })
}
actions.push({ name: 'remove', Component: Remove })
}

if (!isInvitation) {
// If the user or target cannot send/receive messages, hide message button
const canSendOrReceiveMessage = canCurrentUserViewActivities && canTargetUserViewActivities
// If viewing self, return hide message button
const isSelf = user.uuid === currentUser.uuid
if (!isInvitation) {
// If the user or target cannot send/receive messages, hide message button
const canSendOrReceiveMessage = canCurrentUserViewActivities && canTargetUserViewActivities
// If viewing self, return hide message button
const isSelf = user.uuid === currentUser.uuid

if (canSendOrReceiveMessage && !isSelf) {
actions.push({ name: 'message', Component: Message })
}
if (canSendOrReceiveMessage && !isSelf) {
actions.push({ name: 'message', Component: Message })
}

if (Users.isAdministrator(currentUser) || isSelf) {
actions.push({ name: 'edit', Component: Edit })
if (
Authorizer.canEditUser({ countryIso, cycle, user: currentUser, target: user }) ||
Users.isReviewer(currentUser, countryIso, cycle)
) {
actions.push({ name: 'edit', Component: Edit })
}
}
}
return actions
return actions
}, [
canCurrentUserEditActivities,
canCurrentUserViewActivities,
canTargetUserViewActivities,
countryIso,
currentUser,
cycle,
expired,
isInvitation,
user,
])
}
3 changes: 1 addition & 2 deletions src/client/pages/CountryHome/CountryHeader/CountryHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next'
import { NavLink } from 'react-router-dom'

import classNames from 'classnames'
import { Objects } from 'utils/objects'

import { Areas } from 'meta/area'
import { MessageTopicType, Topics } from 'meta/messageCenter'
Expand Down Expand Up @@ -42,7 +41,7 @@ const CountryHeader: React.FC<Props> = (props) => {
)

const withTabs = useMemo<boolean>(
() => !Objects.isEmpty(sections) && Areas.isISOCountry(countryIso),
() => Areas.isISOCountry(countryIso) && sections?.length > 1,
[countryIso, sections]
)

Expand Down
4 changes: 3 additions & 1 deletion src/client/pages/CountryHome/FraHome/hooks/useSections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useMemo } from 'react'
import { Areas } from 'meta/area'
import { Cycles } from 'meta/assessment'
import { SectionNames } from 'meta/routes'
import { Users } from 'meta/user'

import { useCycle } from 'client/store/assessment'
import { useCanSeeUserActivities, useUser } from 'client/store/user'
Expand All @@ -26,12 +27,13 @@ export const useSections = (): Array<CountryHomeSection> => {
if (!cycle) return null
const isCountry = Areas.isISOCountry(countryIso)
const showOverview = Cycles.isPublished(cycle) || Areas.isISOCountry(countryIso)
const hasRoleInCountry = user && isCountry && Users.hasRoleInCountry({ countryIso, cycle, user })

if (showOverview) {
sections.push({ name: SectionNames.Country.Home.overview, component: Overview })
}

if (user && isCountry) {
if (hasRoleInCountry) {
sections.push({ name: SectionNames.Country.Home.repository, component: Repository })
}

Expand Down
25 changes: 25 additions & 0 deletions src/meta/user/authorizer/canEditUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { CountryIso } from 'meta/area'
import { Cycle } from 'meta/assessment'
import { User } from 'meta/user/user'
import { Users } from 'meta/user/users'

type Props = { countryIso: CountryIso; cycle: Cycle; user: User; target: User }

export const canEditUserRole = (props: Props) => {
const { countryIso, cycle, user, target } = props

if (Users.isAdministrator(user)) return true
if (user.id === target.id) return false

const rolesAllowedToEdit = Users.getRolesAllowedToEdit({ user, countryIso, cycle })
return rolesAllowedToEdit.includes(Users.getRole(target, countryIso, cycle)?.role)
}

export const canEditUser = (props: Props) => {
const { user, target } = props

if (Users.isAdministrator(user)) return true
if (user.id === target.id) return true

return canEditUserRole(props)
}
4 changes: 4 additions & 0 deletions src/meta/user/authorizer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { User } from 'meta/user/user'
import { Collaborator, CollaboratorEditPropertyType } from 'meta/user/userRole'
import { Users } from 'meta/user/users'

import { canEditUser, canEditUserRole } from './canEditUser'
import { canViewReview } from './canViewReview'

/**
Expand Down Expand Up @@ -224,5 +225,8 @@ export const Authorizer = {
canViewHistory,
canViewRepositoryItem,
canViewReview,
// user
canViewUsers,
canEditUser,
canEditUserRole,
}
Loading