mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-24 10:46:30 -05:00
fix(permissions): show guest avatar when note owner is anonymous
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
parent
62dfe4df72
commit
ebf8e3a759
7 changed files with 127 additions and 11 deletions
|
@ -0,0 +1,16 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`GuestUserAvatar renders the guest user avatar correctly 1`] = `
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
class="d-inline-flex align-items-center "
|
||||||
|
>
|
||||||
|
BootstrapIconMock_Person
|
||||||
|
<span
|
||||||
|
class="ms-2 me-1 user-line-name"
|
||||||
|
>
|
||||||
|
common.guestUser
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
`;
|
|
@ -105,6 +105,40 @@ exports[`UserAvatar renders the user avatar in size sm 1`] = `
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`UserAvatar uses custom photo component if provided 1`] = `
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
class="d-inline-flex align-items-center "
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
Custom Photo
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="ms-2 me-1 user-line-name"
|
||||||
|
>
|
||||||
|
test
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`UserAvatar uses custom photo component preferred over photoUrl 1`] = `
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
class="d-inline-flex align-items-center "
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
Custom Photo
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="ms-2 me-1 user-line-name"
|
||||||
|
>
|
||||||
|
test
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`UserAvatar uses identicon when empty photoUrl is given 1`] = `
|
exports[`UserAvatar uses identicon when empty photoUrl is given 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<span
|
<span
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { mockI18n } from '../../../test-utils/mock-i18n'
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import { GuestUserAvatar } from './guest-user-avatar'
|
||||||
|
|
||||||
|
jest.mock('@dicebear/identicon', () => null)
|
||||||
|
jest.mock('@dicebear/core', () => ({
|
||||||
|
createAvatar: jest.fn(() => ({
|
||||||
|
toDataUri: jest.fn(() => 'data:image/x-other,identicon-mock')
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('GuestUserAvatar', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await mockI18n()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the guest user avatar correctly', () => {
|
||||||
|
const view = render(<GuestUserAvatar />)
|
||||||
|
expect(view.container).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import React from 'react'
|
||||||
|
import type { UserAvatarProps } from './user-avatar'
|
||||||
|
import { UserAvatar } from './user-avatar'
|
||||||
|
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
|
||||||
|
import { Person as IconPerson } from 'react-bootstrap-icons'
|
||||||
|
|
||||||
|
export type GuestUserAvatarProps = Omit<UserAvatarProps, 'displayName' | 'photoUrl' | 'username'>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The avatar component for an anonymous user.
|
||||||
|
* @param props The properties of the guest user avatar ({@link UserAvatarProps})
|
||||||
|
*/
|
||||||
|
export const GuestUserAvatar: React.FC<GuestUserAvatarProps> = (props) => {
|
||||||
|
const label = useTranslatedText('common.guestUser')
|
||||||
|
return <UserAvatar displayName={label} photoComponent={<IconPerson />} {...props} />
|
||||||
|
}
|
|
@ -59,4 +59,16 @@ describe('UserAvatar', () => {
|
||||||
const view = render(<UserAvatar displayName={'test'} photoUrl={''} />)
|
const view = render(<UserAvatar displayName={'test'} photoUrl={''} />)
|
||||||
expect(view.container).toMatchSnapshot()
|
expect(view.container).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('uses custom photo component if provided', () => {
|
||||||
|
const view = render(<UserAvatar displayName={'test'} photoComponent={<div>Custom Photo</div>} />)
|
||||||
|
expect(view.container).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses custom photo component preferred over photoUrl', () => {
|
||||||
|
const view = render(
|
||||||
|
<UserAvatar displayName={'test'} photoComponent={<div>Custom Photo</div>} photoUrl={user.photoUrl} />
|
||||||
|
)
|
||||||
|
expect(view.container).toMatchSnapshot()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -15,6 +15,7 @@ export interface UserAvatarProps {
|
||||||
photoUrl?: string
|
photoUrl?: string
|
||||||
displayName: string
|
displayName: string
|
||||||
username?: string | null
|
username?: string | null
|
||||||
|
photoComponent?: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -24,6 +25,8 @@ export interface UserAvatarProps {
|
||||||
* @param size The size in which the user image should be shown.
|
* @param size The size in which the user image should be shown.
|
||||||
* @param additionalClasses Additional CSS classes that will be added to the container.
|
* @param additionalClasses Additional CSS classes that will be added to the container.
|
||||||
* @param showName true when the name should be displayed alongside the image, false otherwise. Defaults to true.
|
* @param showName true when the name should be displayed alongside the image, false otherwise. Defaults to true.
|
||||||
|
* @param username The username to use for generating the fallback avatar image.
|
||||||
|
* @param photoComponent A custom component to use as the user's photo.
|
||||||
*/
|
*/
|
||||||
export const UserAvatar: React.FC<UserAvatarProps> = ({
|
export const UserAvatar: React.FC<UserAvatarProps> = ({
|
||||||
photoUrl,
|
photoUrl,
|
||||||
|
@ -31,7 +34,8 @@ export const UserAvatar: React.FC<UserAvatarProps> = ({
|
||||||
size,
|
size,
|
||||||
additionalClasses = '',
|
additionalClasses = '',
|
||||||
showName = true,
|
showName = true,
|
||||||
username
|
username,
|
||||||
|
photoComponent
|
||||||
}) => {
|
}) => {
|
||||||
const imageSize = useMemo(() => {
|
const imageSize = useMemo(() => {
|
||||||
switch (size) {
|
switch (size) {
|
||||||
|
@ -56,15 +60,17 @@ export const UserAvatar: React.FC<UserAvatarProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={'d-inline-flex align-items-center ' + additionalClasses}>
|
<span className={'d-inline-flex align-items-center ' + additionalClasses}>
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
{photoComponent ?? (
|
||||||
<img
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
src={avatarUrl}
|
<img
|
||||||
className={`rounded ${styles['user-image']}`}
|
src={avatarUrl}
|
||||||
alt={imgDescription}
|
className={`rounded ${styles['user-image']}`}
|
||||||
title={imgDescription}
|
alt={imgDescription}
|
||||||
height={imageSize}
|
title={imgDescription}
|
||||||
width={imageSize}
|
height={imageSize}
|
||||||
/>
|
width={imageSize}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{showName && <span className={`ms-2 me-1 ${styles['user-line-name']}`}>{displayName}</span>}
|
{showName && <span className={`ms-2 me-1 ${styles['user-line-name']}`}>{displayName}</span>}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
|
|
|
@ -11,6 +11,7 @@ import type { PermissionDisabledProps } from './permission-disabled.prop'
|
||||||
import React, { Fragment } from 'react'
|
import React, { Fragment } from 'react'
|
||||||
import { Button } from 'react-bootstrap'
|
import { Button } from 'react-bootstrap'
|
||||||
import { Pencil as IconPencil } from 'react-bootstrap-icons'
|
import { Pencil as IconPencil } from 'react-bootstrap-icons'
|
||||||
|
import { GuestUserAvatar } from '../../../../../common/user-avatar/guest-user-avatar'
|
||||||
|
|
||||||
export interface PermissionOwnerInfoProps {
|
export interface PermissionOwnerInfoProps {
|
||||||
onEditOwner: () => void
|
onEditOwner: () => void
|
||||||
|
@ -30,7 +31,7 @@ export const PermissionOwnerInfo: React.FC<PermissionOwnerInfoProps & Permission
|
||||||
const buttonTitle = useTranslatedText('editor.modal.permissions.ownerChange.button')
|
const buttonTitle = useTranslatedText('editor.modal.permissions.ownerChange.button')
|
||||||
|
|
||||||
if (!noteOwner) {
|
if (!noteOwner) {
|
||||||
return null
|
return <GuestUserAvatar />
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
Loading…
Reference in a new issue