mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-06-01 23:58:58 -04:00
refactor(frontend): switch to DTOs from @hedgedoc/commons
Co-authored-by: Philip Molares <philip.molares@udo.edu> Signed-off-by: Philip Molares <philip.molares@udo.edu> Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
parent
deee8e885f
commit
e411ddf099
121 changed files with 620 additions and 819 deletions
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
@ -20,7 +20,7 @@ describe('Copy to clipboard button', () => {
|
|||
beforeAll(async () => {
|
||||
await mockI18n()
|
||||
originalClipboard = window.navigator.clipboard
|
||||
jest.spyOn(uuidModule, 'v4').mockReturnValue(uuidMock)
|
||||
jest.spyOn(uuidModule, 'v4').mockReturnValue(Buffer.from(uuidMock))
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { FrontendConfig } from '../../../api/config/types'
|
||||
import type { FrontendConfigDto } from '@hedgedoc/commons'
|
||||
import * as UseFrontendConfigMock from '../frontend-config-context/use-frontend-config'
|
||||
import { CustomBranding } from './custom-branding'
|
||||
import { render } from '@testing-library/react'
|
||||
|
@ -12,10 +12,10 @@ import { Mock } from 'ts-mockery'
|
|||
jest.mock('../frontend-config-context/use-frontend-config')
|
||||
|
||||
describe('custom branding', () => {
|
||||
const mockFrontendConfigHook = (logo?: string, name?: string) => {
|
||||
const mockFrontendConfigHook = (logo: string | null = null, name: string | null = null) => {
|
||||
jest
|
||||
.spyOn(UseFrontendConfigMock, 'useFrontendConfig')
|
||||
.mockReturnValue(Mock.of<FrontendConfig>({ branding: { logo, name } }))
|
||||
.mockReturnValue(Mock.of<FrontendConfigDto>({ branding: { logo, name } }))
|
||||
}
|
||||
|
||||
it("doesn't show anything if no branding is defined", () => {
|
||||
|
@ -32,7 +32,7 @@ describe('custom branding', () => {
|
|||
})
|
||||
|
||||
it('shows an text if branding text is defined', () => {
|
||||
mockFrontendConfigHook(undefined, 'mockedBranding')
|
||||
mockFrontendConfigHook(null, 'mockedBranding')
|
||||
const view = render(<CustomBranding inline={inline} />)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
@ -36,7 +36,12 @@ export const CustomBranding: React.FC<BrandingProps> = ({ inline = false }) => {
|
|||
} else if (branding.logo) {
|
||||
return (
|
||||
/* eslint-disable-next-line @next/next/no-img-element */
|
||||
<img src={branding.logo} alt={branding.name} title={branding.name} className={className} />
|
||||
<img
|
||||
src={branding.logo}
|
||||
alt={branding.name !== null ? branding.name : undefined}
|
||||
title={branding.name !== null ? branding.name : undefined}
|
||||
className={className}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
return <span className={className}>{branding.name}</span>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { BrandingConfig } from '../../../api/config/types'
|
||||
import type { BrandingDto } from '@hedgedoc/commons'
|
||||
import { useFrontendConfig } from '../frontend-config-context/use-frontend-config'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
|
@ -12,10 +12,10 @@ import { useMemo } from 'react'
|
|||
*
|
||||
* @return the branding configuration or null if no branding has been configured
|
||||
*/
|
||||
export const useBrandingDetails = (): null | BrandingConfig => {
|
||||
export const useBrandingDetails = (): null | BrandingDto => {
|
||||
const branding = useFrontendConfig().branding
|
||||
|
||||
return useMemo(() => {
|
||||
return !branding.name && !branding.logo ? null : branding
|
||||
return branding.name === null && branding.logo === null ? null : branding
|
||||
}, [branding])
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
@ -18,7 +18,7 @@ export enum ProfilePictureChoice {
|
|||
export interface ProfilePictureSelectFieldProps extends CommonFieldProps<ProfilePictureChoice> {
|
||||
onChange: (choice: ProfilePictureChoice) => void
|
||||
value: ProfilePictureChoice
|
||||
pictureUrl?: string
|
||||
photoUrl: string | null
|
||||
username: string
|
||||
}
|
||||
|
||||
|
@ -31,11 +31,15 @@ export interface ProfilePictureSelectFieldProps extends CommonFieldProps<Profile
|
|||
*/
|
||||
export const ProfilePictureSelectField: React.FC<ProfilePictureSelectFieldProps> = ({
|
||||
onChange,
|
||||
pictureUrl,
|
||||
photoUrl,
|
||||
username,
|
||||
value
|
||||
}) => {
|
||||
const fallbackUrl = useAvatarUrl(undefined, username)
|
||||
const fallbackUrl = useAvatarUrl({
|
||||
username,
|
||||
photoUrl,
|
||||
displayName: username
|
||||
})
|
||||
const profileEditsAllowed = useFrontendConfig().allowProfileEdits
|
||||
const onSetProviderPicture = useCallback(() => {
|
||||
if (value !== ProfilePictureChoice.PROVIDER) {
|
||||
|
@ -57,7 +61,7 @@ export const ProfilePictureSelectField: React.FC<ProfilePictureSelectFieldProps>
|
|||
<Form.Label>
|
||||
<Trans i18nKey='profile.selectProfilePicture.title' />
|
||||
</Form.Label>
|
||||
{pictureUrl && (
|
||||
{photoUrl && (
|
||||
<Form.Check className={'d-flex gap-2 align-items-center mb-3'} type='radio'>
|
||||
<Form.Check.Input
|
||||
type={'radio'}
|
||||
|
@ -66,7 +70,7 @@ export const ProfilePictureSelectField: React.FC<ProfilePictureSelectFieldProps>
|
|||
/>
|
||||
<Form.Check.Label>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={pictureUrl} alt={'Profile picture provided by the identity provider'} height={48} width={48} />
|
||||
<img src={photoUrl} alt={'Profile picture provided by the identity provider'} height={48} width={48} />
|
||||
</Form.Check.Label>
|
||||
</Form.Check>
|
||||
)}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { FrontendConfig } from '../../../api/config/types'
|
||||
import type { FrontendConfigDto } from '@hedgedoc/commons'
|
||||
import { createContext } from 'react'
|
||||
|
||||
export const frontendConfigContext = createContext<FrontendConfig | undefined>(undefined)
|
||||
export const frontendConfigContext = createContext<FrontendConfigDto | undefined>(undefined)
|
||||
|
|
|
@ -1,16 +1,22 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
'use client'
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { FrontendConfig } from '../../../api/config/types'
|
||||
import type { FrontendConfigDto } from '@hedgedoc/commons'
|
||||
import { frontendConfigContext } from './context'
|
||||
import type { PropsWithChildren } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
interface FrontendConfigContextProviderProps extends PropsWithChildren {
|
||||
config?: FrontendConfig
|
||||
config?: FrontendConfigDto
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { FrontendConfig } from '../../../api/config/types'
|
||||
import type { FrontendConfigDto } from '@hedgedoc/commons'
|
||||
import { frontendConfigContext } from './context'
|
||||
import { Optional } from '@mrdrogdrog/optional'
|
||||
import { useContext } from 'react'
|
||||
|
@ -11,7 +11,7 @@ import { useContext } from 'react'
|
|||
/**
|
||||
* Retrieves the current frontend config from the next react context.
|
||||
*/
|
||||
export const useFrontendConfig = (): FrontendConfig => {
|
||||
export const useFrontendConfig = (): FrontendConfigDto => {
|
||||
return Optional.ofNullable(useContext(frontendConfigContext)).orElseThrow(
|
||||
() => new Error('No frontend config context found. Did you forget to use the provider component?')
|
||||
)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
@ -12,7 +12,7 @@ import React, { useCallback } from 'react'
|
|||
import { FileEarmarkPlus as IconPlus } from 'react-bootstrap-icons'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { useFrontendConfig } from '../frontend-config-context/use-frontend-config'
|
||||
import { GuestAccessLevel } from '../../../api/config/types'
|
||||
import { GuestAccess } from '@hedgedoc/commons'
|
||||
import { useIsLoggedIn } from '../../../hooks/common/use-is-logged-in'
|
||||
|
||||
/**
|
||||
|
@ -34,7 +34,7 @@ export const NewNoteButton: React.FC = () => {
|
|||
})
|
||||
}, [router, showErrorNotification])
|
||||
|
||||
if (!isLoggedIn && guestAccessLevel !== GuestAccessLevel.CREATE) {
|
||||
if (!isLoggedIn && guestAccessLevel !== GuestAccess.CREATE) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import * as createNoteWithPrimaryAliasModule from '../../../api/notes'
|
||||
import type { Note, NoteMetadata } from '../../../api/notes/types'
|
||||
import { mockI18n } from '../../../test-utils/mock-i18n'
|
||||
import { CreateNonExistingNoteHint } from './create-non-existing-note-hint'
|
||||
import type { NoteDto, NoteMetadataDto } from '@hedgedoc/commons'
|
||||
import { waitForOtherPromisesToFinish } from '@hedgedoc/commons'
|
||||
import { act, render, screen, waitFor } from '@testing-library/react'
|
||||
import { Mock } from 'ts-mockery'
|
||||
|
@ -20,20 +20,20 @@ describe('create non existing note hint', () => {
|
|||
const mockCreateNoteWithPrimaryAlias = () => {
|
||||
jest
|
||||
.spyOn(createNoteWithPrimaryAliasModule, 'createNoteWithPrimaryAlias')
|
||||
.mockImplementation(async (markdown, primaryAlias): Promise<Note> => {
|
||||
.mockImplementation(async (markdown, primaryAlias): Promise<NoteDto> => {
|
||||
expect(markdown).toBe('')
|
||||
expect(primaryAlias).toBe(mockedNoteId)
|
||||
const metadata: NoteMetadata = Mock.of<NoteMetadata>({ primaryAddress: 'mockedPrimaryAlias' })
|
||||
const metadata: NoteMetadataDto = Mock.of<NoteMetadataDto>({ primaryAddress: 'mockedPrimaryAlias' })
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
await waitForOtherPromisesToFinish()
|
||||
return Mock.of<Note>({ metadata })
|
||||
return Mock.of<NoteDto>({ metadata })
|
||||
})
|
||||
}
|
||||
|
||||
const mockFailingCreateNoteWithPrimaryAlias = () => {
|
||||
jest
|
||||
.spyOn(createNoteWithPrimaryAliasModule, 'createNoteWithPrimaryAlias')
|
||||
.mockImplementation(async (markdown, primaryAlias): Promise<Note> => {
|
||||
.mockImplementation(async (markdown, primaryAlias): Promise<NoteDto> => {
|
||||
expect(markdown).toBe('')
|
||||
expect(primaryAlias).toBe(mockedNoteId)
|
||||
await waitForOtherPromisesToFinish()
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { ApiError } from '../../../api/common/api-error'
|
||||
import * as getNoteModule from '../../../api/notes'
|
||||
import type { Note } from '../../../api/notes/types'
|
||||
import * as LoadingScreenModule from '../../../components/application-loader/loading-screen/loading-screen'
|
||||
import * as setNoteDataFromServerModule from '../../../redux/note-details/methods'
|
||||
import { mockI18n } from '../../../test-utils/mock-i18n'
|
||||
|
@ -16,6 +15,7 @@ import { NoteLoadingBoundary } from './note-loading-boundary'
|
|||
import { render, screen } from '@testing-library/react'
|
||||
import { Fragment } from 'react'
|
||||
import { Mock } from 'ts-mockery'
|
||||
import type { NoteDto } from '@hedgedoc/commons'
|
||||
|
||||
jest.mock('../../../hooks/common/use-single-string-url-parameter')
|
||||
jest.mock('../../../api/notes')
|
||||
|
@ -65,7 +65,7 @@ describe('Note loading boundary', () => {
|
|||
})
|
||||
})
|
||||
|
||||
const mockGetNoteApiCall = (returnValue: Note) => {
|
||||
const mockGetNoteApiCall = (returnValue: NoteDto) => {
|
||||
jest.spyOn(getNoteModule, 'getNote').mockImplementation((id) => {
|
||||
expect(id).toBe(mockedNoteId)
|
||||
return new Promise((resolve) => {
|
||||
|
@ -83,14 +83,14 @@ describe('Note loading boundary', () => {
|
|||
})
|
||||
}
|
||||
|
||||
const mockSetNoteInRedux = (expectedNote: Note): jest.SpyInstance<void, [apiResponse: Note]> => {
|
||||
const mockSetNoteInRedux = (expectedNote: NoteDto): jest.SpyInstance<void, [apiResponse: NoteDto]> => {
|
||||
return jest.spyOn(setNoteDataFromServerModule, 'setNoteDataFromServer').mockImplementation((givenNote) => {
|
||||
expect(givenNote).toBe(expectedNote)
|
||||
})
|
||||
}
|
||||
|
||||
it('loads a note', async () => {
|
||||
const mockedNote: Note = Mock.of<Note>()
|
||||
const mockedNote: NoteDto = Mock.of<NoteDto>()
|
||||
mockGetNoteApiCall(mockedNote)
|
||||
const setNoteInReduxFunctionMock = mockSetNoteInRedux(mockedNote)
|
||||
|
||||
|
@ -106,7 +106,7 @@ describe('Note loading boundary', () => {
|
|||
})
|
||||
|
||||
it('shows an error', async () => {
|
||||
const mockedNote: Note = Mock.of<Note>()
|
||||
const mockedNote: NoteDto = Mock.of<NoteDto>()
|
||||
mockCrashingNoteApiCall()
|
||||
const setNoteInReduxFunctionMock = mockSetNoteInRedux(mockedNote)
|
||||
|
||||
|
|
|
@ -116,7 +116,7 @@ exports[`UserAvatar uses custom photo component if provided 1`] = `
|
|||
<span
|
||||
class="ms-2 me-1 user-line-name"
|
||||
>
|
||||
test
|
||||
No face user
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -133,7 +133,7 @@ exports[`UserAvatar uses custom photo component preferred over photoUrl 1`] = `
|
|||
<span
|
||||
class="ms-2 me-1 user-line-name"
|
||||
>
|
||||
test
|
||||
Boaty McBoatFace
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -155,7 +155,7 @@ exports[`UserAvatar uses identicon when empty photoUrl is given 1`] = `
|
|||
<span
|
||||
class="ms-2 me-1 user-line-name"
|
||||
>
|
||||
test
|
||||
Empty
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -177,7 +177,7 @@ exports[`UserAvatar uses identicon when no photoUrl is given 1`] = `
|
|||
<span
|
||||
class="ms-2 me-1 user-line-name"
|
||||
>
|
||||
test
|
||||
No face user
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -1,21 +1,29 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 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'>
|
||||
import type { CommonUserAvatarProps } from './types'
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
export const GuestUserAvatar: React.FC<CommonUserAvatarProps> = (props) => {
|
||||
const label = useTranslatedText('common.guestUser')
|
||||
return <UserAvatar displayName={label} photoComponent={<IconPerson />} {...props} />
|
||||
return (
|
||||
<UserAvatar
|
||||
user={{
|
||||
username: '',
|
||||
photoUrl: null,
|
||||
displayName: label
|
||||
}}
|
||||
photoComponent={<IconPerson />}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,28 +1,30 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { useMemo } from 'react'
|
||||
import { createAvatar } from '@dicebear/core'
|
||||
import * as identicon from '@dicebear/identicon'
|
||||
import type { UserInfoDto } from '@hedgedoc/commons'
|
||||
|
||||
/**
|
||||
* Returns the correct avatar url for a user.
|
||||
* When an empty or no photoUrl is given, a random avatar is generated from the displayName.
|
||||
* When the user has no photoUrl, a random avatar is generated from the display name.
|
||||
*
|
||||
* @param photoUrl The photo url of the user to use. Maybe empty or not set.
|
||||
* @param username The username of the user to use as input to the random avatar.
|
||||
* @return The correct avatar url for the user.
|
||||
* @param user The user for which to get the avatar URL
|
||||
* @return The correct avatar url for the user
|
||||
*/
|
||||
export const useAvatarUrl = (photoUrl: string | undefined, username: string): string => {
|
||||
export const useAvatarUrl = (user: UserInfoDto): string => {
|
||||
const { photoUrl, displayName } = user
|
||||
|
||||
return useMemo(() => {
|
||||
if (photoUrl && photoUrl.trim() !== '') {
|
||||
return photoUrl
|
||||
}
|
||||
const avatar = createAvatar(identicon, {
|
||||
seed: username
|
||||
seed: displayName
|
||||
})
|
||||
return avatar.toDataUri()
|
||||
}, [photoUrl, username])
|
||||
}, [photoUrl, displayName])
|
||||
}
|
||||
|
|
14
frontend/src/components/common/user-avatar/types.ts
Normal file
14
frontend/src/components/common/user-avatar/types.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type React from 'react'
|
||||
|
||||
export interface CommonUserAvatarProps {
|
||||
size?: 'sm' | 'lg'
|
||||
additionalClasses?: string
|
||||
showName?: boolean
|
||||
photoComponent?: React.ReactNode
|
||||
overrideDisplayName?: string
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { UserInfo } from '../../../api/users/types'
|
||||
import type { UserAvatarProps } from './user-avatar'
|
||||
import { UserAvatar } from './user-avatar'
|
||||
import React from 'react'
|
||||
|
||||
export interface UserAvatarForUserProps extends Omit<UserAvatarProps, 'photoUrl' | 'displayName'> {
|
||||
user: UserInfo
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the avatar image of a user, optionally altogether with their name.
|
||||
*
|
||||
* @param user The user object with the display name and photo.
|
||||
* @param props remaining avatar props
|
||||
*/
|
||||
export const UserAvatarForUser: React.FC<UserAvatarForUserProps> = ({ user, ...props }) => {
|
||||
return <UserAvatar displayName={user.displayName} photoUrl={user.photoUrl} username={user.username} {...props} />
|
||||
}
|
|
@ -1,18 +1,18 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { getUserInfo } from '../../../api/users'
|
||||
import { AsyncLoadingBoundary } from '../async-loading-boundary/async-loading-boundary'
|
||||
import type { UserAvatarProps } from './user-avatar'
|
||||
import { UserAvatar } from './user-avatar'
|
||||
import React, { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAsync } from 'react-use'
|
||||
import type { UserInfo } from '../../../api/users/types'
|
||||
import type { CommonUserAvatarProps } from './types'
|
||||
import type { UserInfoDto } from '@hedgedoc/commons'
|
||||
|
||||
export interface UserAvatarForUsernameProps extends Omit<UserAvatarProps, 'photoUrl' | 'displayName'> {
|
||||
export interface UserAvatarForUsernameProps extends CommonUserAvatarProps {
|
||||
username: string | null
|
||||
}
|
||||
|
||||
|
@ -27,12 +27,13 @@ export interface UserAvatarForUsernameProps extends Omit<UserAvatarProps, 'photo
|
|||
*/
|
||||
export const UserAvatarForUsername: React.FC<UserAvatarForUsernameProps> = ({ username, ...props }) => {
|
||||
const { t } = useTranslation()
|
||||
const { error, value, loading } = useAsync(async (): Promise<UserInfo> => {
|
||||
const { error, value, loading } = useAsync(async (): Promise<UserInfoDto> => {
|
||||
return username
|
||||
? await getUserInfo(username)
|
||||
: {
|
||||
displayName: t('common.guestUser'),
|
||||
username: ''
|
||||
username: '',
|
||||
photoUrl: null
|
||||
}
|
||||
}, [username, t])
|
||||
|
||||
|
@ -40,8 +41,8 @@ export const UserAvatarForUsername: React.FC<UserAvatarForUsernameProps> = ({ us
|
|||
if (!value) {
|
||||
return null
|
||||
}
|
||||
return <UserAvatar displayName={value.displayName} photoUrl={value.photoUrl} username={username} {...props} />
|
||||
}, [props, value, username])
|
||||
return <UserAvatar user={value} {...props} />
|
||||
}, [props, value])
|
||||
|
||||
return (
|
||||
<AsyncLoadingBoundary loading={loading || !value} error={error} componentName={'UserAvatarForUsername'}>
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { UserInfo } from '../../../api/users/types'
|
||||
import { mockI18n } from '../../../test-utils/mock-i18n'
|
||||
import { UserAvatarForUser } from './user-avatar-for-user'
|
||||
import { render } from '@testing-library/react'
|
||||
import { UserAvatar } from './user-avatar'
|
||||
import type { UserInfoDto } from '@hedgedoc/commons'
|
||||
|
||||
jest.mock('@dicebear/identicon', () => null)
|
||||
jest.mock('@dicebear/core', () => ({
|
||||
|
@ -17,58 +16,68 @@ jest.mock('@dicebear/core', () => ({
|
|||
}))
|
||||
|
||||
describe('UserAvatar', () => {
|
||||
const user: UserInfo = {
|
||||
const user: UserInfoDto = {
|
||||
username: 'boatface',
|
||||
displayName: 'Boaty McBoatFace',
|
||||
photoUrl: 'https://example.com/test.png'
|
||||
}
|
||||
|
||||
const userWithoutPhoto: UserInfoDto = {
|
||||
username: 'pictureless',
|
||||
displayName: 'No face user',
|
||||
photoUrl: null
|
||||
}
|
||||
|
||||
const userWithEmptyPhoto: UserInfoDto = {
|
||||
username: 'void',
|
||||
displayName: 'Empty',
|
||||
photoUrl: ''
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await mockI18n()
|
||||
})
|
||||
|
||||
it('renders the user avatar correctly', () => {
|
||||
const view = render(<UserAvatarForUser user={user} />)
|
||||
const view = render(<UserAvatar user={user} />)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
describe('renders the user avatar in size', () => {
|
||||
it('sm', () => {
|
||||
const view = render(<UserAvatarForUser user={user} size={'sm'} />)
|
||||
const view = render(<UserAvatar user={user} size={'sm'} />)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
it('lg', () => {
|
||||
const view = render(<UserAvatarForUser user={user} size={'lg'} />)
|
||||
const view = render(<UserAvatar user={user} size={'lg'} />)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
it('adds additionalClasses props to wrapping span', () => {
|
||||
const view = render(<UserAvatarForUser user={user} additionalClasses={'testClass'} />)
|
||||
const view = render(<UserAvatar user={user} additionalClasses={'testClass'} />)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
it('does not show names if showName prop is false', () => {
|
||||
const view = render(<UserAvatarForUser user={user} showName={false} />)
|
||||
const view = render(<UserAvatar user={user} showName={false} />)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('uses identicon when no photoUrl is given', () => {
|
||||
const view = render(<UserAvatar displayName={'test'} />)
|
||||
const view = render(<UserAvatar user={userWithoutPhoto} />)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('uses identicon when empty photoUrl is given', () => {
|
||||
const view = render(<UserAvatar displayName={'test'} photoUrl={''} />)
|
||||
const view = render(<UserAvatar user={userWithEmptyPhoto} />)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('uses custom photo component if provided', () => {
|
||||
const view = render(<UserAvatar displayName={'test'} photoComponent={<div>Custom Photo</div>} />)
|
||||
const view = render(<UserAvatar user={userWithoutPhoto} 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} />
|
||||
)
|
||||
const view = render(<UserAvatar user={user} photoComponent={<div>Custom Photo</div>} />)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
@ -7,15 +7,11 @@ import { useTranslatedText } from '../../../hooks/common/use-translated-text'
|
|||
import styles from './user-avatar.module.scss'
|
||||
import React, { useMemo } from 'react'
|
||||
import { useAvatarUrl } from './hooks/use-avatar-url'
|
||||
import type { UserInfoDto } from '@hedgedoc/commons'
|
||||
import type { CommonUserAvatarProps } from './types'
|
||||
|
||||
export interface UserAvatarProps {
|
||||
size?: 'sm' | 'lg'
|
||||
additionalClasses?: string
|
||||
showName?: boolean
|
||||
photoUrl?: string
|
||||
displayName: string
|
||||
username?: string | null
|
||||
photoComponent?: React.ReactNode
|
||||
interface UserAvatarProps extends CommonUserAvatarProps {
|
||||
user: UserInfoDto
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -25,17 +21,16 @@ export interface UserAvatarProps {
|
|||
* @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 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.
|
||||
* @param overrideDisplayName Used to override the used display name, for example for setting random guest names
|
||||
*/
|
||||
export const UserAvatar: React.FC<UserAvatarProps> = ({
|
||||
photoUrl,
|
||||
displayName,
|
||||
size,
|
||||
additionalClasses = '',
|
||||
showName = true,
|
||||
username,
|
||||
photoComponent
|
||||
photoComponent,
|
||||
user,
|
||||
overrideDisplayName
|
||||
}) => {
|
||||
const imageSize = useMemo(() => {
|
||||
switch (size) {
|
||||
|
@ -48,13 +43,21 @@ export const UserAvatar: React.FC<UserAvatarProps> = ({
|
|||
}
|
||||
}, [size])
|
||||
|
||||
const avatarUrl = useAvatarUrl(photoUrl, username ?? displayName)
|
||||
const modifiedUser: UserInfoDto = useMemo(
|
||||
() => ({
|
||||
...user,
|
||||
displayName: overrideDisplayName ?? user.displayName
|
||||
}),
|
||||
[user, overrideDisplayName]
|
||||
)
|
||||
|
||||
const avatarUrl = useAvatarUrl(modifiedUser)
|
||||
|
||||
const imageTranslateOptions = useMemo(
|
||||
() => ({
|
||||
name: displayName
|
||||
name: modifiedUser.displayName
|
||||
}),
|
||||
[displayName]
|
||||
[modifiedUser.displayName]
|
||||
)
|
||||
const imgDescription = useTranslatedText('common.avatarOf', imageTranslateOptions)
|
||||
|
||||
|
@ -71,7 +74,7 @@ export const UserAvatar: React.FC<UserAvatarProps> = ({
|
|||
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']}`}>{modifiedUser.displayName}</span>}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { LoginUserInfo } from '../../../../api/me/types'
|
||||
import { useDisconnectOnUserLoginStatusChange } from './use-disconnect-on-user-login-status-change'
|
||||
import type { MessageTransporter } from '@hedgedoc/commons'
|
||||
import type { LoginUserInfoDto, MessageTransporter } from '@hedgedoc/commons'
|
||||
import { render } from '@testing-library/react'
|
||||
import React, { Fragment } from 'react'
|
||||
import { Mock } from 'ts-mockery'
|
||||
|
@ -21,7 +20,7 @@ describe('use logout on user change', () => {
|
|||
|
||||
const mockUseApplicationState = (userLoggedIn: boolean) => {
|
||||
mockAppState({
|
||||
user: userLoggedIn ? Mock.of<LoginUserInfo>({}) : null
|
||||
user: userLoggedIn ? Mock.of<LoginUserInfoDto>({}) : null
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
@ -36,7 +36,8 @@ describe('frontend websocket', () => {
|
|||
mockSocket()
|
||||
const handler = jest.fn((reason?: DisconnectReason) => console.log(reason))
|
||||
|
||||
let modifiedHandler: (event: CloseEvent) => void = jest.fn()
|
||||
let modifiedHandler: EventListenerOrEventListenerObject = jest.fn()
|
||||
|
||||
jest.spyOn(mockedSocket, 'addEventListener').mockImplementation((event, handler_) => {
|
||||
modifiedHandler = handler_
|
||||
})
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { addLink } from './add-link'
|
||||
import type { ContentEdits } from './changes'
|
||||
import type { ContentEdits } from './types/changes'
|
||||
|
||||
describe('add link', () => {
|
||||
describe('without to-cursor', () => {
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import * as AliasModule from '../../../../../../api/alias'
|
||||
import type { Alias } from '../../../../../../api/alias/types'
|
||||
import * as NoteDetailsReduxModule from '../../../../../../redux/note-details/methods'
|
||||
import { mockI18n } from '../../../../../../test-utils/mock-i18n'
|
||||
import { mockNotePermissions } from '../../../../../../test-utils/mock-note-permissions'
|
||||
|
@ -12,6 +11,7 @@ import { AliasesListEntry } from './aliases-list-entry'
|
|||
import { act, render, screen } from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import { mockUiNotifications } from '../../../../../../test-utils/mock-ui-notifications'
|
||||
import type { AliasDto } from '@hedgedoc/commons'
|
||||
|
||||
jest.mock('../../../../../../api/alias')
|
||||
jest.mock('../../../../../../redux/note-details/methods')
|
||||
|
@ -37,7 +37,7 @@ describe('AliasesListEntry', () => {
|
|||
|
||||
it('renders an AliasesListEntry that is primary', async () => {
|
||||
mockNotePermissions('test', 'test')
|
||||
const testAlias: Alias = {
|
||||
const testAlias: AliasDto = {
|
||||
name: 'test-primary',
|
||||
primaryAlias: true,
|
||||
noteId: 'test-note-id'
|
||||
|
@ -55,7 +55,7 @@ describe('AliasesListEntry', () => {
|
|||
|
||||
it("adds aliasPrimaryBadge & removes aliasButtonMakePrimary in AliasesListEntry if it's primary", () => {
|
||||
mockNotePermissions('test2', 'test')
|
||||
const testAlias: Alias = {
|
||||
const testAlias: AliasDto = {
|
||||
name: 'test-primary',
|
||||
primaryAlias: true,
|
||||
noteId: 'test-note-id'
|
||||
|
@ -66,7 +66,7 @@ describe('AliasesListEntry', () => {
|
|||
|
||||
it('renders an AliasesListEntry that is not primary', async () => {
|
||||
mockNotePermissions('test', 'test')
|
||||
const testAlias: Alias = {
|
||||
const testAlias: AliasDto = {
|
||||
name: 'test-non-primary',
|
||||
primaryAlias: false,
|
||||
noteId: 'test-note-id'
|
||||
|
@ -91,7 +91,7 @@ describe('AliasesListEntry', () => {
|
|||
|
||||
it("removes aliasPrimaryBadge & adds aliasButtonMakePrimary in AliasesListEntry if it's not primary", () => {
|
||||
mockNotePermissions('test2', 'test')
|
||||
const testAlias: Alias = {
|
||||
const testAlias: AliasDto = {
|
||||
name: 'test-primary',
|
||||
primaryAlias: false,
|
||||
noteId: 'test-note-id'
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { deleteAlias, markAliasAsPrimary } from '../../../../../../api/alias'
|
||||
import type { Alias } from '../../../../../../api/alias/types'
|
||||
import { useIsOwner } from '../../../../../../hooks/common/use-is-owner'
|
||||
import { useTranslatedText } from '../../../../../../hooks/common/use-translated-text'
|
||||
import { updateMetadata } from '../../../../../../redux/note-details/methods'
|
||||
|
@ -16,9 +15,10 @@ import { Badge } from 'react-bootstrap'
|
|||
import { Button } from 'react-bootstrap'
|
||||
import { Star as IconStar, X as IconX } from 'react-bootstrap-icons'
|
||||
import { useTranslation, Trans } from 'react-i18next'
|
||||
import type { AliasDto } from '@hedgedoc/commons'
|
||||
|
||||
export interface AliasesListEntryProps {
|
||||
alias: Alias
|
||||
alias: AliasDto
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { useApplicationState } from '../../../../../../hooks/common/use-application-state'
|
||||
import type { Alias } from '../../../../../../api/alias/types'
|
||||
import type { ApplicationState } from '../../../../../../redux'
|
||||
import { AliasesListEntry } from './aliases-list-entry'
|
||||
import React, { Fragment, useMemo } from 'react'
|
||||
import type { AliasDto } from '@hedgedoc/commons'
|
||||
|
||||
/**
|
||||
* Renders the list of aliases.
|
||||
|
@ -18,8 +18,8 @@ export const AliasesList: React.FC = () => {
|
|||
return aliases === undefined
|
||||
? null
|
||||
: Object.assign([], aliases)
|
||||
.sort((a: Alias, b: Alias) => a.name.localeCompare(b.name))
|
||||
.map((alias: Alias) => <AliasesListEntry alias={alias} key={alias.name} />)
|
||||
.sort((a: AliasDto, b: AliasDto) => a.name.localeCompare(b.name))
|
||||
.map((alias: AliasDto) => <AliasesListEntry alias={alias} key={alias.name} />)
|
||||
}, [aliases])
|
||||
|
||||
return <Fragment>{aliasesDom}</Fragment>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
@ -15,9 +15,9 @@ import { useApplicationState } from '../../../../../hooks/common/use-application
|
|||
import { getMediaForNote } from '../../../../../api/notes'
|
||||
import { AsyncLoadingBoundary } from '../../../../common/async-loading-boundary/async-loading-boundary'
|
||||
import { MediaEntry } from './media-entry'
|
||||
import type { MediaUpload } from '../../../../../api/media/types'
|
||||
import { MediaEntryDeletionModal } from './media-entry-deletion-modal'
|
||||
import { MediaBrowserEmpty } from './media-browser-empty'
|
||||
import type { MediaUploadDto } from '@hedgedoc/commons'
|
||||
|
||||
/**
|
||||
* Renders the media browser "menu" for the sidebar.
|
||||
|
@ -35,7 +35,7 @@ export const MediaBrowserSidebarMenu: React.FC<SpecificSidebarMenuProps> = ({
|
|||
}) => {
|
||||
useTranslation()
|
||||
const noteId = useApplicationState((state) => state.noteDetails?.id ?? '')
|
||||
const [mediaEntryForDeletion, setMediaEntryForDeletion] = useState<MediaUpload | null>(null)
|
||||
const [mediaEntryForDeletion, setMediaEntryForDeletion] = useState<MediaUploadDto | null>(null)
|
||||
|
||||
const hide = selectedMenuId !== DocumentSidebarMenuSelection.NONE && selectedMenuId !== menuId
|
||||
const expand = selectedMenuId === menuId
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import type { MediaUpload } from '../../../../../api/media/types'
|
||||
import { useBaseUrl } from '../../../../../hooks/common/use-base-url'
|
||||
import { Button, ButtonGroup } from 'react-bootstrap'
|
||||
import {
|
||||
|
@ -20,10 +19,11 @@ import { UserAvatarForUsername } from '../../../../common/user-avatar/user-avata
|
|||
import { useChangeEditorContentCallback } from '../../../change-content-context/use-change-editor-content-callback'
|
||||
import { replaceSelection } from '../../../editor-pane/tool-bar/formatters/replace-selection'
|
||||
import styles from './media-entry.module.css'
|
||||
import type { MediaUploadDto } from '@hedgedoc/commons'
|
||||
|
||||
export interface MediaEntryProps {
|
||||
entry: MediaUpload
|
||||
onDelete: (entry: MediaUpload) => void
|
||||
entry: MediaUploadDto
|
||||
onDelete: (entry: MediaUploadDto) => void
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { useTranslatedText } from '../../../../../../hooks/common/use-translated-text'
|
||||
import { UiIcon } from '../../../../../common/icons/ui-icon'
|
||||
import type { PermissionDisabledProps } from './permission-disabled.prop'
|
||||
import { AccessLevel } from '@hedgedoc/commons'
|
||||
import { GuestAccess } from '@hedgedoc/commons'
|
||||
import React, { useMemo } from 'react'
|
||||
import { Button, ToggleButtonGroup } from 'react-bootstrap'
|
||||
import { Eye as IconEye, Pencil as IconPencil, X as IconX } from 'react-bootstrap-icons'
|
||||
|
@ -24,7 +24,7 @@ export enum PermissionType {
|
|||
|
||||
export interface PermissionEntryButtonsProps {
|
||||
type: PermissionType
|
||||
currentSetting: AccessLevel
|
||||
currentSetting: GuestAccess
|
||||
name: string
|
||||
onSetReadOnly: () => void
|
||||
onSetWriteable: () => void
|
||||
|
@ -79,14 +79,14 @@ export const PermissionEntryButtons: React.FC<PermissionEntryButtonsProps & Perm
|
|||
<Button
|
||||
disabled={disabled}
|
||||
title={setReadOnlyTitle}
|
||||
variant={currentSetting === AccessLevel.READ_ONLY ? 'secondary' : 'outline-secondary'}
|
||||
variant={currentSetting === GuestAccess.READ ? 'secondary' : 'outline-secondary'}
|
||||
onClick={onSetReadOnly}>
|
||||
<UiIcon icon={IconEye} />
|
||||
</Button>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
title={setWritableTitle}
|
||||
variant={currentSetting === AccessLevel.WRITEABLE ? 'secondary' : 'outline-secondary'}
|
||||
variant={currentSetting === GuestAccess.WRITE ? 'secondary' : 'outline-secondary'}
|
||||
onClick={onSetWriteable}>
|
||||
<UiIcon icon={IconPencil} />
|
||||
</Button>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
@ -10,7 +10,7 @@ import { setNotePermissionsFromServer } from '../../../../../../redux/note-detai
|
|||
import { IconButton } from '../../../../../common/icon-button/icon-button'
|
||||
import { useUiNotifications } from '../../../../../notifications/ui-notification-boundary'
|
||||
import type { PermissionDisabledProps } from './permission-disabled.prop'
|
||||
import { AccessLevel, SpecialGroup } from '@hedgedoc/commons'
|
||||
import { GuestAccess, SpecialGroup } from '@hedgedoc/commons'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { ToggleButtonGroup } from 'react-bootstrap'
|
||||
import { Eye as IconEye, Pencil as IconPencil, SlashCircle as IconSlashCircle } from 'react-bootstrap-icons'
|
||||
|
@ -19,7 +19,7 @@ import { PermissionInconsistentAlert } from './permission-inconsistent-alert'
|
|||
import { cypressId } from '../../../../../../utils/cypress-attribute'
|
||||
|
||||
export interface PermissionEntrySpecialGroupProps {
|
||||
level: AccessLevel
|
||||
level: GuestAccess
|
||||
type: SpecialGroup
|
||||
inconsistent?: boolean
|
||||
}
|
||||
|
@ -98,7 +98,7 @@ export const PermissionEntrySpecialGroup: React.FC<PermissionEntrySpecialGroupPr
|
|||
<IconButton
|
||||
icon={IconSlashCircle}
|
||||
title={denyGroupText}
|
||||
variant={level === AccessLevel.NONE ? 'secondary' : 'outline-secondary'}
|
||||
variant={level === GuestAccess.DENY ? 'secondary' : 'outline-secondary'}
|
||||
onClick={onSetEntryDenied}
|
||||
disabled={disabled}
|
||||
className={'p-1'}
|
||||
|
@ -107,7 +107,7 @@ export const PermissionEntrySpecialGroup: React.FC<PermissionEntrySpecialGroupPr
|
|||
<IconButton
|
||||
icon={IconEye}
|
||||
title={viewOnlyGroupText}
|
||||
variant={level === AccessLevel.READ_ONLY ? 'secondary' : 'outline-secondary'}
|
||||
variant={level === GuestAccess.READ ? 'secondary' : 'outline-secondary'}
|
||||
onClick={onSetEntryReadOnly}
|
||||
disabled={disabled}
|
||||
className={'p-1'}
|
||||
|
@ -116,7 +116,7 @@ export const PermissionEntrySpecialGroup: React.FC<PermissionEntrySpecialGroupPr
|
|||
<IconButton
|
||||
icon={IconPencil}
|
||||
title={editGroupText}
|
||||
variant={level === AccessLevel.WRITEABLE ? 'secondary' : 'outline-secondary'}
|
||||
variant={level === GuestAccess.WRITE ? 'secondary' : 'outline-secondary'}
|
||||
onClick={onSetEntryWriteable}
|
||||
disabled={disabled}
|
||||
className={'p-1'}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
@ -7,20 +7,20 @@ import { removeUserPermission, setUserPermission } from '../../../../../../api/p
|
|||
import { getUserInfo } from '../../../../../../api/users'
|
||||
import { useApplicationState } from '../../../../../../hooks/common/use-application-state'
|
||||
import { setNotePermissionsFromServer } from '../../../../../../redux/note-details/methods'
|
||||
import { UserAvatarForUser } from '../../../../../common/user-avatar/user-avatar-for-user'
|
||||
import { useUiNotifications } from '../../../../../notifications/ui-notification-boundary'
|
||||
import type { PermissionDisabledProps } from './permission-disabled.prop'
|
||||
import { PermissionEntryButtons, PermissionType } from './permission-entry-buttons'
|
||||
import type { NoteUserPermissionEntry } from '@hedgedoc/commons'
|
||||
import { AccessLevel, SpecialGroup } from '@hedgedoc/commons'
|
||||
import type { NoteUserPermissionEntryDto } from '@hedgedoc/commons'
|
||||
import { GuestAccess, SpecialGroup } from '@hedgedoc/commons'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { useAsync } from 'react-use'
|
||||
import { PermissionInconsistentAlert } from './permission-inconsistent-alert'
|
||||
import { useGetSpecialPermissions } from './hooks/use-get-special-permissions'
|
||||
import { AsyncLoadingBoundary } from '../../../../../common/async-loading-boundary/async-loading-boundary'
|
||||
import { UserAvatar } from '../../../../../common/user-avatar/user-avatar'
|
||||
|
||||
export interface PermissionEntryUserProps {
|
||||
entry: NoteUserPermissionEntry
|
||||
entry: NoteUserPermissionEntryDto
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -89,12 +89,12 @@ export const PermissionEntryUser: React.FC<PermissionEntryUserProps & Permission
|
|||
return (
|
||||
<AsyncLoadingBoundary loading={loading} error={error} componentName={'PermissionEntryUser'}>
|
||||
<li className={'list-group-item d-flex flex-row justify-content-between align-items-center'}>
|
||||
<UserAvatarForUser user={value} />
|
||||
<UserAvatar user={value} />
|
||||
<div className={'d-flex flex-row align-items-center'}>
|
||||
<PermissionInconsistentAlert show={permissionInconsistent ?? false} />
|
||||
<PermissionEntryButtons
|
||||
type={PermissionType.USER}
|
||||
currentSetting={entry.canEdit ? AccessLevel.WRITEABLE : AccessLevel.READ_ONLY}
|
||||
currentSetting={entry.canEdit ? GuestAccess.WRITE : GuestAccess.READ}
|
||||
name={value.displayName}
|
||||
onSetReadOnly={onSetEntryReadOnly}
|
||||
onSetWriteable={onSetEntryWriteable}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { useIsOwner } from '../../../../../../hooks/common/use-is-owner'
|
||||
import type { PermissionDisabledProps } from './permission-disabled.prop'
|
||||
import { PermissionEntrySpecialGroup } from './permission-entry-special-group'
|
||||
import { AccessLevel, SpecialGroup } from '@hedgedoc/commons'
|
||||
import { GuestAccess, SpecialGroup } from '@hedgedoc/commons'
|
||||
import React, { Fragment, useMemo } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { useGetSpecialPermissions } from './hooks/use-get-special-permissions'
|
||||
|
@ -23,16 +23,8 @@ export const PermissionSectionSpecialGroups: React.FC<PermissionDisabledProps> =
|
|||
|
||||
const specialGroupEntries = useMemo(() => {
|
||||
return {
|
||||
everyoneLevel: groupEveryone
|
||||
? groupEveryone.canEdit
|
||||
? AccessLevel.WRITEABLE
|
||||
: AccessLevel.READ_ONLY
|
||||
: AccessLevel.NONE,
|
||||
loggedInLevel: groupLoggedIn
|
||||
? groupLoggedIn.canEdit
|
||||
? AccessLevel.WRITEABLE
|
||||
: AccessLevel.READ_ONLY
|
||||
: AccessLevel.NONE,
|
||||
everyoneLevel: groupEveryone ? (groupEveryone.canEdit ? GuestAccess.WRITE : GuestAccess.READ) : GuestAccess.DENY,
|
||||
loggedInLevel: groupLoggedIn ? (groupLoggedIn.canEdit ? GuestAccess.WRITE : GuestAccess.READ) : GuestAccess.DENY,
|
||||
loggedInInconsistentAlert: groupEveryone && (!groupLoggedIn || (groupEveryone.canEdit && !groupLoggedIn.canEdit))
|
||||
}
|
||||
}, [groupEveryone, groupLoggedIn])
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { RevisionMetadata } from '../../../../../../api/revisions/types'
|
||||
import type { RevisionMetadataDto } from '@hedgedoc/commons'
|
||||
import { UiIcon } from '../../../../../common/icons/ui-icon'
|
||||
import { UserAvatarForUser } from '../../../../../common/user-avatar/user-avatar-for-user'
|
||||
import { WaitSpinner } from '../../../../../common/wait-spinner/wait-spinner'
|
||||
import { useUiNotifications } from '../../../../../notifications/ui-notification-boundary'
|
||||
import styles from './revision-list-entry.module.scss'
|
||||
|
@ -21,11 +20,12 @@ import {
|
|||
} from 'react-bootstrap-icons'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { useAsync } from 'react-use'
|
||||
import { UserAvatar } from '../../../../../common/user-avatar/user-avatar'
|
||||
|
||||
export interface RevisionListEntryProps {
|
||||
active: boolean
|
||||
onSelect: () => void
|
||||
revision: RevisionMetadata
|
||||
revision: RevisionMetadataDto
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -47,7 +47,7 @@ export const RevisionListEntry: React.FC<RevisionListEntryProps> = ({ active, on
|
|||
try {
|
||||
const authorDetails = await getUserDataForRevision(revision.authorUsernames)
|
||||
return authorDetails.map((author) => (
|
||||
<UserAvatarForUser user={author} key={author.username} showName={false} additionalClasses={'mx-1'} />
|
||||
<UserAvatar user={author} key={author.username} showName={false} additionalClasses={'mx-1'} />
|
||||
))
|
||||
} catch (error) {
|
||||
showErrorNotification('editor.modal.revision.errorUser')(error as Error)
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { RevisionMetadata } from '../../../../../../api/revisions/types'
|
||||
import type { RevisionMetadataDto } from '@hedgedoc/commons'
|
||||
import { cypressId } from '../../../../../../utils/cypress-attribute'
|
||||
import { AsyncLoadingBoundary } from '../../../../../common/async-loading-boundary/async-loading-boundary'
|
||||
import { RevisionListEntry } from './revision-list-entry'
|
||||
|
@ -13,7 +13,7 @@ import { ListGroup } from 'react-bootstrap'
|
|||
|
||||
interface RevisionListProps {
|
||||
selectedRevisionId?: number
|
||||
revisions?: RevisionMetadata[]
|
||||
revisions?: RevisionMetadataDto[]
|
||||
loadingRevisions: boolean
|
||||
error?: Error | boolean
|
||||
onRevisionSelect: (selectedRevisionId: number) => void
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { RevisionDetails } from '../../../../../../api/revisions/types'
|
||||
import { getUserInfo } from '../../../../../../api/users'
|
||||
import type { UserInfo } from '../../../../../../api/users/types'
|
||||
import { download } from '../../../../../common/download/download'
|
||||
import type { RevisionDto, UserInfoDto } from '@hedgedoc/commons'
|
||||
|
||||
const DISPLAY_MAX_USERS_PER_REVISION = 9
|
||||
|
||||
|
@ -16,7 +15,7 @@ const DISPLAY_MAX_USERS_PER_REVISION = 9
|
|||
* @param noteId The id of the note from which to download the revision.
|
||||
* @param revision The revision details object containing the content to download.
|
||||
*/
|
||||
export const downloadRevision = (noteId: string, revision: RevisionDetails | null): void => {
|
||||
export const downloadRevision = (noteId: string, revision: RevisionDto | null): void => {
|
||||
if (!revision) {
|
||||
return
|
||||
}
|
||||
|
@ -30,8 +29,8 @@ export const downloadRevision = (noteId: string, revision: RevisionDetails | nul
|
|||
* @throws {Error} in case the user-data request failed.
|
||||
* @return An array of user details.
|
||||
*/
|
||||
export const getUserDataForRevision = async (usernames: string[]): Promise<UserInfo[]> => {
|
||||
const users: UserInfo[] = []
|
||||
export const getUserDataForRevision = async (usernames: string[]): Promise<UserInfoDto[]> => {
|
||||
const users: UserInfoDto[] = []
|
||||
const usersToFetch = Math.min(usernames.length, DISPLAY_MAX_USERS_PER_REVISION) - 1
|
||||
for (let i = 0; i <= usersToFetch; i++) {
|
||||
const user = await getUserInfo(usernames[i])
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { UserAvatar } from '../../../../../common/user-avatar/user-avatar'
|
||||
import { UserAvatarForUsername } from '../../../../../common/user-avatar/user-avatar-for-username'
|
||||
import { createCursorCssClass } from '../../../../editor-pane/codemirror-extensions/remote-cursors/create-cursor-css-class'
|
||||
import { ActiveIndicator } from '../active-indicator'
|
||||
|
@ -12,6 +11,7 @@ import React, { useMemo } from 'react'
|
|||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { Incognito as IconIncognito } from 'react-bootstrap-icons'
|
||||
import { useTranslatedText } from '../../../../../../hooks/common/use-translated-text'
|
||||
import { GuestUserAvatar } from '../../../../../common/user-avatar/guest-user-avatar'
|
||||
|
||||
export interface UserLineProps {
|
||||
username: string | null
|
||||
|
@ -38,7 +38,10 @@ export const UserLine: React.FC<UserLineProps> = ({ username, displayName, activ
|
|||
return username ? (
|
||||
<UserAvatarForUsername username={username} additionalClasses={'flex-fill overflow-hidden px-2 text-nowrap'} />
|
||||
) : (
|
||||
<UserAvatar displayName={displayName} additionalClasses={'flex-fill overflow-hidden px-2 text-nowrap'} />
|
||||
<GuestUserAvatar
|
||||
overrideDisplayName={displayName}
|
||||
additionalClasses={'flex-fill overflow-hidden px-2 text-nowrap'}
|
||||
/>
|
||||
)
|
||||
}, [displayName, username])
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
@ -61,18 +61,56 @@ describe('Splitter', () => {
|
|||
const view = render(<Splitter left={<>left</>} right={<>right</>} />)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
const divider = await screen.findByTestId('splitter-divider')
|
||||
const target: EventTarget = Mock.of<EventTarget>()
|
||||
const defaultTouchEvent: Omit<Touch, 'clientX'> = {
|
||||
clientY: 0,
|
||||
target: target,
|
||||
identifier: 0,
|
||||
pageX: 0,
|
||||
pageY: 0,
|
||||
screenX: 0,
|
||||
screenY: 0,
|
||||
force: 0,
|
||||
radiusX: 0,
|
||||
radiusY: 0,
|
||||
rotationAngle: 0
|
||||
}
|
||||
|
||||
fireEvent.touchStart(divider, {})
|
||||
fireEvent.touchMove(window, Mock.of<TouchEvent>({ touches: [{ clientX: 1920 }, { clientX: 200 }] }))
|
||||
fireEvent.touchMove(
|
||||
window,
|
||||
Mock.of<TouchEvent>({
|
||||
touches: [
|
||||
{ ...defaultTouchEvent, clientX: 1920 },
|
||||
{ ...defaultTouchEvent, clientX: 200 }
|
||||
]
|
||||
})
|
||||
)
|
||||
fireEvent.touchEnd(window)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
|
||||
fireEvent.touchStart(divider, {})
|
||||
fireEvent.touchMove(window, Mock.of<TouchEvent>({ touches: [{ clientX: 0 }, { clientX: 100 }] }))
|
||||
fireEvent.touchMove(
|
||||
window,
|
||||
Mock.of<TouchEvent>({
|
||||
touches: [
|
||||
{ ...defaultTouchEvent, clientX: 0 },
|
||||
{ ...defaultTouchEvent, clientX: 100 }
|
||||
]
|
||||
})
|
||||
)
|
||||
fireEvent.touchCancel(window)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
|
||||
fireEvent.touchMove(window, Mock.of<TouchEvent>({ touches: [{ clientX: 500 }, { clientX: 900 }] }))
|
||||
fireEvent.touchMove(
|
||||
window,
|
||||
Mock.of<TouchEvent>({
|
||||
touches: [
|
||||
{ ...defaultTouchEvent, clientX: 500 },
|
||||
{ ...defaultTouchEvent, clientX: 900 }
|
||||
]
|
||||
})
|
||||
)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { BackendVersion } from '../../../api/config/types'
|
||||
import type { ServerVersionDto } from '@hedgedoc/commons'
|
||||
import links from '../../../links.json'
|
||||
import { cypressId } from '../../../utils/cypress-attribute'
|
||||
import { CopyableField } from '../../common/copyable/copyable-field/copyable-field'
|
||||
|
@ -21,7 +21,7 @@ import { Modal } from 'react-bootstrap'
|
|||
* @param show If the modal should be shown.
|
||||
*/
|
||||
export const VersionInfoModal: React.FC<CommonModalProps> = ({ onHide, show }) => {
|
||||
const serverVersion: BackendVersion = useFrontendConfig().version
|
||||
const serverVersion: ServerVersionDto = useFrontendConfig().version
|
||||
const backendVersion = useMemo(() => {
|
||||
const version = `${serverVersion.major}.${serverVersion.minor}.${serverVersion.patch}`
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
@ -7,13 +7,13 @@ import { useApplicationState } from '../../../hooks/common/use-application-state
|
|||
import { useOutlineButtonVariant } from '../../../hooks/dark-mode/use-outline-button-variant'
|
||||
import { cypressId } from '../../../utils/cypress-attribute'
|
||||
import { UiIcon } from '../../common/icons/ui-icon'
|
||||
import { UserAvatarForUser } from '../../common/user-avatar/user-avatar-for-user'
|
||||
import { SignOutDropdownButton } from './sign-out-dropdown-button'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
import { Dropdown } from 'react-bootstrap'
|
||||
import { Person as IconPerson } from 'react-bootstrap-icons'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { UserAvatar } from '../../common/user-avatar/user-avatar'
|
||||
|
||||
/**
|
||||
* Renders a dropdown menu with user-relevant actions.
|
||||
|
@ -34,7 +34,7 @@ export const UserDropdown: React.FC = () => {
|
|||
size='sm'
|
||||
variant={buttonVariant}
|
||||
className={'d-flex align-items-center'}>
|
||||
<UserAvatarForUser user={user} />
|
||||
<UserAvatar user={user} />
|
||||
</Dropdown.Toggle>
|
||||
|
||||
<Dropdown.Menu className='text-start'>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
@ -28,13 +28,13 @@ export const LegalSubmenu: React.FC = (): null | ReactElement => {
|
|||
<Fragment>
|
||||
<Dropdown.Divider />
|
||||
<DropdownHeader i18nKey={'appbar.help.legal.header'} />
|
||||
{specialUrls.privacy !== undefined && (
|
||||
{specialUrls.privacy !== null && (
|
||||
<TranslatedDropdownItem href={specialUrls.privacy} i18nKey={'appbar.help.legal.privacy'} />
|
||||
)}
|
||||
{specialUrls.termsOfUse !== undefined && (
|
||||
{specialUrls.termsOfUse !== null && (
|
||||
<TranslatedDropdownItem href={specialUrls.termsOfUse} i18nKey={'appbar.help.legal.termsOfUse'} />
|
||||
)}
|
||||
{specialUrls.imprint !== undefined && (
|
||||
{specialUrls.imprint !== null && (
|
||||
<TranslatedDropdownItem href={specialUrls.imprint} i18nKey={'appbar.help.legal.imprint'} />
|
||||
)}
|
||||
</Fragment>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
@ -10,7 +10,7 @@ import { NewNoteButton } from '../../common/new-note-button/new-note-button'
|
|||
import { HistoryButton } from '../../layout/app-bar/app-bar-elements/help-dropdown/history-button'
|
||||
import { useFrontendConfig } from '../../common/frontend-config-context/use-frontend-config'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { GuestAccessLevel } from '../../../api/config/types'
|
||||
import { GuestAccess } from '@hedgedoc/commons'
|
||||
|
||||
/**
|
||||
* Renders the card with the options for not logged-in users.
|
||||
|
@ -20,7 +20,7 @@ export const GuestCard: React.FC = () => {
|
|||
|
||||
useTranslation()
|
||||
|
||||
if (guestAccessLevel === GuestAccessLevel.DENY) {
|
||||
if (guestAccessLevel === GuestAccess.DENY) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,7 @@ export const GuestCard: React.FC = () => {
|
|||
<NewNoteButton />
|
||||
<HistoryButton />
|
||||
</div>
|
||||
{guestAccessLevel !== GuestAccessLevel.CREATE && (
|
||||
{guestAccessLevel !== GuestAccess.CREATE && (
|
||||
<div className={'text-muted mt-2 small'}>
|
||||
<Trans i18nKey={'login.guest.noteCreationDisabled'} />
|
||||
</div>
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment, useMemo } from 'react'
|
||||
import { useFrontendConfig } from '../../common/frontend-config-context/use-frontend-config'
|
||||
import type { AuthProviderWithCustomName } from '../../../api/config/types'
|
||||
import { AuthProviderType } from '../../../api/config/types'
|
||||
import type { AuthProviderWithCustomNameDto } from '@hedgedoc/commons'
|
||||
import { ProviderType } from '@hedgedoc/commons'
|
||||
import { LdapLoginCard } from './ldap-login-card'
|
||||
|
||||
/**
|
||||
|
@ -18,9 +18,9 @@ export const LdapLoginCards: React.FC = () => {
|
|||
|
||||
const ldapProviders = useMemo(() => {
|
||||
return authProviders
|
||||
.filter((provider) => provider.type === AuthProviderType.LDAP)
|
||||
.filter((provider) => provider.type === ProviderType.LDAP)
|
||||
.map((provider) => {
|
||||
const ldapProvider = provider as AuthProviderWithCustomName
|
||||
const ldapProvider = provider as AuthProviderWithCustomNameDto
|
||||
return (
|
||||
<LdapLoginCard
|
||||
providerName={ldapProvider.providerName}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
@ -7,7 +7,7 @@
|
|||
import React, { useMemo } from 'react'
|
||||
import { Card } from 'react-bootstrap'
|
||||
import { useFrontendConfig } from '../../common/frontend-config-context/use-frontend-config'
|
||||
import { AuthProviderType } from '../../../api/config/types'
|
||||
import { ProviderType } from '@hedgedoc/commons'
|
||||
import { LocalLoginCardBody } from './local-login-card-body'
|
||||
import { LocalRegisterCardBody } from './register/local-register-card-body'
|
||||
|
||||
|
@ -18,7 +18,7 @@ export const LocalLoginCard: React.FC = () => {
|
|||
const frontendConfig = useFrontendConfig()
|
||||
|
||||
const localLoginEnabled = useMemo(() => {
|
||||
return frontendConfig.authProviders.some((provider) => provider.type === AuthProviderType.LOCAL)
|
||||
return frontendConfig.authProviders.some((provider) => provider.type === ProviderType.LOCAL)
|
||||
}, [frontendConfig])
|
||||
|
||||
if (!localLoginEnabled) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
@ -40,10 +40,15 @@ export const NewUserCard: React.FC = () => {
|
|||
const submitUserdata = useCallback(
|
||||
(event: FormEvent) => {
|
||||
event.preventDefault()
|
||||
let profilePicture: string | null = null
|
||||
if (pictureChoice === ProfilePictureChoice.PROVIDER && value) {
|
||||
profilePicture = value.photoUrl
|
||||
}
|
||||
|
||||
confirmPendingUser({
|
||||
username,
|
||||
displayName,
|
||||
profilePicture: pictureChoice === ProfilePictureChoice.PROVIDER ? value?.photoUrl : undefined
|
||||
profilePicture
|
||||
})
|
||||
.then(() => fetchAndSetUser())
|
||||
.then(() => {
|
||||
|
@ -51,7 +56,7 @@ export const NewUserCard: React.FC = () => {
|
|||
})
|
||||
.catch(showErrorNotification('login.welcome.error'))
|
||||
},
|
||||
[username, displayName, pictureChoice, router, showErrorNotification, value?.photoUrl]
|
||||
[pictureChoice, value, username, displayName, showErrorNotification, router]
|
||||
)
|
||||
|
||||
const cancelUserCreation = useCallback(() => {
|
||||
|
@ -111,7 +116,7 @@ export const NewUserCard: React.FC = () => {
|
|||
<ProfilePictureSelectField
|
||||
onChange={setPictureChoice}
|
||||
value={pictureChoice}
|
||||
pictureUrl={value?.photoUrl}
|
||||
photoUrl={value?.photoUrl ?? null}
|
||||
username={username}
|
||||
/>
|
||||
<div className={'d-flex gap-3'}>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
@ -16,8 +16,8 @@ import {
|
|||
Mastodon as IconMastodon
|
||||
} from 'react-bootstrap-icons'
|
||||
import { Logger } from '../../../utils/logger'
|
||||
import type { AuthProvider } from '../../../api/config/types'
|
||||
import { AuthProviderType } from '../../../api/config/types'
|
||||
import type { AuthProviderDto } from '@hedgedoc/commons'
|
||||
import { ProviderType } from '@hedgedoc/commons'
|
||||
import { IconGitlab } from '../../common/icons/additional/icon-gitlab'
|
||||
import styles from './one-click-login-button.module.scss'
|
||||
|
||||
|
@ -36,8 +36,8 @@ const logger = new Logger('GetOneClickProviderMetadata')
|
|||
* @param provider The provider for which to retrieve the metadata.
|
||||
* @return Name, icon, URL and CSS class of the given provider for rendering a login button.
|
||||
*/
|
||||
export const getOneClickProviderMetadata = (provider: AuthProvider): OneClickMetadata => {
|
||||
if (provider.type !== AuthProviderType.OIDC) {
|
||||
export const getOneClickProviderMetadata = (provider: AuthProviderDto): OneClickMetadata => {
|
||||
if (provider.type !== ProviderType.OIDC) {
|
||||
logger.warn('Metadata for one-click-provider does not exist', provider)
|
||||
return {
|
||||
name: '',
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { AuthProvider, AuthProviderWithCustomName } from '../../../api/config/types'
|
||||
import type { AuthProviderDto, AuthProviderWithCustomNameDto } from '@hedgedoc/commons'
|
||||
import { IconButton } from '../../common/icon-button/icon-button'
|
||||
import React, { useMemo } from 'react'
|
||||
import { getOneClickProviderMetadata } from './get-one-click-provider-metadata'
|
||||
|
||||
export interface ViaOneClickProps {
|
||||
provider: AuthProvider
|
||||
provider: AuthProviderDto
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -19,7 +19,7 @@ export interface ViaOneClickProps {
|
|||
*/
|
||||
export const OneClickLoginButton: React.FC<ViaOneClickProps> = ({ provider }) => {
|
||||
const { className, icon, url, name } = useMemo(() => getOneClickProviderMetadata(provider), [provider])
|
||||
const text = (provider as AuthProviderWithCustomName).providerName || name
|
||||
const text = (provider as AuthProviderWithCustomNameDto).providerName || name
|
||||
|
||||
return (
|
||||
<IconButton className={className} icon={icon} href={url} title={text} border={true}>
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { AuthProvider } from '../../../api/config/types'
|
||||
import { authProviderTypeOneClick } from '../../../api/config/types'
|
||||
import type { AuthProviderDto } from '@hedgedoc/commons'
|
||||
import { ProviderType } from '@hedgedoc/commons'
|
||||
|
||||
/**
|
||||
* Filters the given auth providers to one-click providers only.
|
||||
* @param authProviders The auth providers to filter
|
||||
* @return only one click auth providers
|
||||
*/
|
||||
export const filterOneClickProviders = (authProviders: AuthProvider[]) => {
|
||||
return authProviders.filter((provider: AuthProvider): boolean => authProviderTypeOneClick.includes(provider.type))
|
||||
export const filterOneClickProviders = (authProviders: AuthProviderDto[]) => {
|
||||
return authProviders.filter((provider: AuthProviderDto): boolean => provider.type === ProviderType.OIDC)
|
||||
}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { AccessTokenWithSecret } from '../../../api/tokens/types'
|
||||
import { cypressId } from '../../../utils/cypress-attribute'
|
||||
import { CopyableField } from '../../common/copyable/copyable-field/copyable-field'
|
||||
import type { ModalVisibilityProps } from '../../common/modals/common-modal'
|
||||
|
@ -11,9 +10,10 @@ import { CommonModal } from '../../common/modals/common-modal'
|
|||
import React from 'react'
|
||||
import { Button, Modal } from 'react-bootstrap'
|
||||
import { Trans } from 'react-i18next'
|
||||
import type { ApiTokenWithSecretDto } from '@hedgedoc/commons'
|
||||
|
||||
export interface AccessTokenCreatedModalProps extends ModalVisibilityProps {
|
||||
tokenWithSecret?: AccessTokenWithSecret
|
||||
tokenWithSecret?: ApiTokenWithSecretDto
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { AccessTokenWithSecret } from '../../../../api/tokens/types'
|
||||
import { AccessTokenCreatedModal } from '../access-token-created-modal'
|
||||
import type { AccessTokenUpdateProps } from '../profile-access-tokens'
|
||||
import { AccessTokenCreationFormExpiryField } from './access-token-creation-form-expiry-field'
|
||||
|
@ -15,6 +14,7 @@ import type { ChangeEvent } from 'react'
|
|||
import React, { Fragment, useCallback, useMemo, useState } from 'react'
|
||||
import { Form } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import type { ApiTokenWithSecretDto } from '@hedgedoc/commons'
|
||||
|
||||
interface NewTokenFormValues {
|
||||
label: string
|
||||
|
@ -38,7 +38,7 @@ export const AccessTokenCreationForm: React.FC<AccessTokenUpdateProps> = ({ onUp
|
|||
}, [expiryDates])
|
||||
|
||||
const [formValues, setFormValues] = useState<NewTokenFormValues>(() => formValuesInitialState)
|
||||
const [newTokenWithSecret, setNewTokenWithSecret] = useState<AccessTokenWithSecret>()
|
||||
const [newTokenWithSecret, setNewTokenWithSecret] = useState<ApiTokenWithSecretDto>()
|
||||
|
||||
const onHideCreatedModal = useCallback(() => {
|
||||
setFormValues(formValuesInitialState)
|
||||
|
|
|
@ -1,40 +1,40 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { postNewAccessToken } from '../../../../../api/tokens'
|
||||
import type { AccessTokenWithSecret } from '../../../../../api/tokens/types'
|
||||
import { postNewAccessToken } from '../../../../../api/api-tokens'
|
||||
import { useUiNotifications } from '../../../../notifications/ui-notification-boundary'
|
||||
import { DateTime } from 'luxon'
|
||||
import type { FormEvent } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import type { ApiTokenWithSecretDto } from '@hedgedoc/commons'
|
||||
|
||||
/**
|
||||
* Callback for requesting a new access token from the API and returning the response token and secret.
|
||||
*
|
||||
* @param label The label for the new access token.
|
||||
* @param expiryDate The expiry date of the new access token.
|
||||
* @param expiryDateStr The expiry date of the new access token.
|
||||
* @param setNewTokenWithSecret Callback to set the new access token with the secret from the API.
|
||||
* @return Callback that can be called when the new access token should be requested.
|
||||
*/
|
||||
export const useOnCreateToken = (
|
||||
label: string,
|
||||
expiryDate: string,
|
||||
setNewTokenWithSecret: (token: AccessTokenWithSecret) => void
|
||||
expiryDateStr: string,
|
||||
setNewTokenWithSecret: (token: ApiTokenWithSecretDto) => void
|
||||
): ((event: FormEvent) => void) => {
|
||||
const { showErrorNotification } = useUiNotifications()
|
||||
|
||||
return useCallback(
|
||||
(event: FormEvent) => {
|
||||
event.preventDefault()
|
||||
const expiryInMillis = DateTime.fromFormat(expiryDate, 'yyyy-MM-dd').toMillis()
|
||||
postNewAccessToken(label, expiryInMillis)
|
||||
const expiryDate = DateTime.fromFormat(expiryDateStr, 'yyyy-MM-dd').toJSDate()
|
||||
postNewAccessToken(label, expiryDate)
|
||||
.then((tokenWithSecret) => {
|
||||
setNewTokenWithSecret(tokenWithSecret)
|
||||
})
|
||||
.catch(showErrorNotification('profile.accessTokens.creationFailed'))
|
||||
},
|
||||
[expiryDate, label, setNewTokenWithSecret, showErrorNotification]
|
||||
[expiryDateStr, label, setNewTokenWithSecret, showErrorNotification]
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { deleteAccessToken } from '../../../api/tokens'
|
||||
import type { AccessToken } from '../../../api/tokens/types'
|
||||
import { deleteAccessToken } from '../../../api/api-tokens'
|
||||
import { cypressId } from '../../../utils/cypress-attribute'
|
||||
import type { ModalVisibilityProps } from '../../common/modals/common-modal'
|
||||
import { CommonModal } from '../../common/modals/common-modal'
|
||||
|
@ -12,9 +11,10 @@ import { useUiNotifications } from '../../notifications/ui-notification-boundary
|
|||
import React, { useCallback } from 'react'
|
||||
import { Button, Modal } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import type { ApiTokenDto } from '@hedgedoc/commons'
|
||||
|
||||
export interface AccessTokenDeletionModalProps extends ModalVisibilityProps {
|
||||
token: AccessToken
|
||||
token: ApiTokenDto
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { AccessToken } from '../../../api/tokens/types'
|
||||
import { useBooleanState } from '../../../hooks/common/use-boolean-state'
|
||||
import { cypressId } from '../../../utils/cypress-attribute'
|
||||
import { IconButton } from '../../common/icon-button/icon-button'
|
||||
|
@ -14,9 +13,10 @@ import React, { useCallback, useMemo } from 'react'
|
|||
import { Col, ListGroup, Row } from 'react-bootstrap'
|
||||
import { Trash as IconTrash } from 'react-bootstrap-icons'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import type { ApiTokenDto } from '@hedgedoc/commons'
|
||||
|
||||
export interface AccessTokenListEntryProps {
|
||||
token: AccessToken
|
||||
token: ApiTokenDto
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { getAccessTokenList } from '../../../api/tokens'
|
||||
import type { AccessToken } from '../../../api/tokens/types'
|
||||
import { getAccessTokenList } from '../../../api/api-tokens'
|
||||
import { useUiNotifications } from '../../notifications/ui-notification-boundary'
|
||||
import { AccessTokenCreationForm } from './access-token-creation-form/access-token-creation-form'
|
||||
import { AccessTokenListEntry } from './access-token-list-entry'
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Card, ListGroup } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import type { ApiTokenDto } from '@hedgedoc/commons'
|
||||
|
||||
export interface AccessTokenUpdateProps {
|
||||
onUpdateList: () => void
|
||||
|
@ -21,7 +21,7 @@ export interface AccessTokenUpdateProps {
|
|||
*/
|
||||
export const ProfileAccessTokens: React.FC = () => {
|
||||
useTranslation()
|
||||
const [accessTokens, setAccessTokens] = useState<AccessToken[]>([])
|
||||
const [accessTokens, setAccessTokens] = useState<ApiTokenDto[]>([])
|
||||
const { showErrorNotification } = useUiNotifications()
|
||||
|
||||
const refreshAccessTokens = useCallback(() => {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { updateDisplayName } from '../../../api/me'
|
||||
import { updateUser } from '../../../api/me'
|
||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
||||
import { useOnInputChange } from '../../../hooks/common/use-on-input-change'
|
||||
import { DisplayNameField } from '../../common/fields/display-name-field'
|
||||
|
@ -27,7 +27,7 @@ export const ProfileDisplayName: React.FC = () => {
|
|||
const onSubmitNameChange = useCallback(
|
||||
(event: FormEvent) => {
|
||||
event.preventDefault()
|
||||
updateDisplayName(displayName)
|
||||
updateUser(displayName, null)
|
||||
.then(fetchAndSetUser)
|
||||
.catch(showErrorNotification('profile.changeDisplayNameFailed'))
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue