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:
Erik Michelson 2025-03-29 13:51:02 +01:00 committed by Philip Molares
parent deee8e885f
commit e411ddf099
121 changed files with 620 additions and 819 deletions

View file

@ -1,11 +1,11 @@
/*
* 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 { NotePermissions } from '@hedgedoc/commons'
import type { NotePermissionsDto } from '@hedgedoc/commons'
const mockPermissionChangeApiRoutes = (permission: NotePermissions) => {
const mockPermissionChangeApiRoutes = (permission: NotePermissionsDto) => {
cy.intercept('PUT', 'api/private/notes/mock-note/metadata/permissions/groups/_EVERYONE', {
statusCode: 200,
body: permission

View file

@ -1,12 +1,12 @@
/*
* 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 { AuthProvider } from '../../src/api/config/types'
import { AuthProviderType } from '../../src/api/config/types'
import type { ProviderType } from '../../src/api/config/types'
import { ProviderType } from '../../src/api/config/types'
const initLoggedOutTestWithCustomAuthProviders = (cy: Cypress.cy, enabledProviders: AuthProvider[]) => {
const initLoggedOutTestWithCustomAuthProviders = (cy: Cypress.cy, enabledProviders: ProviderType[]) => {
cy.logOut()
cy.loadConfig({
authProviders: enabledProviders
@ -48,7 +48,7 @@ describe('When logged-out ', () => {
it('sign-in button points to login route: internal', () => {
initLoggedOutTestWithCustomAuthProviders(cy, [
{
type: AuthProviderType.LOCAL
type: ProviderType.LOCAL
}
])
cy.getByCypressId('sign-in-button')
@ -60,7 +60,7 @@ describe('When logged-out ', () => {
it('sign-in button points to login route: ldap', () => {
initLoggedOutTestWithCustomAuthProviders(cy, [
{
type: AuthProviderType.LDAP,
type: ProviderType.LDAP,
identifier: 'cy-ldap',
providerName: 'cy LDAP'
}
@ -76,7 +76,7 @@ describe('When logged-out ', () => {
it('sign-in button points to auth-provider', () => {
initLoggedOutTestWithCustomAuthProviders(cy, [
{
type: AuthProviderType.OIDC,
type: ProviderType.OIDC,
identifier: 'github',
providerName: 'GitHub',
theme: 'github'
@ -94,13 +94,13 @@ describe('When logged-out ', () => {
it('sign-in button points to login route', () => {
initLoggedOutTestWithCustomAuthProviders(cy, [
{
type: AuthProviderType.OIDC,
type: ProviderType.OIDC,
identifier: 'github',
providerName: 'GitHub',
theme: 'github'
},
{
type: AuthProviderType.OIDC,
type: ProviderType.OIDC,
identifier: 'gitlab',
providerName: 'GitLab',
theme: 'gitlab'
@ -117,13 +117,13 @@ describe('When logged-out ', () => {
it('sign-in button points to login route', () => {
initLoggedOutTestWithCustomAuthProviders(cy, [
{
type: AuthProviderType.OIDC,
type: ProviderType.OIDC,
identifier: 'github',
providerName: 'GitHub',
theme: 'github'
},
{
type: AuthProviderType.LOCAL
type: ProviderType.LOCAL
}
])
cy.getByCypressId('sign-in-button')

View file

@ -1,9 +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 { AuthProviderType } from '../../src/api/config/types'
import { ProviderType } from '@hedgedoc/commons'
import { HttpMethod } from '../../src/handler-utils/respond-to-matching-request'
import { IGNORE_MOTD, MOTD_LOCAL_STORAGE_KEY } from '../../src/components/global-dialogs/motd-modal/local-storage-keys'
@ -22,15 +22,15 @@ export const branding = {
export const authProviders = [
{
type: AuthProviderType.LOCAL
type: ProviderType.LOCAL
},
{
type: AuthProviderType.LDAP,
type: ProviderType.LDAP,
identifier: 'test-ldap',
providerName: 'Test LDAP'
},
{
type: AuthProviderType.OIDC,
type: ProviderType.OIDC,
identifier: 'test-oidc',
providerName: 'Test OIDC'
}

View file

@ -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 { Note } from '../../src/api/notes/types'
import type { NoteDto } from '@hedgedoc/commons'
export const testNoteId = 'test'
const mockMetadata = {
@ -37,6 +37,6 @@ beforeEach(() => {
content: '',
metadata: mockMetadata,
editedByAtPosition: []
} as Note)
} as NoteDto)
cy.intercept(`api/private/notes/${testNoteId}/metadata`, mockMetadata)
})

View file

@ -1,12 +1,12 @@
/*
* 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 { DeleteApiRequestBuilder } from '../common/api-request-builder/delete-api-request-builder'
import { PostApiRequestBuilder } from '../common/api-request-builder/post-api-request-builder'
import { PutApiRequestBuilder } from '../common/api-request-builder/put-api-request-builder'
import type { Alias, NewAliasDto, PrimaryAliasDto } from './types'
import type { AliasDto, AliasCreateDto, AliasUpdateDto } from '@hedgedoc/commons'
/**
* Adds an alias to an existing note.
@ -16,8 +16,8 @@ import type { Alias, NewAliasDto, PrimaryAliasDto } from './types'
* @return Information about the newly created alias.
* @throws {Error} when the api request wasn't successful
*/
export const addAlias = async (noteIdOrAlias: string, newAlias: string): Promise<Alias> => {
const response = await new PostApiRequestBuilder<Alias, NewAliasDto>('alias')
export const addAlias = async (noteIdOrAlias: string, newAlias: string): Promise<AliasDto> => {
const response = await new PostApiRequestBuilder<AliasDto, AliasCreateDto>('alias')
.withJsonBody({
noteIdOrAlias,
newAlias
@ -34,8 +34,8 @@ export const addAlias = async (noteIdOrAlias: string, newAlias: string): Promise
* @return The updated information about the alias.
* @throws {Error} when the api request wasn't successfull
*/
export const markAliasAsPrimary = async (alias: string): Promise<Alias> => {
const response = await new PutApiRequestBuilder<Alias, PrimaryAliasDto>('alias/' + alias)
export const markAliasAsPrimary = async (alias: string): Promise<AliasDto> => {
const response = await new PutApiRequestBuilder<AliasDto, AliasUpdateDto>('alias/' + alias)
.withJsonBody({
primaryAlias: true
})

View file

@ -1,19 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export interface Alias {
name: string
primaryAlias: boolean
noteId: string
}
export interface NewAliasDto {
noteIdOrAlias: string
newAlias: string
}
export interface PrimaryAliasDto {
primaryAlias: boolean
}

View file

@ -1,12 +1,12 @@
/*
* 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 { DeleteApiRequestBuilder } from '../common/api-request-builder/delete-api-request-builder'
import { GetApiRequestBuilder } from '../common/api-request-builder/get-api-request-builder'
import { PostApiRequestBuilder } from '../common/api-request-builder/post-api-request-builder'
import type { AccessToken, AccessTokenWithSecret, CreateAccessTokenDto } from './types'
import type { ApiTokenCreateDto, ApiTokenDto, ApiTokenWithSecretDto } from '@hedgedoc/commons'
/**
* Retrieves the access tokens for the current user.
@ -14,8 +14,8 @@ import type { AccessToken, AccessTokenWithSecret, CreateAccessTokenDto } from '.
* @return List of access token metadata.
* @throws {Error} when the api request wasn't successful.
*/
export const getAccessTokenList = async (): Promise<AccessToken[]> => {
const response = await new GetApiRequestBuilder<AccessToken[]>('tokens').sendRequest()
export const getAccessTokenList = async (): Promise<ApiTokenDto[]> => {
const response = await new GetApiRequestBuilder<ApiTokenDto[]>('tokens').sendRequest()
return response.asParsedJsonObject()
}
@ -27,8 +27,8 @@ export const getAccessTokenList = async (): Promise<AccessToken[]> => {
* @return The new access token metadata along with its secret.
* @throws {Error} when the api request wasn't successful.
*/
export const postNewAccessToken = async (label: string, validUntil: number): Promise<AccessTokenWithSecret> => {
const response = await new PostApiRequestBuilder<AccessTokenWithSecret, CreateAccessTokenDto>('tokens')
export const postNewAccessToken = async (label: string, validUntil: Date): Promise<ApiTokenWithSecretDto> => {
const response = await new PostApiRequestBuilder<ApiTokenWithSecretDto, ApiTokenCreateDto>('tokens')
.withJsonBody({
label,
validUntil

View file

@ -1,11 +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 { DeleteApiRequestBuilder } from '../common/api-request-builder/delete-api-request-builder'
import { PostApiRequestBuilder } from '../common/api-request-builder/post-api-request-builder'
import type { LogoutResponseDto, UsernameCheckDto, UsernameCheckResponseDto } from './types'
import type { LogoutResponseDto, UsernameCheckDto, UsernameCheckResponseDto } from '@hedgedoc/commons'
/**
* Requests to log out the current user.

View file

@ -1,10 +1,10 @@
/*
* 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 { PostApiRequestBuilder } from '../common/api-request-builder/post-api-request-builder'
import type { LdapLoginResponseDto, LoginDto } from './types'
import type { LdapLoginDto, LdapLoginResponseDto } from '@hedgedoc/commons'
/**
* Requests to log in a user via LDAP credentials.
@ -19,7 +19,7 @@ export const doLdapLogin = async (
username: string,
password: string
): Promise<LdapLoginResponseDto> => {
const response = await new PostApiRequestBuilder<LdapLoginResponseDto, LoginDto>(`auth/ldap/${provider}/login`)
const response = await new PostApiRequestBuilder<LdapLoginResponseDto, LdapLoginDto>(`auth/ldap/${provider}/login`)
.withJsonBody({
username: username,
password: password

View file

@ -1,11 +1,11 @@
/*
* 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 { PostApiRequestBuilder } from '../common/api-request-builder/post-api-request-builder'
import { PutApiRequestBuilder } from '../common/api-request-builder/put-api-request-builder'
import type { ChangePasswordDto, LoginDto, RegisterDto } from './types'
import type { UpdatePasswordDto, LoginDto, RegisterDto } from '@hedgedoc/commons'
/**
* Requests to do a local login with a provided username and password.
@ -54,7 +54,7 @@ export const doLocalRegister = async (username: string, displayName: string, pas
* @throws {AuthError.LOGIN_DISABLED} when local login is disabled on the backend.
*/
export const doLocalPasswordChange = async (currentPassword: string, newPassword: string): Promise<void> => {
await new PutApiRequestBuilder<void, ChangePasswordDto>('auth/local')
await new PutApiRequestBuilder<void, UpdatePasswordDto>('auth/local')
.withJsonBody({
currentPassword,
newPassword

View file

@ -1,20 +1,20 @@
/*
* 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 { FullUserInfo } from '../users/types'
import type { FullUserInfoDto } from '@hedgedoc/commons'
import { GetApiRequestBuilder } from '../common/api-request-builder/get-api-request-builder'
import { DeleteApiRequestBuilder } from '../common/api-request-builder/delete-api-request-builder'
import type { PendingUserConfirmDto } from './types'
import type { PendingUserConfirmationDto } from '@hedgedoc/commons'
import { PutApiRequestBuilder } from '../common/api-request-builder/put-api-request-builder'
/**
* Fetches the pending user information.
* @returns The pending user information.
*/
export const getPendingUserInfo = async (): Promise<Partial<FullUserInfo>> => {
const response = await new GetApiRequestBuilder<Partial<FullUserInfo>>('auth/pending-user').sendRequest()
export const getPendingUserInfo = async (): Promise<FullUserInfoDto> => {
const response = await new GetApiRequestBuilder<FullUserInfoDto>('auth/pending-user').sendRequest()
return response.asParsedJsonObject()
}
@ -29,8 +29,8 @@ export const cancelPendingUser = async (): Promise<void> => {
* Confirms the pending user with updated user information.
* @param updatedUserInfo The updated user information.
*/
export const confirmPendingUser = async (updatedUserInfo: PendingUserConfirmDto): Promise<void> => {
await new PutApiRequestBuilder<void, PendingUserConfirmDto>('auth/pending-user')
export const confirmPendingUser = async (updatedUserInfo: PendingUserConfirmationDto): Promise<void> => {
await new PutApiRequestBuilder<void, PendingUserConfirmationDto>('auth/pending-user')
.withJsonBody(updatedUserInfo)
.sendRequest()
}

View file

@ -1,43 +0,0 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export interface LoginDto {
username: string
password: string
}
export interface RegisterDto {
username: string
password: string
displayName: string
}
export interface ChangePasswordDto {
currentPassword: string
newPassword: string
}
export interface LogoutResponseDto {
redirect: string
}
export interface UsernameCheckDto {
username: string
}
export interface UsernameCheckResponseDto {
usernameAvailable: boolean
}
export interface PendingUserConfirmDto {
username: string
displayName: string
profilePicture: string | undefined
}
export interface LdapLoginResponseDto {
newUser: boolean
}

View file

@ -1,11 +1,11 @@
/*
* 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 { GetApiRequestBuilder } from '../common/api-request-builder/get-api-request-builder'
import type { FrontendConfig } from './types'
import { isBuildTime } from '../../utils/test-modes'
import type { FrontendConfigDto } from '@hedgedoc/commons'
/**
* Fetches the frontend config from the backend.
@ -13,10 +13,10 @@ import { isBuildTime } from '../../utils/test-modes'
* @return The frontend config.
* @throws {Error} when the api request wasn't successful.
*/
export const getConfig = async (baseUrl?: string): Promise<FrontendConfig | undefined> => {
export const getConfig = async (baseUrl?: string): Promise<FrontendConfigDto | undefined> => {
if (isBuildTime) {
return undefined
}
const response = await new GetApiRequestBuilder<FrontendConfig>('config', baseUrl).sendRequest()
const response = await new GetApiRequestBuilder<FrontendConfigDto>('config', baseUrl).sendRequest()
return response.asParsedJsonObject()
}

View file

@ -1,70 +0,0 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export interface FrontendConfig {
allowRegister: boolean
allowProfileEdits: boolean
allowChooseUsername: boolean
authProviders: AuthProvider[]
branding: BrandingConfig
guestAccess: GuestAccessLevel
useImageProxy: boolean
specialUrls: SpecialUrls
version: BackendVersion
plantumlServer?: string
maxDocumentLength: number
}
export enum GuestAccessLevel {
DENY = 'deny',
READ = 'read',
WRITE = 'write',
CREATE = 'create'
}
export enum AuthProviderType {
OIDC = 'oidc',
LDAP = 'ldap',
LOCAL = 'local'
}
export type AuthProviderTypeWithCustomName = AuthProviderType.LDAP | AuthProviderType.OIDC
export type AuthProviderTypeWithoutCustomName = AuthProviderType.LOCAL
export const authProviderTypeOneClick = [AuthProviderType.OIDC]
export interface AuthProviderWithCustomName {
type: AuthProviderTypeWithCustomName
identifier: string
providerName: string
theme?: string
}
export interface AuthProviderWithoutCustomName {
type: AuthProviderTypeWithoutCustomName
}
export type AuthProvider = AuthProviderWithCustomName | AuthProviderWithoutCustomName
export interface BrandingConfig {
name?: string
logo?: string
}
export interface BackendVersion {
major: number
minor: number
patch: number
preRelease?: string
commit?: string
}
export interface SpecialUrls {
privacy?: string
termsOfUse?: string
imprint?: string
}

View file

@ -1,10 +1,10 @@
/*
* 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 { GetApiRequestBuilder } from '../common/api-request-builder/get-api-request-builder'
import type { GroupInfo } from './types'
import type { GroupInfoDto } from '@hedgedoc/commons'
/**
* Retrieves information about a group with a given name.
@ -13,7 +13,7 @@ import type { GroupInfo } from './types'
* @return Information about the group.
* @throws {Error} when the api request wasn't successful.
*/
export const getGroup = async (groupName: string): Promise<GroupInfo> => {
const response = await new GetApiRequestBuilder<GroupInfo>('groups/' + groupName).sendRequest()
export const getGroup = async (groupName: string): Promise<GroupInfoDto> => {
const response = await new GetApiRequestBuilder<GroupInfoDto>('groups/' + groupName).sendRequest()
return response.asParsedJsonObject()
}

View file

@ -1,11 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export interface GroupInfo {
name: string
displayName: string
special: boolean
}

View file

@ -1,13 +1,12 @@
/*
* 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 { DeleteApiRequestBuilder } from '../common/api-request-builder/delete-api-request-builder'
import { GetApiRequestBuilder } from '../common/api-request-builder/get-api-request-builder'
import { PostApiRequestBuilder } from '../common/api-request-builder/post-api-request-builder'
import type { MediaUpload } from '../media/types'
import type { ChangeDisplayNameDto, LoginUserInfo } from './types'
import type { UpdateUserInfoDto, LoginUserInfoDto, MediaUploadDto } from '@hedgedoc/commons'
/**
* Returns metadata about the currently signed-in user from the API.
@ -15,8 +14,8 @@ import type { ChangeDisplayNameDto, LoginUserInfo } from './types'
* @return The user metadata.
* @throws {Error} when the user is not signed-in.
*/
export const getMe = async (): Promise<LoginUserInfo> => {
const response = await new GetApiRequestBuilder<LoginUserInfo>('me').sendRequest()
export const getMe = async (): Promise<LoginUserInfoDto> => {
const response = await new GetApiRequestBuilder<LoginUserInfoDto>('me').sendRequest()
return response.asParsedJsonObject()
}
@ -33,12 +32,14 @@ export const deleteUser = async (): Promise<void> => {
* Changes the display name of the current user.
*
* @param displayName The new display name to set.
* @param email The new email to set.
* @throws {Error} when the api request wasn't successful.
*/
export const updateDisplayName = async (displayName: string): Promise<void> => {
await new PostApiRequestBuilder<void, ChangeDisplayNameDto>('me/profile')
export const updateUser = async (displayName: string | null, email: string | null): Promise<void> => {
await new PostApiRequestBuilder<void, UpdateUserInfoDto>('me/profile')
.withJsonBody({
displayName
displayName,
email
})
.sendRequest()
}
@ -49,7 +50,7 @@ export const updateDisplayName = async (displayName: string): Promise<void> => {
* @return List of media object information.
* @throws {Error} when the api request wasn't successful.
*/
export const getMyMedia = async (): Promise<MediaUpload[]> => {
const response = await new GetApiRequestBuilder<MediaUpload[]>('me/media').sendRequest()
export const getMyMedia = async (): Promise<MediaUploadDto[]> => {
const response = await new GetApiRequestBuilder<MediaUploadDto[]>('me/media').sendRequest()
return response.asParsedJsonObject()
}

View file

@ -1,15 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { UserInfo } from '../users/types'
export interface LoginUserInfo extends UserInfo {
authProvider: string
email: string
}
export interface ChangeDisplayNameDto {
displayName: string
}

View file

@ -1,11 +1,12 @@
/*
* 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 { DeleteApiRequestBuilder } from '../common/api-request-builder/delete-api-request-builder'
import { PostApiRequestBuilder } from '../common/api-request-builder/post-api-request-builder'
import type { ImageProxyRequestDto, ImageProxyResponse, MediaUpload } from './types'
import type { ImageProxyRequestDto, ImageProxyResponse } from './types'
import type { MediaUploadDto } from '@hedgedoc/commons'
/**
* Requests an image-proxy URL from the backend for a given image URL.
@ -31,10 +32,10 @@ export const getProxiedUrl = async (imageUrl: string): Promise<ImageProxyRespons
* @return The URL of the uploaded media object.
* @throws {Error} when the api request wasn't successful.
*/
export const uploadFile = async (noteIdOrAlias: string, media: File): Promise<MediaUpload> => {
export const uploadFile = async (noteIdOrAlias: string, media: File): Promise<MediaUploadDto> => {
const postData = new FormData()
postData.append('file', media)
const response = await new PostApiRequestBuilder<MediaUpload, void>('media')
const response = await new PostApiRequestBuilder<MediaUploadDto, void>('media')
.withHeader('HedgeDoc-Note', noteIdOrAlias)
.withBody(postData)
.sendRequest()

View file

@ -1,16 +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
*/
export interface MediaUpload {
uuid: string
fileName: string
noteId: string | null
createdAt: string
username: string | null
}
export interface ImageProxyResponse {
url: string
}

View file

@ -1,13 +1,13 @@
/*
* 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 { DeleteApiRequestBuilder } from '../common/api-request-builder/delete-api-request-builder'
import { GetApiRequestBuilder } from '../common/api-request-builder/get-api-request-builder'
import { PostApiRequestBuilder } from '../common/api-request-builder/post-api-request-builder'
import type { MediaUpload } from '../media/types'
import type { Note, NoteDeletionOptions, NoteMetadata } from './types'
import type { MediaUploadDto, NoteDto, NoteMetadataDto } from '@hedgedoc/commons'
import type { NoteMediaDeletionDto } from '@hedgedoc/commons/dist/esm'
/**
* Retrieves the content and metadata about the specified note.
@ -16,8 +16,8 @@ import type { Note, NoteDeletionOptions, NoteMetadata } from './types'
* @return Content and metadata of the specified note.
* @throws {Error} when the api request wasn't successful.
*/
export const getNote = async (noteIdOrAlias: string, baseUrl?: string): Promise<Note> => {
const response = await new GetApiRequestBuilder<Note>('notes/' + noteIdOrAlias, baseUrl).sendRequest()
export const getNote = async (noteIdOrAlias: string, baseUrl?: string): Promise<NoteDto> => {
const response = await new GetApiRequestBuilder<NoteDto>('notes/' + noteIdOrAlias, baseUrl).sendRequest()
return response.asParsedJsonObject()
}
@ -27,8 +27,8 @@ export const getNote = async (noteIdOrAlias: string, baseUrl?: string): Promise<
* @param noteIdOrAlias The id or alias of the note.
* @return Metadata of the specified note.
*/
export const getNoteMetadata = async (noteIdOrAlias: string): Promise<NoteMetadata> => {
const response = await new GetApiRequestBuilder<NoteMetadata>(`notes/${noteIdOrAlias}/metadata`).sendRequest()
export const getNoteMetadata = async (noteIdOrAlias: string): Promise<NoteMetadataDto> => {
const response = await new GetApiRequestBuilder<NoteMetadataDto>(`notes/${noteIdOrAlias}/metadata`).sendRequest()
return response.asParsedJsonObject()
}
@ -39,8 +39,8 @@ export const getNoteMetadata = async (noteIdOrAlias: string): Promise<NoteMetada
* @return List of media object metadata associated with specified note.
* @throws {Error} when the api request wasn't successful.
*/
export const getMediaForNote = async (noteIdOrAlias: string): Promise<MediaUpload[]> => {
const response = await new GetApiRequestBuilder<MediaUpload[]>(`notes/${noteIdOrAlias}/media`).sendRequest()
export const getMediaForNote = async (noteIdOrAlias: string): Promise<MediaUploadDto[]> => {
const response = await new GetApiRequestBuilder<MediaUploadDto[]>(`notes/${noteIdOrAlias}/media`).sendRequest()
return response.asParsedJsonObject()
}
@ -51,8 +51,8 @@ export const getMediaForNote = async (noteIdOrAlias: string): Promise<MediaUploa
* @return Content and metadata of the new note.
* @throws {Error} when the api request wasn't successful.
*/
export const createNote = async (markdown: string): Promise<Note> => {
const response = await new PostApiRequestBuilder<Note, void>('notes')
export const createNote = async (markdown: string): Promise<NoteDto> => {
const response = await new PostApiRequestBuilder<NoteDto, void>('notes')
.withHeader('Content-Type', 'text/markdown')
.withBody(markdown)
.sendRequest()
@ -67,8 +67,8 @@ export const createNote = async (markdown: string): Promise<Note> => {
* @return Content and metadata of the new note.
* @throws {Error} when the api request wasn't successful.
*/
export const createNoteWithPrimaryAlias = async (markdown: string, primaryAlias: string): Promise<Note> => {
const response = await new PostApiRequestBuilder<Note, void>('notes/' + primaryAlias)
export const createNoteWithPrimaryAlias = async (markdown: string, primaryAlias: string): Promise<NoteDto> => {
const response = await new PostApiRequestBuilder<NoteDto, void>('notes/' + primaryAlias)
.withHeader('Content-Type', 'text/markdown')
.withBody(markdown)
.sendRequest()
@ -83,7 +83,7 @@ export const createNoteWithPrimaryAlias = async (markdown: string, primaryAlias:
* @throws {Error} when the api request wasn't successful.
*/
export const deleteNote = async (noteIdOrAlias: string, keepMedia: boolean): Promise<void> => {
await new DeleteApiRequestBuilder<void, NoteDeletionOptions>('notes/' + noteIdOrAlias)
await new DeleteApiRequestBuilder<void, NoteMediaDeletionDto>('notes/' + noteIdOrAlias)
.withJsonBody({
keepMedia
})

View file

@ -1,41 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { Alias } from '../alias/types'
import type { NotePermissions } from '@hedgedoc/commons'
export interface Note {
content: string
metadata: NoteMetadata
editedByAtPosition: NoteEdit[]
}
export interface NoteMetadata {
id: string
aliases: Alias[]
primaryAddress: string
title: string
description: string
tags: string[]
updatedAt: string
updateUsername: string | null
viewCount: number
createdAt: string
editedBy: string[]
permissions: NotePermissions
version: number
}
export interface NoteEdit {
username: string | null
startPos: number
endPos: number
createdAt: string
updatedAt: string
}
export interface NoteDeletionOptions {
keepMedia: boolean
}

View file

@ -1,27 +1,31 @@
/*
* 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 { DeleteApiRequestBuilder } from '../common/api-request-builder/delete-api-request-builder'
import { PutApiRequestBuilder } from '../common/api-request-builder/put-api-request-builder'
import type { OwnerChangeDto, PermissionSetDto } from './types'
import type { NotePermissions } from '@hedgedoc/commons'
import type {
ChangeNoteOwnerDto,
NoteGroupPermissionUpdateDto,
NotePermissionsDto,
NoteUserPermissionUpdateDto
} from '@hedgedoc/commons'
/**
* Sets the owner of a note.
*
* @param noteId The id of the note.
* @param newOwner The username of the new owner.
* @return The updated {@link NotePermissions}.
* @return The updated {@link NotePermissionsDto}.
* @throws {Error} when the api request wasn't successful.
*/
export const setNoteOwner = async (noteId: string, newOwner: string): Promise<NotePermissions> => {
const response = await new PutApiRequestBuilder<NotePermissions, OwnerChangeDto>(
export const setNoteOwner = async (noteId: string, newOwner: string): Promise<NotePermissionsDto> => {
const response = await new PutApiRequestBuilder<NotePermissionsDto, ChangeNoteOwnerDto>(
`notes/${noteId}/metadata/permissions/owner`
)
.withJsonBody({
newOwner
owner: newOwner
})
.sendRequest()
return response.asParsedJsonObject()
@ -33,15 +37,15 @@ export const setNoteOwner = async (noteId: string, newOwner: string): Promise<No
* @param noteId The id of the note.
* @param username The username of the user to set the permission for.
* @param canEdit true if the user should be able to update the note, false otherwise.
* @return The updated {@link NotePermissions}.
* @return The updated {@link NotePermissionsDto}.
* @throws {Error} when the api request wasn't successful.
*/
export const setUserPermission = async (
noteId: string,
username: string,
canEdit: boolean
): Promise<NotePermissions> => {
const response = await new PutApiRequestBuilder<NotePermissions, PermissionSetDto>(
): Promise<NotePermissionsDto> => {
const response = await new PutApiRequestBuilder<NotePermissionsDto, Pick<NoteUserPermissionUpdateDto, 'canEdit'>>(
`notes/${noteId}/metadata/permissions/users/${username}`
)
.withJsonBody({
@ -57,15 +61,15 @@ export const setUserPermission = async (
* @param noteId The id of the note.
* @param groupName The name of the group to set the permission for.
* @param canEdit true if the group should be able to update the note, false otherwise.
* @return The updated {@link NotePermissions}.
* @return The updated {@link NotePermissionsDto}.
* @throws {Error} when the api request wasn't successful.
*/
export const setGroupPermission = async (
noteId: string,
groupName: string,
canEdit: boolean
): Promise<NotePermissions> => {
const response = await new PutApiRequestBuilder<NotePermissions, PermissionSetDto>(
): Promise<NotePermissionsDto> => {
const response = await new PutApiRequestBuilder<NotePermissionsDto, Pick<NoteGroupPermissionUpdateDto, 'canEdit'>>(
`notes/${noteId}/metadata/permissions/groups/${groupName}`
)
.withJsonBody({
@ -80,11 +84,11 @@ export const setGroupPermission = async (
*
* @param noteId The id of the note.
* @param username The name of the user to remove the permission of.
* @return The updated {@link NotePermissions}.
* @return The updated {@link NotePermissionsDto}.
* @throws {Error} when the api request wasn't successful.
*/
export const removeUserPermission = async (noteId: string, username: string): Promise<NotePermissions> => {
const response = await new DeleteApiRequestBuilder<NotePermissions>(
export const removeUserPermission = async (noteId: string, username: string): Promise<NotePermissionsDto> => {
const response = await new DeleteApiRequestBuilder<NotePermissionsDto>(
`notes/${noteId}/metadata/permissions/users/${username}`
).sendRequest()
return response.asParsedJsonObject()
@ -95,11 +99,11 @@ export const removeUserPermission = async (noteId: string, username: string): Pr
*
* @param noteId The id of the note.
* @param groupName The name of the group to remove the permission of.
* @return The updated {@link NotePermissions}.
* @return The updated {@link NotePermissionsDto}.
* @throws {Error} when the api request wasn't successful.
*/
export const removeGroupPermission = async (noteId: string, groupName: string): Promise<NotePermissions> => {
const response = await new DeleteApiRequestBuilder<NotePermissions>(
export const removeGroupPermission = async (noteId: string, groupName: string): Promise<NotePermissionsDto> => {
const response = await new DeleteApiRequestBuilder<NotePermissionsDto>(
`notes/${noteId}/metadata/permissions/groups/${groupName}`
).sendRequest()
return response.asParsedJsonObject()

View file

@ -1,12 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export interface OwnerChangeDto {
newOwner: string
}
export interface PermissionSetDto {
canEdit: boolean
}

View file

@ -1,11 +1,11 @@
/*
* 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 { DeleteApiRequestBuilder } from '../common/api-request-builder/delete-api-request-builder'
import { GetApiRequestBuilder } from '../common/api-request-builder/get-api-request-builder'
import type { RevisionDetails, RevisionMetadata } from './types'
import type { RevisionDto, RevisionMetadataDto } from '@hedgedoc/commons'
/**
* Retrieves a note revision while using a cache for often retrieved revisions.
@ -15,10 +15,8 @@ import type { RevisionDetails, RevisionMetadata } from './types'
* @return The revision.
* @throws {Error} when the api request wasn't successful.
*/
export const getRevision = async (noteId: string, revisionId: number): Promise<RevisionDetails> => {
const response = await new GetApiRequestBuilder<RevisionDetails>(
`notes/${noteId}/revisions/${revisionId}`
).sendRequest()
export const getRevision = async (noteId: string, revisionId: number): Promise<RevisionDto> => {
const response = await new GetApiRequestBuilder<RevisionDto>(`notes/${noteId}/revisions/${revisionId}`).sendRequest()
return response.asParsedJsonObject()
}
@ -29,8 +27,8 @@ export const getRevision = async (noteId: string, revisionId: number): Promise<R
* @return A list of revision ids.
* @throws {Error} when the api request wasn't successful.
*/
export const getAllRevisions = async (noteId: string): Promise<RevisionMetadata[]> => {
const response = await new GetApiRequestBuilder<RevisionMetadata[]>(`notes/${noteId}/revisions`).sendRequest()
export const getAllRevisions = async (noteId: string): Promise<RevisionMetadataDto[]> => {
const response = await new GetApiRequestBuilder<RevisionMetadataDto[]>(`notes/${noteId}/revisions`).sendRequest()
return response.asParsedJsonObject()
}

View file

@ -1,23 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { NoteEdit } from '../notes/types'
export interface RevisionDetails extends RevisionMetadata {
content: string
patch: string
edits: NoteEdit[]
}
export interface RevisionMetadata {
id: number
createdAt: string
length: number
authorUsernames: string[]
anonymousAuthorCount: number
title: string
tags: string[]
description: string
}

View file

@ -1,22 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export interface AccessToken {
label: string
validUntil: string
keyId: string
createdAt: string
lastUsedAt: string | null
}
export interface AccessTokenWithSecret extends AccessToken {
secret: string
}
export interface CreateAccessTokenDto {
label: string
validUntil: number
}

View file

@ -1,10 +1,10 @@
/*
* 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 { GetApiRequestBuilder } from '../common/api-request-builder/get-api-request-builder'
import type { UserInfo } from './types'
import type { UserInfoDto } from '@hedgedoc/commons'
/**
* Retrieves information about a specific user while using a cache to avoid many requests for the same username.
@ -13,7 +13,7 @@ import type { UserInfo } from './types'
* @return Metadata about the requested user.
* @throws {Error} when the api request wasn't successful.
*/
export const getUserInfo = async (username: string): Promise<UserInfo> => {
const response = await new GetApiRequestBuilder<UserInfo>(`users/profile/${username}`).sendRequest()
export const getUserInfo = async (username: string): Promise<UserInfoDto> => {
const response = await new GetApiRequestBuilder<UserInfoDto>(`users/profile/${username}`).sendRequest()
return response.asParsedJsonObject()
}

View file

@ -1,15 +0,0 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export interface UserInfo {
username: string
displayName: string
photoUrl?: string
}
export interface FullUserInfo extends UserInfo {
email: string
}

View file

@ -1,16 +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 { mockI18n } from '../../../../../test-utils/mock-i18n'
import { EditorAppBar } from './editor-app-bar'
import type { NoteGroupPermissionEntry, NoteUserPermissionEntry } from '@hedgedoc/commons'
import type { LoginUserInfoDto, NoteGroupPermissionEntryDto, NoteUserPermissionEntryDto } from '@hedgedoc/commons'
import { render } from '@testing-library/react'
import type { PropsWithChildren } from 'react'
import React from 'react'
import { mockAppState } from '../../../../../test-utils/mock-app-state'
import type { LoginUserInfo } from '../../../../../api/me/types'
jest.mock('../../../../../components/layout/app-bar/base-app-bar', () => ({
__esModule: true,
@ -34,13 +33,13 @@ const mockedCommonAppState = {
groupName: '_EVERYONE',
canEdit: false
}
] as NoteGroupPermissionEntry[],
sharedToUsers: [] as NoteUserPermissionEntry[]
] as NoteGroupPermissionEntryDto[],
sharedToUsers: [] as NoteUserPermissionEntryDto[]
}
},
user: {
username: 'test'
} as LoginUserInfo
} as LoginUserInfoDto
}
describe('app bar', () => {

View file

@ -1,3 +1,9 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
'use client'
/*
@ -5,7 +11,7 @@
SPDX-License-Identifier: AGPL-3.0-only
*/
import { AuthProviderType } from '../../../api/config/types'
import { ProviderType } from '@hedgedoc/commons'
import { Redirect } from '../../../components/common/redirect'
import { LandingLayout } from '../../../components/landing-layout/landing-layout'
import { ProfileAccessTokens } from '../../../components/profile-page/access-tokens/profile-access-tokens'
@ -34,7 +40,7 @@ const ProfilePage: NextPage = () => {
<Row className='h-100 flex justify-content-center'>
<Col lg={6}>
<ProfileDisplayName />
{userProvider === (AuthProviderType.LOCAL as string) && <ProfileChangePassword />}
{userProvider === ProviderType.LOCAL && <ProfileChangePassword />}
<ProfileAccessTokens />
<ProfileAccountManagement />
</Col>

View file

@ -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(() => {

View file

@ -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()
})

View file

@ -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>

View file

@ -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])
}

View file

@ -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>
)}

View file

@ -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)

View file

@ -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
}
/**

View file

@ -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?')
)

View file

@ -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
}

View file

@ -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()

View file

@ -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)

View file

@ -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>

View file

@ -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}
/>
)
}

View file

@ -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])
}

View 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
}

View file

@ -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} />
}

View file

@ -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'}>

View file

@ -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()
})
})

View file

@ -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>
)
}

View file

@ -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
})
}

View file

@ -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_
})

View file

@ -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', () => {

View file

@ -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'

View file

@ -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
}
/**

View file

@ -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>

View file

@ -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

View file

@ -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
}
/**

View file

@ -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>

View file

@ -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'}

View file

@ -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}

View file

@ -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])

View file

@ -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)

View file

@ -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

View file

@ -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])

View file

@ -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])

View file

@ -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()
})
})

View file

@ -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}`

View file

@ -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'>

View file

@ -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>

View file

@ -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>

View file

@ -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}

View file

@ -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) {

View file

@ -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'}>

View file

@ -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: '',

View file

@ -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}>

View file

@ -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)
}

View file

@ -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
}
/**

View file

@ -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)

View file

@ -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]
)
}

View file

@ -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
}
/**

View file

@ -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
}
/**

View file

@ -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(() => {

View file

@ -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'))
},

View file

@ -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 type { CheatsheetExtension } from '../../components/cheatsheet/cheatsheet-extension'
import type { Linter } from '../../components/editor-page/editor-pane/linter/linter'
import type { MarkdownRendererExtension } from '../../components/markdown-renderer/extensions/_base-classes/markdown-renderer-extension'
@ -14,7 +14,7 @@ import type React from 'react'
import { Fragment } from 'react'
export interface MarkdownRendererExtensionOptions {
frontendConfig: FrontendConfig
frontendConfig: FrontendConfigDto
eventEmitter: EventEmitter2
rendererType: RendererType
}

View file

@ -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
*/
@ -21,7 +21,7 @@ import type { CompletionSource } from '@codemirror/autocomplete'
*/
export class PlantumlAppExtension extends AppExtension {
buildMarkdownRendererExtensions(options: MarkdownRendererExtensionOptions): MarkdownRendererExtension[] {
return [new PlantumlMarkdownExtension(options.frontendConfig.plantumlServer)]
return [new PlantumlMarkdownExtension(options.frontendConfig.plantUmlServer)]
}
buildCheatsheetExtensions(): CheatsheetExtension[] {

View file

@ -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
*/
@ -33,7 +33,7 @@ describe('PlantUML markdown extensions', () => {
it('renders an error if no server is defined', () => {
const view = render(
<TestMarkdownRenderer
extensions={[new PlantumlMarkdownExtension(undefined)]}
extensions={[new PlantumlMarkdownExtension(null)]}
content={'```plantuml\nclass Example\n```'}
/>
)

View file

@ -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
*/
@ -19,7 +19,7 @@ import type Token from 'markdown-it/lib/token'
* @see https://plantuml.com
*/
export class PlantumlMarkdownExtension extends MarkdownRendererExtension {
constructor(private plantumlServerUrl: string | undefined) {
constructor(private plantumlServerUrl: string | null) {
super()
}

View file

@ -1,5 +1,5 @@
/*
* 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
*/
@ -30,8 +30,8 @@ export const respondToMatchingRequest = <T>(
req: NextApiRequest,
res: NextApiResponse,
response: T,
statusCode = 200,
respondMethodNotAllowedOnMismatch = true
statusCode: number = 200,
respondMethodNotAllowedOnMismatch: boolean = true
): boolean => {
if (!isMockMode) {
res.status(404).send('Mock API is disabled')

View file

@ -1,27 +1,25 @@
/*
* 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 './use-translated-text'
import { renderHook } from '@testing-library/react'
import type { Namespace } from 'i18next'
import * as ReactI18NextModule from 'react-i18next'
import type { UseTranslationResponse } from 'react-i18next'
import { Mock } from 'ts-mockery'
import type { UseTranslationResponse } from 'react-i18next'
jest.mock('react-i18next')
describe('useTranslatedText', () => {
const mockTranslation = 'mockTranslation'
const mockKey = 'mockKey'
let translateFunction: jest.Mock
const translateFunction = jest.fn(() => mockTranslation)
beforeEach(() => {
translateFunction = jest.fn(() => mockTranslation)
const useTranslateMock = Mock.of<UseTranslationResponse<Namespace, unknown>>({
const useTranslateMock = Mock.of({
t: translateFunction
})
}) as unknown as UseTranslationResponse<never, never>
jest.spyOn(ReactI18NextModule, 'useTranslation').mockReturnValue(useTranslateMock)
})

View file

@ -1,10 +1,10 @@
/*
* 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 { FrontendConfig } from '../../../api/config/types'
import { AuthProviderType, GuestAccessLevel } from '../../../api/config/types'
import type { FrontendConfigDto } from '@hedgedoc/commons'
import { ProviderType, GuestAccess } from '@hedgedoc/commons'
import {
HttpMethod,
respondToMatchingRequest,
@ -13,7 +13,7 @@ import {
import { isTestMode } from '../../../utils/test-modes'
import type { NextApiRequest, NextApiResponse } from 'next'
const initialConfig: FrontendConfig = {
const initialConfig: FrontendConfigDto = {
allowRegister: true,
allowProfileEdits: true,
allowChooseUsername: true,
@ -21,7 +21,7 @@ const initialConfig: FrontendConfig = {
name: 'DEMO Corp',
logo: '/public/img/demo.png'
},
guestAccess: GuestAccessLevel.WRITE,
guestAccess: GuestAccess.WRITE,
useImageProxy: false,
specialUrls: {
privacy: 'https://example.com/privacy',
@ -33,31 +33,34 @@ const initialConfig: FrontendConfig = {
minor: 0,
patch: 0,
preRelease: isTestMode ? undefined : '',
commit: 'mock'
commit: 'mock',
fullString: `${isTestMode ? 0 : 2}.0.0`
},
plantumlServer: isTestMode ? 'http://mock-plantuml.local' : 'https://www.plantuml.com/plantuml',
plantUmlServer: isTestMode ? 'http://mock-plantuml.local' : 'https://www.plantuml.com/plantuml',
maxDocumentLength: isTestMode ? 200 : 1000000,
authProviders: [
{
type: AuthProviderType.LOCAL
type: ProviderType.LOCAL
},
{
type: AuthProviderType.LDAP,
type: ProviderType.LDAP,
identifier: 'test-ldap',
providerName: 'Test LDAP'
providerName: 'Test LDAP',
theme: null
},
{
type: AuthProviderType.OIDC,
type: ProviderType.OIDC,
identifier: 'test-oidc',
providerName: 'Test OIDC'
providerName: 'Test OIDC',
theme: null
}
]
}
let currentConfig: FrontendConfig = initialConfig
let currentConfig: FrontendConfigDto = initialConfig
const handler = (req: NextApiRequest, res: NextApiResponse) => {
const responseSuccessful = respondToMatchingRequest<FrontendConfig>(
const responseSuccessful = respondToMatchingRequest<FrontendConfigDto>(
HttpMethod.GET,
req,
res,
@ -66,10 +69,10 @@ const handler = (req: NextApiRequest, res: NextApiResponse) => {
false
)
if (!responseSuccessful) {
respondToTestRequest<FrontendConfig>(req, res, () => {
respondToTestRequest<FrontendConfigDto>(req, res, () => {
currentConfig = {
...initialConfig,
...(req.body as FrontendConfig)
...(req.body as FrontendConfigDto)
}
return currentConfig
})

View file

@ -1,14 +1,14 @@
/*
* 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 { GroupInfo } from '../../../../api/group/types'
import { HttpMethod, respondToMatchingRequest } from '../../../../handler-utils/respond-to-matching-request'
import type { NextApiRequest, NextApiResponse } from 'next'
import type { GroupInfoDto } from '@hedgedoc/commons'
const handler = (req: NextApiRequest, res: NextApiResponse) => {
respondToMatchingRequest<GroupInfo>(HttpMethod.GET, req, res, {
respondToMatchingRequest<GroupInfoDto>(HttpMethod.GET, req, res, {
name: '_EVERYONE',
displayName: 'Everyone',
special: true

View file

@ -1,14 +1,14 @@
/*
* 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 { GroupInfo } from '../../../../api/group/types'
import { HttpMethod, respondToMatchingRequest } from '../../../../handler-utils/respond-to-matching-request'
import type { NextApiRequest, NextApiResponse } from 'next'
import type { GroupInfoDto } from '@hedgedoc/commons'
const handler = (req: NextApiRequest, res: NextApiResponse) => {
respondToMatchingRequest<GroupInfo>(HttpMethod.GET, req, res, {
respondToMatchingRequest<GroupInfoDto>(HttpMethod.GET, req, res, {
name: '_LOGGED_IN',
displayName: 'All registered users',
special: true

View file

@ -1,14 +1,14 @@
/*
* 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 { GroupInfo } from '../../../../api/group/types'
import { HttpMethod, respondToMatchingRequest } from '../../../../handler-utils/respond-to-matching-request'
import type { NextApiRequest, NextApiResponse } from 'next'
import type { GroupInfoDto } from '@hedgedoc/commons'
const handler = (req: NextApiRequest, res: NextApiResponse) => {
respondToMatchingRequest<GroupInfo>(HttpMethod.GET, req, res, {
respondToMatchingRequest<GroupInfoDto>(HttpMethod.GET, req, res, {
name: 'hedgedoc-devs',
displayName: 'HedgeDoc devs',
special: true

View file

@ -1,11 +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 { LoginUserInfo } from '../../../../api/me/types'
import { HttpMethod, respondToMatchingRequest } from '../../../../handler-utils/respond-to-matching-request'
import type { NextApiRequest, NextApiResponse } from 'next'
import type { LoginUserInfoDto } from '@hedgedoc/commons'
import { ProviderType } from '@hedgedoc/commons'
const handler = (req: NextApiRequest, res: NextApiResponse): void => {
const cookieSet = req.headers?.['cookie']?.split(';').find((value) => value.trim() === 'mock-session=1') !== undefined
@ -13,11 +14,11 @@ const handler = (req: NextApiRequest, res: NextApiResponse): void => {
res.status(403).json({})
return
}
respondToMatchingRequest<LoginUserInfo>(HttpMethod.GET, req, res, {
respondToMatchingRequest<LoginUserInfoDto>(HttpMethod.GET, req, res, {
username: 'mock',
photoUrl: '/public/img/avatar.png',
displayName: 'Mock User',
authProvider: 'local',
authProvider: ProviderType.LOCAL,
email: 'mock@hedgedoc.test'
})
}

View file

@ -1,14 +1,14 @@
/*
* 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 { MediaUpload } from '../../../../api/media/types'
import { HttpMethod, respondToMatchingRequest } from '../../../../handler-utils/respond-to-matching-request'
import type { NextApiRequest, NextApiResponse } from 'next'
import type { MediaUploadDto } from '@hedgedoc/commons'
const handler = (req: NextApiRequest, res: NextApiResponse) => {
respondToMatchingRequest<MediaUpload[]>(HttpMethod.GET, req, res, [
respondToMatchingRequest<MediaUploadDto[]>(HttpMethod.GET, req, res, [
{
username: 'tilman',
createdAt: '2022-03-20T20:36:32Z',

View file

@ -1,12 +1,12 @@
/*
* 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 { MediaUpload } from '../../../api/media/types'
import { HttpMethod, respondToMatchingRequest } from '../../../handler-utils/respond-to-matching-request'
import { isMockMode, isTestMode } from '../../../utils/test-modes'
import type { NextApiRequest, NextApiResponse } from 'next'
import type { MediaUploadDto } from '@hedgedoc/commons'
const handler = async (req: NextApiRequest, res: NextApiResponse): Promise<void> => {
if (isMockMode && !isTestMode) {
@ -15,7 +15,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse): Promise<void>
})
}
respondToMatchingRequest<MediaUpload>(
respondToMatchingRequest<MediaUploadDto>(
HttpMethod.POST,
req,
res,

File diff suppressed because one or more lines are too long

View file

@ -1,14 +1,14 @@
/*
* 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 { RevisionDetails } from '../../../../../../api/revisions/types'
import type { RevisionDto } from '@hedgedoc/commons'
import { HttpMethod, respondToMatchingRequest } from '../../../../../../handler-utils/respond-to-matching-request'
import type { NextApiRequest, NextApiResponse } from 'next'
const handler = (req: NextApiRequest, res: NextApiResponse): void => {
respondToMatchingRequest<RevisionDetails>(HttpMethod.GET, req, res, {
respondToMatchingRequest<RevisionDto>(HttpMethod.GET, req, res, {
id: 0,
createdAt: '2021-12-21T16:59:42.000Z',
title: 'Features',

View file

@ -1,14 +1,14 @@
/*
* 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 { RevisionDetails } from '../../../../../../api/revisions/types'
import type { RevisionDto } from '@hedgedoc/commons'
import { HttpMethod, respondToMatchingRequest } from '../../../../../../handler-utils/respond-to-matching-request'
import type { NextApiRequest, NextApiResponse } from 'next'
const handler = (req: NextApiRequest, res: NextApiResponse): void => {
respondToMatchingRequest<RevisionDetails>(HttpMethod.GET, req, res, {
respondToMatchingRequest<RevisionDto>(HttpMethod.GET, req, res, {
id: 1,
createdAt: '2021-12-29T17:54:11.000Z',
title: 'Features',

Some files were not shown because too many files have changed in this diff Show more