diff --git a/.env.template b/.env.template index 41414d81f5..5ffd90cfc6 100644 --- a/.env.template +++ b/.env.template @@ -31,7 +31,6 @@ FRA_GOOGLE_CLIENT_SECRET=goggle-client-secret FRA_GOOGLE_MAPS_API_KEY=google-maps-api-key #FRA -FRA_ATLANTIS_ALLOWED='[{"assessmentName":"fra","cycleName":"2025"}]' FRA_REPORT_COLLABORATORS_EXCLUDED=[] # Local development mail server diff --git a/package.json b/package.json index 3e4b317c8c..bba1da8a41 100644 --- a/package.json +++ b/package.json @@ -153,7 +153,7 @@ "ts-node-dev": "^2.0.0", "tsc-alias": "^1.8.6", "turndown": "^7.1.1", - "typescript": "^4.6.3", + "typescript": "^5.7.2", "webpack": "^5.71.0", "webpack-bundle-analyzer": "^4.4.2", "webpack-cli": "^4.7.2", diff --git a/src/client/components/AreaSelector/CountryList/hooks/useUserCountryISOs.tsx b/src/client/components/AreaSelector/CountryList/hooks/useUserCountryISOs.tsx index d9abd3d502..acb75c58de 100644 --- a/src/client/components/AreaSelector/CountryList/hooks/useUserCountryISOs.tsx +++ b/src/client/components/AreaSelector/CountryList/hooks/useUserCountryISOs.tsx @@ -1,4 +1,4 @@ -import { CountryIso } from 'meta/area' +import { Areas, CountryIso } from 'meta/area' import { Cycles, CycleUuid } from 'meta/assessment' import { RoleName, Users } from 'meta/user' import { UserRoles } from 'meta/user/userRoles' @@ -37,7 +37,7 @@ export const useUserCountryISOs = (): Record c.countryIso) - .filter((countryIso: CountryIso) => !userCountries?.includes(countryIso)) + .filter((countryIso: CountryIso) => !Areas.isAtlantis(countryIso) && !userCountries?.includes(countryIso)) } } diff --git a/src/client/components/Avatar/Avatar.scss b/src/client/components/Avatar/Avatar.scss new file mode 100644 index 0000000000..bcee4349d5 --- /dev/null +++ b/src/client/components/Avatar/Avatar.scss @@ -0,0 +1,15 @@ +@import 'src/client/style/partials'; + +.user-avatar { + width: 28px; + height: 28px; + border-radius: 50%; + overflow: hidden; + + img { + box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1); + height: 100%; + object-fit: cover; + width: 100%; + } +} diff --git a/src/client/components/Avatar/Avatar.tsx b/src/client/components/Avatar/Avatar.tsx new file mode 100644 index 0000000000..f9b2ba35f2 --- /dev/null +++ b/src/client/components/Avatar/Avatar.tsx @@ -0,0 +1,23 @@ +import './Avatar.scss' +import React from 'react' + +import { CountryUserSummary, User, Users } from 'meta/user' + +type Props = { + user: User | CountryUserSummary +} + +const Avatar: React.FC = (props: Props) => { + const { user } = props + + const alt = 'fullName' in user ? user.fullName : Users.getFullName(user) + const src = Users.profilePictureUri(user.id) + + return ( +
+ {alt} +
+ ) +} + +export default Avatar diff --git a/src/client/components/Avatar/index.ts b/src/client/components/Avatar/index.ts new file mode 100644 index 0000000000..a4600ec776 --- /dev/null +++ b/src/client/components/Avatar/index.ts @@ -0,0 +1 @@ +export { default } from './Avatar' diff --git a/src/client/components/Buttons/Button/Button.scss b/src/client/components/Buttons/Button/Button.scss index 5b63a05e93..5015dfe67b 100644 --- a/src/client/components/Buttons/Button/Button.scss +++ b/src/client/components/Buttons/Button/Button.scss @@ -112,6 +112,10 @@ } } + &.noBorder { + border: unset; + } + &.inverse { background-color: $colorSecondary; color: $colorPrimary; @@ -153,7 +157,7 @@ } .button__type-danger { - @include withTheme($ui-destructive, 58%); + @include withTheme(darken($ui-destructive, 5%), 58%); } .button__type-primary { diff --git a/src/client/components/Buttons/Button/hooks/useButtonClassName.ts b/src/client/components/Buttons/Button/hooks/useButtonClassName.ts index 3aefb5678e..9ccde57a2d 100644 --- a/src/client/components/Buttons/Button/hooks/useButtonClassName.ts +++ b/src/client/components/Buttons/Button/hooks/useButtonClassName.ts @@ -5,7 +5,15 @@ import classNames from 'classnames' import { ButtonProps, ButtonSize, ButtonType } from 'client/components/Buttons/Button/types' export const useButtonClassName = (props: ButtonProps): string => { - const { className, disabled, inverse, noPrint = true, size = ButtonSize.s, type = ButtonType.primary } = props + const { + className, + disabled, + inverse, + noBorder = false, + noPrint = true, + size = ButtonSize.s, + type = ButtonType.primary, + } = props return useMemo(() => { return classNames( @@ -15,7 +23,8 @@ export const useButtonClassName = (props: ButtonProps): string => { `button__type-${type}`, { inverse }, { disabled }, + { noBorder }, className ) - }, [className, disabled, inverse, noPrint, size, type]) + }, [className, disabled, inverse, noBorder, noPrint, size, type]) } diff --git a/src/client/components/Buttons/Button/types.ts b/src/client/components/Buttons/Button/types.ts index 597921b1d2..0620157708 100644 --- a/src/client/components/Buttons/Button/types.ts +++ b/src/client/components/Buttons/Button/types.ts @@ -22,4 +22,5 @@ export type ButtonProps = Pick, 'classNam noPrint?: boolean size?: ButtonSize type?: ButtonType + noBorder?: boolean } diff --git a/src/client/components/Dashboard/Dashboard.scss b/src/client/components/Dashboard/Dashboard.scss index a0d770788e..6e3cb38386 100644 --- a/src/client/components/Dashboard/Dashboard.scss +++ b/src/client/components/Dashboard/Dashboard.scss @@ -1,15 +1,35 @@ @import 'src/client/style/partials'; +$border: 1px dotted $ui-border; + .dashboard { + background-color: $ui-bg-light; + border-left: $border; + border-radius: 2px; + border-right: $border; + border-top: $border; display: grid; - grid-column-gap: $spacing-xs; - grid-row-gap: $spacing-xs; grid-template-columns: 1fr; overflow: hidden; @include min-width($laptop) { grid-template-columns: repeat(2, minmax(0, 1fr)); - padding-top: $spacing-s; + margin-top: $spacing-s; + + > .dashboard-item:nth-child(odd) { + border-right: $border; + } + } +} + +@include rtl { + .dashboard { + @include min-width($laptop) { + > .dashboard-item:nth-child(odd) { + border-left: $border; + border-right: unset; + } + } } } @@ -18,23 +38,16 @@ align-items: start; display: grid; overflow: hidden; + padding: $spacing-xs $spacing-s; + border-bottom: $border; .header { - border-radius: 2px; - border: 1px solid $ui-border; - padding: 6px; - h3 { - background-color: $ui-bg; - border-radius: 2px; - font-size: $font-m; - padding: 6px $spacing-xxs; - text-transform: uppercase; + font-size: $font-m + 1px; } } > :nth-child(2) { - padding: 0 $spacing-xs; overflow: hidden; } diff --git a/src/client/components/EditUserForm/CountryRoles/CountryRoles.tsx b/src/client/components/EditUserForm/CountryRoles/CountryRoles.tsx index 1f4c605aca..9a23a5424f 100644 --- a/src/client/components/EditUserForm/CountryRoles/CountryRoles.tsx +++ b/src/client/components/EditUserForm/CountryRoles/CountryRoles.tsx @@ -17,6 +17,10 @@ import CountrySelectModal from 'client/components/CountrySelectModal' import CountryRole from './CountryRole' +/* + This component is used by admins when setting or removing multiple roles + */ + // properties used to render ui form fields const roles = [ RoleName.REVIEWER, @@ -33,6 +37,10 @@ type ModalOptionsProps = { role: RoleName | null } +/** + * @deprecated + * Deprecation notice: EditUserForm is scheduled for refactor in #3810 + */ const CountryRoles: React.FC<{ user: User }> = ({ user }) => { const dispatch = useAppDispatch() const { t } = useTranslation() @@ -58,7 +66,7 @@ const CountryRoles: React.FC<{ user: User }> = ({ user }) => { (countryIso): Partial> => ({ countryIso: countryIso as CountryIso, role, - assessmentId: assessment.id, + assessmentUuid: assessment.uuid, cycleUuid: cycle.uuid, }) ) @@ -70,16 +78,16 @@ const CountryRoles: React.FC<{ user: User }> = ({ user }) => { assessmentName: assessment.props.name, cycleName: cycle.name, roles, - userId: user.id, + userUuid: user.uuid, }) ) }, - [assessment.id, assessment.props.name, cycle.name, cycle.uuid, dispatch, user.id, user.roles] + [assessment.props.name, assessment.uuid, cycle.name, cycle.uuid, dispatch, user.roles, user.uuid] ) const _toggleAdmin = useCallback(() => { if (window.confirm(t(Users.isAdministrator(user) ? 'editUser.demoteToUser' : 'editUser.promoteToAdmin'))) { - dispatch(UserManagementActions.updateUserAdminRole({ userId: user.id })) + dispatch(UserManagementActions.updateUserAdminRole({ userUuid: user.uuid })) } }, [dispatch, t, user]) @@ -121,9 +129,9 @@ const CountryRoles: React.FC<{ user: User }> = ({ user }) => { {Users.isAdministrator(userInfo) && ( ) diff --git a/src/client/components/EditUserForm/EditUserForm.tsx b/src/client/components/EditUserForm/EditUserForm.tsx index eca3be6194..530c655be9 100644 --- a/src/client/components/EditUserForm/EditUserForm.tsx +++ b/src/client/components/EditUserForm/EditUserForm.tsx @@ -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) => { - const { targetUser, canEditPermissions, canEditRoles, canEditUser } = props + const { canEditPermissions, canEditRoles, canEditUser, targetUser } = props const dispatch = useAppDispatch() const assessment = useAssessment() const countryIso = useCountryIso() @@ -81,45 +81,49 @@ const EditUserForm: React.FC = (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 (
setProfilePicture(profilePicture)} userId={targetUser.id} - enabled={enabled} /> - + - {showRoleSelector && } + {showRoleSelector && } {[RoleName.NATIONAL_CORRESPONDENT, RoleName.ALTERNATE_NATIONAL_CORRESPONDENT, RoleName.COLLABORATOR].includes( userRole?.role ) && - roleToEdit && } + roleToEdit && }
{t('editUser.mandatoryFields')}
@@ -128,7 +132,7 @@ const EditUserForm: React.FC = (props: Props) => { )} {canEditRoles && } - {isAdmin && } + {isAdmin && }
) } diff --git a/src/client/components/EditUserForm/TextInputField/TextInputField.tsx b/src/client/components/EditUserForm/TextInputField/TextInputField.tsx index 6fb8acce0f..c67f189aea 100644 --- a/src/client/components/EditUserForm/TextInputField/TextInputField.tsx +++ b/src/client/components/EditUserForm/TextInputField/TextInputField.tsx @@ -43,7 +43,9 @@ const TextInputField: React.FC = (props) => { error: !valid, })} > - {editorLink && onChange(name, _value)} value={value} />} + {editorLink && ( + onChange(name, _value)} value={value} /> + )} {!editorLink && ( = (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 + return } export default UserCountryRoleSelector diff --git a/src/client/components/EditUserForm/UserCountryRoleSelector/hooks/useOnChange.ts b/src/client/components/EditUserForm/UserCountryRoleSelector/hooks/useOnChange.ts index d9388e5880..301419ab82 100644 --- a/src/client/components/EditUserForm/UserCountryRoleSelector/hooks/useOnChange.ts +++ b/src/client/components/EditUserForm/UserCountryRoleSelector/hooks/useOnChange.ts @@ -25,7 +25,7 @@ export const useOnChange = (user: User): Returned => { return userRole }) - const params = { assessmentName, cycleName, roles, userId: user.id } + const params = { assessmentName, cycleName, roles, userUuid: user.uuid } dispatch(UserManagementActions.updateUserRoles(params)) }, [assessmentName, countryIso, cycle, cycleName, dispatch, user] diff --git a/src/client/components/EditUserForm/UserCountryRoleSelector/hooks/useOptions.tsx b/src/client/components/EditUserForm/UserCountryRoleSelector/hooks/useOptions.tsx index c80045a824..e10e329e07 100644 --- a/src/client/components/EditUserForm/UserCountryRoleSelector/hooks/useOptions.tsx +++ b/src/client/components/EditUserForm/UserCountryRoleSelector/hooks/useOptions.tsx @@ -1,26 +1,26 @@ import { useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { RoleName, Users } from 'meta/user' +import { 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