docs: consolidate docs (#2182)

Signed-off-by: Philip Molares <philip.molares@udo.edu>
This commit is contained in:
Philip Molares 2022-07-21 22:36:46 +02:00 committed by GitHub
parent 8d46d7e39e
commit ecffebc43c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
307 changed files with 1474 additions and 487 deletions

View file

@ -10,9 +10,11 @@ import { DeleteApiRequestBuilder } from '../common/api-request-builder/delete-ap
/** /**
* Adds an alias to an existing note. * Adds an alias to an existing note.
*
* @param noteIdOrAlias The note id or an existing alias for a note. * @param noteIdOrAlias The note id or an existing alias for a note.
* @param newAlias The new alias. * @param newAlias The new alias.
* @return Information about the newly created alias. * @return Information about the newly created alias.
* @throws {Error} when the api request wasn't successfull
*/ */
export const addAlias = async (noteIdOrAlias: string, newAlias: string): Promise<Alias> => { export const addAlias = async (noteIdOrAlias: string, newAlias: string): Promise<Alias> => {
const response = await new PostApiRequestBuilder<Alias, NewAliasDto>('alias') const response = await new PostApiRequestBuilder<Alias, NewAliasDto>('alias')
@ -27,8 +29,10 @@ export const addAlias = async (noteIdOrAlias: string, newAlias: string): Promise
/** /**
* Marks a given alias as the primary one for a note. * Marks a given alias as the primary one for a note.
* The former primary alias should be marked as non-primary by the backend automatically. * The former primary alias should be marked as non-primary by the backend automatically.
*
* @param alias The alias to mark as primary for its corresponding note. * @param alias The alias to mark as primary for its corresponding note.
* @return The updated information about the alias. * @return The updated information about the alias.
* @throws {Error} when the api request wasn't successfull
*/ */
export const markAliasAsPrimary = async (alias: string): Promise<Alias> => { export const markAliasAsPrimary = async (alias: string): Promise<Alias> => {
const response = await new PutApiRequestBuilder<Alias, PrimaryAliasDto>('alias/' + alias) const response = await new PutApiRequestBuilder<Alias, PrimaryAliasDto>('alias/' + alias)
@ -41,7 +45,9 @@ export const markAliasAsPrimary = async (alias: string): Promise<Alias> => {
/** /**
* Removes a given alias from its corresponding note. * Removes a given alias from its corresponding note.
*
* @param alias The alias to remove from its note. * @param alias The alias to remove from its note.
* @throws {Error} when the api request wasn't successful.
*/ */
export const deleteAlias = async (alias: string): Promise<void> => { export const deleteAlias = async (alias: string): Promise<void> => {
await new DeleteApiRequestBuilder('alias/' + alias).sendRequest() await new DeleteApiRequestBuilder('alias/' + alias).sendRequest()

View file

@ -7,7 +7,8 @@ import { DeleteApiRequestBuilder } from '../common/api-request-builder/delete-ap
/** /**
* Requests to log out the current user. * Requests to log out the current user.
* @throws Error if logout is not possible. *
* @throws {Error} if logout is not possible.
*/ */
export const doLogout = async (): Promise<void> => { export const doLogout = async (): Promise<void> => {
await new DeleteApiRequestBuilder('auth/logout').sendRequest() await new DeleteApiRequestBuilder('auth/logout').sendRequest()

View file

@ -9,11 +9,13 @@ import { AuthError } from './types'
import { PostApiRequestBuilder } from '../common/api-request-builder/post-api-request-builder' import { PostApiRequestBuilder } from '../common/api-request-builder/post-api-request-builder'
/** /**
* Requests to login a user via LDAP credentials. * Requests to log in a user via LDAP credentials.
*
* @param provider The identifier of the LDAP provider with which to login. * @param provider The identifier of the LDAP provider with which to login.
* @param username The username with which to try the login. * @param username The username with which to try the login.
* @param password The password of the user. * @param password The password of the user.
* @throws {AuthError.INVALID_CREDENTIALS} if the LDAP provider denied the given credentials. * @throws {AuthError.INVALID_CREDENTIALS} if the LDAP provider denied the given credentials.
* @throws {Error} when the api request wasn't successfull
*/ */
export const doLdapLogin = async (provider: string, username: string, password: string): Promise<void> => { export const doLdapLogin = async (provider: string, username: string, password: string): Promise<void> => {
await new PostApiRequestBuilder<void, LoginDto>('auth/ldap/' + provider) await new PostApiRequestBuilder<void, LoginDto>('auth/ldap/' + provider)

View file

@ -10,10 +10,12 @@ import { PutApiRequestBuilder } from '../common/api-request-builder/put-api-requ
/** /**
* Requests to do a local login with a provided username and password. * Requests to do a local login with a provided username and password.
*
* @param username The username for which the login should be tried. * @param username The username for which the login should be tried.
* @param password The password which should be used to log in. * @param password The password which should be used to log in.
* @throws {AuthError.INVALID_CREDENTIALS} when the username or password is wrong. * @throws {AuthError.INVALID_CREDENTIALS} when the username or password is wrong.
* @throws {AuthError.LOGIN_DISABLED} when the local login is disabled on the backend. * @throws {AuthError.LOGIN_DISABLED} when the local login is disabled on the backend.
* @throws {Error} when the api request wasn't successful.
*/ */
export const doLocalLogin = async (username: string, password: string): Promise<void> => { export const doLocalLogin = async (username: string, password: string): Promise<void> => {
await new PostApiRequestBuilder<void, LoginDto>('auth/local/login') await new PostApiRequestBuilder<void, LoginDto>('auth/local/login')
@ -30,11 +32,13 @@ export const doLocalLogin = async (username: string, password: string): Promise<
/** /**
* Requests to register a new local user in the backend. * Requests to register a new local user in the backend.
*
* @param username The username of the new user. * @param username The username of the new user.
* @param displayName The display name of the new user. * @param displayName The display name of the new user.
* @param password The password of the new user. * @param password The password of the new user.
* @throws {RegisterError.USERNAME_EXISTING} when there is already an existing user with the same username. * @throws {RegisterError.USERNAME_EXISTING} when there is already an existing user with the same username.
* @throws {RegisterError.REGISTRATION_DISABLED} when the registration of local users has been disabled on the backend. * @throws {RegisterError.REGISTRATION_DISABLED} when the registration of local users has been disabled on the backend.
* @throws {Error} when the api request wasn't successful.
*/ */
export const doLocalRegister = async (username: string, displayName: string, password: string): Promise<void> => { export const doLocalRegister = async (username: string, displayName: string, password: string): Promise<void> => {
await new PostApiRequestBuilder<void, RegisterDto>('auth/local') await new PostApiRequestBuilder<void, RegisterDto>('auth/local')

View file

@ -29,7 +29,7 @@ export abstract class ApiRequestBuilderWithBody<ResponseType, RequestBodyType> e
* *
* @param bodyData The data to use as request body. Will get stringified to JSON. * @param bodyData The data to use as request body. Will get stringified to JSON.
* @return The API request instance itself for chaining. * @return The API request instance itself for chaining.
* @see {withBody} * @see withBody
*/ */
withJsonBody(bodyData: RequestBodyType): this { withJsonBody(bodyData: RequestBodyType): this {
this.withHeader('Content-Type', 'application/json') this.withHeader('Content-Type', 'application/json')

View file

@ -108,7 +108,7 @@ export abstract class ApiRequestBuilder<ResponseType> {
* Send the prepared API call as a GET request. A default status code of 200 is expected. * Send the prepared API call as a GET request. A default status code of 200 is expected.
* *
* @return The API response. * @return The API response.
* @throws Error when the status code does not match the expected one or is defined as in the custom status code * @throws {Error} when the status code does not match the expected one or is defined as in the custom status code
* error mapping. * error mapping.
*/ */
abstract sendRequest(): Promise<ApiResponse<ResponseType>> abstract sendRequest(): Promise<ApiResponse<ResponseType>>

View file

@ -11,14 +11,14 @@ import { ApiRequestBuilderWithBody } from './api-request-builder-with-body'
* *
* @param ResponseType The type of the expected response. Defaults to no response body. * @param ResponseType The type of the expected response. Defaults to no response body.
* @param RequestBodyType The type of the request body. Defaults to no request body. * @param RequestBodyType The type of the request body. Defaults to no request body.
* @see {ApiRequestBuilder} * @see ApiRequestBuilder
*/ */
export class DeleteApiRequestBuilder<ResponseType = void, RequestBodyType = unknown> extends ApiRequestBuilderWithBody< export class DeleteApiRequestBuilder<ResponseType = void, RequestBodyType = unknown> extends ApiRequestBuilderWithBody<
ResponseType, ResponseType,
RequestBodyType RequestBodyType
> { > {
/** /**
* @see {ApiRequestBuilder#sendRequest} * @see ApiRequestBuilder#sendRequest
*/ */
sendRequest(): Promise<ApiResponse<ResponseType>> { sendRequest(): Promise<ApiResponse<ResponseType>> {
return this.sendRequestAndVerifyResponse('DELETE', 204) return this.sendRequestAndVerifyResponse('DELETE', 204)

View file

@ -11,11 +11,11 @@ import type { ApiResponse } from '../api-response'
* Builder to construct a GET request to the API. * Builder to construct a GET request to the API.
* *
* @param ResponseType The type of the expected response. * @param ResponseType The type of the expected response.
* @see {ApiRequestBuilder} * @see ApiRequestBuilder
*/ */
export class GetApiRequestBuilder<ResponseType> extends ApiRequestBuilder<ResponseType> { export class GetApiRequestBuilder<ResponseType> extends ApiRequestBuilder<ResponseType> {
/** /**
* @see {ApiRequestBuilder#sendRequest} * @see ApiRequestBuilder#sendRequest
*/ */
sendRequest(): Promise<ApiResponse<ResponseType>> { sendRequest(): Promise<ApiResponse<ResponseType>> {
return this.sendRequestAndVerifyResponse('GET', 200) return this.sendRequestAndVerifyResponse('GET', 200)

View file

@ -11,14 +11,14 @@ import { ApiRequestBuilderWithBody } from './api-request-builder-with-body'
* *
* @param ResponseType The type of the expected response. * @param ResponseType The type of the expected response.
* @param RequestBodyType The type of the request body * @param RequestBodyType The type of the request body
* @see {ApiRequestBuilder} * @see ApiRequestBuilder
*/ */
export class PostApiRequestBuilder<ResponseType, RequestBodyType> extends ApiRequestBuilderWithBody< export class PostApiRequestBuilder<ResponseType, RequestBodyType> extends ApiRequestBuilderWithBody<
ResponseType, ResponseType,
RequestBodyType RequestBodyType
> { > {
/** /**
* @see {ApiRequestBuilder#sendRequest} * @see ApiRequestBuilder#sendRequest
*/ */
sendRequest(): Promise<ApiResponse<ResponseType>> { sendRequest(): Promise<ApiResponse<ResponseType>> {
return this.sendRequestAndVerifyResponse('POST', 201) return this.sendRequestAndVerifyResponse('POST', 201)

View file

@ -11,14 +11,14 @@ import { ApiRequestBuilderWithBody } from './api-request-builder-with-body'
* *
* @param ResponseType The type of the expected response. * @param ResponseType The type of the expected response.
* @param RequestBodyType The type of the request body * @param RequestBodyType The type of the request body
* @see {ApiRequestBuilder} * @see ApiRequestBuilder
*/ */
export class PutApiRequestBuilder<ResponseType, RequestBodyType> extends ApiRequestBuilderWithBody< export class PutApiRequestBuilder<ResponseType, RequestBodyType> extends ApiRequestBuilderWithBody<
ResponseType, ResponseType,
RequestBodyType RequestBodyType
> { > {
/** /**
* @see {ApiRequestBuilder#sendRequest} * @see ApiRequestBuilder#sendRequest
*/ */
sendRequest(): Promise<ApiResponse<ResponseType>> { sendRequest(): Promise<ApiResponse<ResponseType>> {
return this.sendRequestAndVerifyResponse('PUT', 200) return this.sendRequestAndVerifyResponse('PUT', 200)

View file

@ -7,7 +7,15 @@
import { defaultConfig } from '../../default-config' import { defaultConfig } from '../../default-config'
import { Mock } from 'ts-mockery' import { Mock } from 'ts-mockery'
export const expectFetch = (expectedUrl: string, expectedStatusCode: number, expectedOptions: RequestInit): void => { /**
* Mock fetch api for tests.
* Check that the given url and options are present in the request and return the given status code.
*
* @param expectedUrl the url that should be requested
* @param requestStatusCode the status code the mocked request should return
* @param expectedOptions additional options
*/
export const expectFetch = (expectedUrl: string, requestStatusCode: number, expectedOptions: RequestInit): void => {
global.fetch = jest.fn((fetchUrl: RequestInfo | URL, fetchOptions?: RequestInit): Promise<Response> => { global.fetch = jest.fn((fetchUrl: RequestInfo | URL, fetchOptions?: RequestInit): Promise<Response> => {
expect(fetchUrl).toEqual(expectedUrl) expect(fetchUrl).toEqual(expectedUrl)
expect(fetchOptions).toStrictEqual({ expect(fetchOptions).toStrictEqual({
@ -18,7 +26,7 @@ export const expectFetch = (expectedUrl: string, expectedStatusCode: number, exp
}) })
return Promise.resolve( return Promise.resolve(
Mock.of<Response>({ Mock.of<Response>({
status: expectedStatusCode status: requestStatusCode
}) })
) )
}) })

View file

@ -12,6 +12,7 @@ export class ApiResponse<ResponseType> {
/** /**
* Initializes a new API response instance based on an HTTP response. * Initializes a new API response instance based on an HTTP response.
*
* @param response The HTTP response from the fetch call. * @param response The HTTP response from the fetch call.
*/ */
constructor(response: Response) { constructor(response: Response) {
@ -31,7 +32,7 @@ export class ApiResponse<ResponseType> {
* Returns the response as parsed JSON. An error will be thrown if the response is not JSON encoded. * Returns the response as parsed JSON. An error will be thrown if the response is not JSON encoded.
* *
* @return The parsed JSON response. * @return The parsed JSON response.
* @throws Error if the response is not JSON encoded. * @throws {Error} if the response is not JSON encoded.
*/ */
async asParsedJsonObject(): Promise<ResponseType> { async asParsedJsonObject(): Promise<ResponseType> {
if (!this.response.headers.get('Content-Type')?.startsWith('application/json')) { if (!this.response.headers.get('Content-Type')?.startsWith('application/json')) {

View file

@ -9,7 +9,9 @@ import { GetApiRequestBuilder } from '../common/api-request-builder/get-api-requ
/** /**
* Fetches the frontend config from the backend. * Fetches the frontend config from the backend.
*
* @return The frontend config. * @return The frontend config.
* @throws {Error} when the api request wasn't successful.
*/ */
export const getConfig = async (): Promise<Config> => { export const getConfig = async (): Promise<Config> => {
const response = await new GetApiRequestBuilder<Config>('config').sendRequest() const response = await new GetApiRequestBuilder<Config>('config').sendRequest()

View file

@ -9,8 +9,10 @@ import { GetApiRequestBuilder } from '../common/api-request-builder/get-api-requ
/** /**
* Retrieves information about a group with a given name. * Retrieves information about a group with a given name.
*
* @param groupName The name of the group. * @param groupName The name of the group.
* @return Information about the group. * @return Information about the group.
* @throws {Error} when the api request wasn't successful.
*/ */
export const getGroup = async (groupName: string): Promise<GroupInfo> => { export const getGroup = async (groupName: string): Promise<GroupInfo> => {
const response = await new GetApiRequestBuilder<GroupInfo>('groups/' + groupName).sendRequest() const response = await new GetApiRequestBuilder<GroupInfo>('groups/' + groupName).sendRequest()

View file

@ -1,18 +1,30 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import type { HistoryEntry, HistoryEntryPutDto, HistoryEntryWithOrigin } from './types' import type { HistoryEntry, HistoryEntryPutDto, HistoryEntryWithOrigin } from './types'
import { HistoryEntryOrigin } from './types' import { HistoryEntryOrigin } from './types'
export const addRemoteOriginToHistoryEntry = (entryDto: HistoryEntry): HistoryEntryWithOrigin => { /**
* Transform a {@link HistoryEntry} into a {@link HistoryEntryWithOrigin}.
*
* @param entry the entry to build from
* @return the history entry with an origin
*/
export const addRemoteOriginToHistoryEntry = (entry: HistoryEntry): HistoryEntryWithOrigin => {
return { return {
...entryDto, ...entry,
origin: HistoryEntryOrigin.REMOTE origin: HistoryEntryOrigin.REMOTE
} }
} }
/**
* Create a {@link HistoryEntryPutDto} from a {@link HistoryEntry}.
*
* @param entry the entry to build the dto from
* @return the dto for the api
*/
export const historyEntryToHistoryEntryPutDto = (entry: HistoryEntry): HistoryEntryPutDto => { export const historyEntryToHistoryEntryPutDto = (entry: HistoryEntry): HistoryEntryPutDto => {
return { return {
pinStatus: entry.pinStatus, pinStatus: entry.pinStatus,

View file

@ -11,7 +11,9 @@ import { DeleteApiRequestBuilder } from '../common/api-request-builder/delete-ap
/** /**
* Fetches the remote history for the user from the server. * Fetches the remote history for the user from the server.
*
* @return The remote history entries of the user. * @return The remote history entries of the user.
* @throws {Error} when the api request wasn't successful.
*/ */
export const getRemoteHistory = async (): Promise<HistoryEntry[]> => { export const getRemoteHistory = async (): Promise<HistoryEntry[]> => {
const response = await new GetApiRequestBuilder<HistoryEntry[]>('me/history').sendRequest() const response = await new GetApiRequestBuilder<HistoryEntry[]>('me/history').sendRequest()
@ -20,7 +22,9 @@ export const getRemoteHistory = async (): Promise<HistoryEntry[]> => {
/** /**
* Replaces the remote history of the user with the given history entries. * Replaces the remote history of the user with the given history entries.
*
* @param entries The history entries to store remotely. * @param entries The history entries to store remotely.
* @throws {Error} when the api request wasn't successful.
*/ */
export const setRemoteHistoryEntries = async (entries: HistoryEntryPutDto[]): Promise<void> => { export const setRemoteHistoryEntries = async (entries: HistoryEntryPutDto[]): Promise<void> => {
await new PostApiRequestBuilder<void, HistoryEntryPutDto[]>('me/history').withJsonBody(entries).sendRequest() await new PostApiRequestBuilder<void, HistoryEntryPutDto[]>('me/history').withJsonBody(entries).sendRequest()
@ -28,8 +32,10 @@ export const setRemoteHistoryEntries = async (entries: HistoryEntryPutDto[]): Pr
/** /**
* Updates a remote history entry's pin state. * Updates a remote history entry's pin state.
*
* @param noteIdOrAlias The note id for which to update the pinning state. * @param noteIdOrAlias The note id for which to update the pinning state.
* @param pinStatus True when the note should be pinned, false otherwise. * @param pinStatus True when the note should be pinned, false otherwise.
* @throws {Error} when the api request wasn't successful.
*/ */
export const updateRemoteHistoryEntryPinStatus = async ( export const updateRemoteHistoryEntryPinStatus = async (
noteIdOrAlias: string, noteIdOrAlias: string,
@ -45,7 +51,9 @@ export const updateRemoteHistoryEntryPinStatus = async (
/** /**
* Deletes a remote history entry. * Deletes a remote history entry.
*
* @param noteIdOrAlias The note id or alias of the history entry to remove. * @param noteIdOrAlias The note id or alias of the history entry to remove.
* @throws {Error} when the api request wasn't successful.
*/ */
export const deleteRemoteHistoryEntry = async (noteIdOrAlias: string): Promise<void> => { export const deleteRemoteHistoryEntry = async (noteIdOrAlias: string): Promise<void> => {
await new DeleteApiRequestBuilder('me/history/' + noteIdOrAlias).sendRequest() await new DeleteApiRequestBuilder('me/history/' + noteIdOrAlias).sendRequest()
@ -53,6 +61,8 @@ export const deleteRemoteHistoryEntry = async (noteIdOrAlias: string): Promise<v
/** /**
* Deletes the complete remote history. * Deletes the complete remote history.
*
* @throws {Error} when the api request wasn't successful.
*/ */
export const deleteRemoteHistory = async (): Promise<void> => { export const deleteRemoteHistory = async (): Promise<void> => {
await new DeleteApiRequestBuilder('me/history').sendRequest() await new DeleteApiRequestBuilder('me/history').sendRequest()

View file

@ -11,8 +11,9 @@ import { PostApiRequestBuilder } from '../common/api-request-builder/post-api-re
/** /**
* Returns metadata about the currently signed-in user from the API. * Returns metadata about the currently signed-in user from the API.
* @throws Error when the user is not signed-in. *
* @return The user metadata. * @return The user metadata.
* @throws {Error} when the user is not signed-in.
*/ */
export const getMe = async (): Promise<LoginUserInfo> => { export const getMe = async (): Promise<LoginUserInfo> => {
const response = await new GetApiRequestBuilder<LoginUserInfo>('me').sendRequest() const response = await new GetApiRequestBuilder<LoginUserInfo>('me').sendRequest()
@ -21,6 +22,8 @@ export const getMe = async (): Promise<LoginUserInfo> => {
/** /**
* Deletes the current user from the server. * Deletes the current user from the server.
*
* @throws {Error} when the api request wasn't successful.
*/ */
export const deleteUser = async (): Promise<void> => { export const deleteUser = async (): Promise<void> => {
await new DeleteApiRequestBuilder('me').sendRequest() await new DeleteApiRequestBuilder('me').sendRequest()
@ -28,7 +31,9 @@ export const deleteUser = async (): Promise<void> => {
/** /**
* Changes the display name of the current user. * Changes the display name of the current user.
*
* @param displayName The new display name to set. * @param displayName The new display name to set.
* @throws {Error} when the api request wasn't successful.
*/ */
export const updateDisplayName = async (displayName: string): Promise<void> => { export const updateDisplayName = async (displayName: string): Promise<void> => {
await new PostApiRequestBuilder<void, ChangeDisplayNameDto>('me/profile') await new PostApiRequestBuilder<void, ChangeDisplayNameDto>('me/profile')
@ -40,7 +45,9 @@ export const updateDisplayName = async (displayName: string): Promise<void> => {
/** /**
* Retrieves a list of media belonging to the user. * Retrieves a list of media belonging to the user.
*
* @return List of media object information. * @return List of media object information.
* @throws {Error} when the api request wasn't successful.
*/ */
export const getMyMedia = async (): Promise<MediaUpload[]> => { export const getMyMedia = async (): Promise<MediaUpload[]> => {
const response = await new GetApiRequestBuilder<MediaUpload[]>('me/media').sendRequest() const response = await new GetApiRequestBuilder<MediaUpload[]>('me/media').sendRequest()

View file

@ -9,8 +9,10 @@ import { DeleteApiRequestBuilder } from '../common/api-request-builder/delete-ap
/** /**
* Requests an image-proxy URL from the backend for a given image URL. * Requests an image-proxy URL from the backend for a given image URL.
*
* @param imageUrl The image URL which should be proxied. * @param imageUrl The image URL which should be proxied.
* @return The proxy URL for the image. * @return The proxy URL for the image.
* @throws {Error} when the api request wasn't successful.
*/ */
export const getProxiedUrl = async (imageUrl: string): Promise<ImageProxyResponse> => { export const getProxiedUrl = async (imageUrl: string): Promise<ImageProxyResponse> => {
const response = await new PostApiRequestBuilder<ImageProxyResponse, ImageProxyRequestDto>('media/proxy') const response = await new PostApiRequestBuilder<ImageProxyResponse, ImageProxyRequestDto>('media/proxy')
@ -23,9 +25,11 @@ export const getProxiedUrl = async (imageUrl: string): Promise<ImageProxyRespons
/** /**
* Uploads a media file to the backend. * Uploads a media file to the backend.
*
* @param noteIdOrAlias The id or alias of the note from which the media is uploaded. * @param noteIdOrAlias The id or alias of the note from which the media is uploaded.
* @param media The binary media content. * @param media The binary media content.
* @return The URL of the uploaded media object. * @return The URL of the uploaded media object.
* @throws {Error} when the api request wasn't successful.
*/ */
export const uploadFile = async (noteIdOrAlias: string, media: Blob): Promise<MediaUpload> => { export const uploadFile = async (noteIdOrAlias: string, media: Blob): Promise<MediaUpload> => {
const postData = new FormData() const postData = new FormData()
@ -40,7 +44,9 @@ export const uploadFile = async (noteIdOrAlias: string, media: Blob): Promise<Me
/** /**
* Deletes some uploaded media object. * Deletes some uploaded media object.
*
* @param mediaId The identifier of the media object to delete. * @param mediaId The identifier of the media object to delete.
* @throws {Error} when the api request wasn't successful.
*/ */
export const deleteUploadedMedia = async (mediaId: string): Promise<void> => { export const deleteUploadedMedia = async (mediaId: string): Promise<void> => {
await new DeleteApiRequestBuilder('media/' + mediaId).sendRequest() await new DeleteApiRequestBuilder('media/' + mediaId).sendRequest()

View file

@ -11,8 +11,10 @@ import { DeleteApiRequestBuilder } from '../common/api-request-builder/delete-ap
/** /**
* Retrieves the content and metadata about the specified note. * Retrieves the content and metadata about the specified note.
*
* @param noteIdOrAlias The id or alias of the note. * @param noteIdOrAlias The id or alias of the note.
* @return Content and metadata of the specified note. * @return Content and metadata of the specified note.
* @throws {Error} when the api request wasn't successful.
*/ */
export const getNote = async (noteIdOrAlias: string): Promise<Note> => { export const getNote = async (noteIdOrAlias: string): Promise<Note> => {
const response = await new GetApiRequestBuilder<Note>('notes/' + noteIdOrAlias) const response = await new GetApiRequestBuilder<Note>('notes/' + noteIdOrAlias)
@ -23,8 +25,10 @@ export const getNote = async (noteIdOrAlias: string): Promise<Note> => {
/** /**
* Returns a list of media objects associated with the specified note. * Returns a list of media objects associated with the specified note.
*
* @param noteIdOrAlias The id or alias of the note. * @param noteIdOrAlias The id or alias of the note.
* @return List of media object metadata associated with specified note. * @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[]> => { export const getMediaForNote = async (noteIdOrAlias: string): Promise<MediaUpload[]> => {
const response = await new GetApiRequestBuilder<MediaUpload[]>(`notes/${noteIdOrAlias}/media`).sendRequest() const response = await new GetApiRequestBuilder<MediaUpload[]>(`notes/${noteIdOrAlias}/media`).sendRequest()
@ -33,8 +37,10 @@ export const getMediaForNote = async (noteIdOrAlias: string): Promise<MediaUploa
/** /**
* Creates a new note with a given markdown content. * Creates a new note with a given markdown content.
*
* @param markdown The content of the new note. * @param markdown The content of the new note.
* @return Content and metadata of the new note. * @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> => { export const createNote = async (markdown: string): Promise<Note> => {
const response = await new PostApiRequestBuilder<Note, void>('notes') const response = await new PostApiRequestBuilder<Note, void>('notes')
@ -46,9 +52,11 @@ export const createNote = async (markdown: string): Promise<Note> => {
/** /**
* Creates a new note with a given markdown content and a defined primary alias. * Creates a new note with a given markdown content and a defined primary alias.
*
* @param markdown The content of the new note. * @param markdown The content of the new note.
* @param primaryAlias The primary alias of the new note. * @param primaryAlias The primary alias of the new note.
* @return Content and metadata of the new 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> => { export const createNoteWithPrimaryAlias = async (markdown: string, primaryAlias: string): Promise<Note> => {
const response = await new PostApiRequestBuilder<Note, void>('notes/' + primaryAlias) const response = await new PostApiRequestBuilder<Note, void>('notes/' + primaryAlias)
@ -60,7 +68,9 @@ export const createNoteWithPrimaryAlias = async (markdown: string, primaryAlias:
/** /**
* Deletes the specified note. * Deletes the specified note.
*
* @param noteIdOrAlias The id or alias of the note to delete. * @param noteIdOrAlias The id or alias of the note to delete.
* @throws {Error} when the api request wasn't successful.
*/ */
export const deleteNote = async (noteIdOrAlias: string): Promise<void> => { export const deleteNote = async (noteIdOrAlias: string): Promise<void> => {
await new DeleteApiRequestBuilder('notes/' + noteIdOrAlias).sendRequest() await new DeleteApiRequestBuilder('notes/' + noteIdOrAlias).sendRequest()

View file

@ -10,9 +10,11 @@ import { DeleteApiRequestBuilder } from '../common/api-request-builder/delete-ap
/** /**
* Sets the owner of a note. * Sets the owner of a note.
*
* @param noteId The id of the note. * @param noteId The id of the note.
* @param owner The username of the new owner. * @param owner The username of the new owner.
* @return The updated note permissions. * @return The updated {@link NotePermissions}.
* @throws {Error} when the api request wasn't successful.
*/ */
export const setNoteOwner = async (noteId: string, owner: string): Promise<NotePermissions> => { export const setNoteOwner = async (noteId: string, owner: string): Promise<NotePermissions> => {
const response = await new PutApiRequestBuilder<NotePermissions, OwnerChangeDto>( const response = await new PutApiRequestBuilder<NotePermissions, OwnerChangeDto>(
@ -27,9 +29,12 @@ export const setNoteOwner = async (noteId: string, owner: string): Promise<NoteP
/** /**
* Sets a permission for one user of a note. * Sets a permission for one user of a note.
*
* @param noteId The id of the note. * @param noteId The id of the note.
* @param username The username of the user to set the permission for. * @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. * @param canEdit true if the user should be able to update the note, false otherwise.
* @return The updated {@link NotePermissions}.
* @throws {Error} when the api request wasn't successful.
*/ */
export const setUserPermission = async ( export const setUserPermission = async (
noteId: string, noteId: string,
@ -48,9 +53,12 @@ export const setUserPermission = async (
/** /**
* Sets a permission for one group of a note. * Sets a permission for one group of a note.
*
* @param noteId The id of the note. * @param noteId The id of the note.
* @param groupName The name of the group to set the permission for. * @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. * @param canEdit true if the group should be able to update the note, false otherwise.
* @return The updated {@link NotePermissions}.
* @throws {Error} when the api request wasn't successful.
*/ */
export const setGroupPermission = async ( export const setGroupPermission = async (
noteId: string, noteId: string,
@ -69,8 +77,11 @@ export const setGroupPermission = async (
/** /**
* Removes the permissions of a note for a user. * Removes the permissions of a note for a user.
*
* @param noteId The id of the note. * @param noteId The id of the note.
* @param username The name of the user to remove the permission of. * @param username The name of the user to remove the permission of.
* @return The updated {@link NotePermissions}.
* @throws {Error} when the api request wasn't successful.
*/ */
export const removeUserPermission = async (noteId: string, username: string): Promise<NotePermissions> => { export const removeUserPermission = async (noteId: string, username: string): Promise<NotePermissions> => {
const response = await new DeleteApiRequestBuilder<NotePermissions>( const response = await new DeleteApiRequestBuilder<NotePermissions>(
@ -83,8 +94,11 @@ export const removeUserPermission = async (noteId: string, username: string): Pr
/** /**
* Removes the permissions of a note for a group. * Removes the permissions of a note for a group.
*
* @param noteId The id of the note. * @param noteId The id of the note.
* @param groupName The name of the group to remove the permission of. * @param groupName The name of the group to remove the permission of.
* @return The updated {@link NotePermissions}.
* @throws {Error} when the api request wasn't successful.
*/ */
export const removeGroupPermission = async (noteId: string, groupName: string): Promise<NotePermissions> => { export const removeGroupPermission = async (noteId: string, groupName: string): Promise<NotePermissions> => {
const response = await new DeleteApiRequestBuilder<NotePermissions>( const response = await new DeleteApiRequestBuilder<NotePermissions>(

View file

@ -9,9 +9,11 @@ import { DeleteApiRequestBuilder } from '../common/api-request-builder/delete-ap
/** /**
* Retrieves a note revision while using a cache for often retrieved revisions. * Retrieves a note revision while using a cache for often retrieved revisions.
*
* @param noteId The id of the note for which to fetch the revision. * @param noteId The id of the note for which to fetch the revision.
* @param revisionId The id of the revision to fetch. * @param revisionId The id of the revision to fetch.
* @return The revision. * @return The revision.
* @throws {Error} when the api request wasn't successful.
*/ */
export const getRevision = async (noteId: string, revisionId: number): Promise<RevisionDetails> => { export const getRevision = async (noteId: string, revisionId: number): Promise<RevisionDetails> => {
const response = await new GetApiRequestBuilder<RevisionDetails>( const response = await new GetApiRequestBuilder<RevisionDetails>(
@ -22,8 +24,10 @@ export const getRevision = async (noteId: string, revisionId: number): Promise<R
/** /**
* Retrieves a list of all revisions stored for a given note. * Retrieves a list of all revisions stored for a given note.
*
* @param noteId The id of the note for which to look up the stored revisions. * @param noteId The id of the note for which to look up the stored revisions.
* @return A list of revision ids. * @return A list of revision ids.
* @throws {Error} when the api request wasn't successful.
*/ */
export const getAllRevisions = async (noteId: string): Promise<RevisionMetadata[]> => { export const getAllRevisions = async (noteId: string): Promise<RevisionMetadata[]> => {
const response = await new GetApiRequestBuilder<RevisionMetadata[]>(`notes/${noteId}/revisions`).sendRequest() const response = await new GetApiRequestBuilder<RevisionMetadata[]>(`notes/${noteId}/revisions`).sendRequest()
@ -32,7 +36,9 @@ export const getAllRevisions = async (noteId: string): Promise<RevisionMetadata[
/** /**
* Deletes all revisions for a note. * Deletes all revisions for a note.
*
* @param noteIdOrAlias The id or alias of the note to delete all revisions for. * @param noteIdOrAlias The id or alias of the note to delete all revisions for.
* @throws {Error} when the api request wasn't successful.
*/ */
export const deleteRevisionsForNote = async (noteIdOrAlias: string): Promise<void> => { export const deleteRevisionsForNote = async (noteIdOrAlias: string): Promise<void> => {
await new DeleteApiRequestBuilder(`notes/${noteIdOrAlias}/revisions`).sendRequest() await new DeleteApiRequestBuilder(`notes/${noteIdOrAlias}/revisions`).sendRequest()

View file

@ -10,7 +10,9 @@ import { DeleteApiRequestBuilder } from '../common/api-request-builder/delete-ap
/** /**
* Retrieves the access tokens for the current user. * Retrieves the access tokens for the current user.
*
* @return List of access token metadata. * @return List of access token metadata.
* @throws {Error} when the api request wasn't successful.
*/ */
export const getAccessTokenList = async (): Promise<AccessToken[]> => { export const getAccessTokenList = async (): Promise<AccessToken[]> => {
const response = await new GetApiRequestBuilder<AccessToken[]>('tokens').sendRequest() const response = await new GetApiRequestBuilder<AccessToken[]>('tokens').sendRequest()
@ -19,9 +21,11 @@ export const getAccessTokenList = async (): Promise<AccessToken[]> => {
/** /**
* Creates a new access token for the current user. * Creates a new access token for the current user.
*
* @param label The user-defined label for the new access token. * @param label The user-defined label for the new access token.
* @param validUntil The user-defined expiry date of the new access token in milliseconds of unix time. * @param validUntil The user-defined expiry date of the new access token in milliseconds of unix time.
* @return The new access token metadata along with its secret. * @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> => { export const postNewAccessToken = async (label: string, validUntil: number): Promise<AccessTokenWithSecret> => {
const response = await new PostApiRequestBuilder<AccessTokenWithSecret, CreateAccessTokenDto>('tokens') const response = await new PostApiRequestBuilder<AccessTokenWithSecret, CreateAccessTokenDto>('tokens')
@ -35,7 +39,9 @@ export const postNewAccessToken = async (label: string, validUntil: number): Pro
/** /**
* Removes an access token from the current user account. * Removes an access token from the current user account.
*
* @param keyId The key id of the access token to delete. * @param keyId The key id of the access token to delete.
* @throws {Error} when the api request wasn't successful.
*/ */
export const deleteAccessToken = async (keyId: string): Promise<void> => { export const deleteAccessToken = async (keyId: string): Promise<void> => {
await new DeleteApiRequestBuilder('tokens/' + keyId).sendRequest() await new DeleteApiRequestBuilder('tokens/' + keyId).sendRequest()

View file

@ -9,8 +9,10 @@ import { GetApiRequestBuilder } from '../common/api-request-builder/get-api-requ
/** /**
* Retrieves information about a specific user while using a cache to avoid many requests for the same username. * Retrieves information about a specific user while using a cache to avoid many requests for the same username.
*
* @param username The username of interest. * @param username The username of interest.
* @return Metadata about the requested user. * @return Metadata about the requested user.
* @throws {Error} when the api request wasn't successful.
*/ */
export const getUser = async (username: string): Promise<UserInfo> => { export const getUser = async (username: string): Promise<UserInfo> => {
const response = await new GetApiRequestBuilder<UserInfo>('users/' + username).sendRequest() const response = await new GetApiRequestBuilder<UserInfo>('users/' + username).sendRequest()

View file

@ -1,8 +1,11 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
/**
* Custom {@link Error} class for the {@link ApplicationLoader}.
*/
export class ApplicationLoaderError extends Error { export class ApplicationLoaderError extends Error {
constructor(taskName: string) { constructor(taskName: string) {
super(`The task ${taskName} failed`) super(`The task ${taskName} failed`)

View file

@ -14,7 +14,13 @@ import { ApplicationLoaderError } from './application-loader-error'
const log = new Logger('ApplicationLoader') const log = new Logger('ApplicationLoader')
export const ApplicationLoader: React.FC<PropsWithChildren<unknown>> = ({ children }) => { /**
* Initializes the application and executes all the setup tasks.
* It renders a {@link LoadingScreen} while this is happening. If there are any error, they will be displayed in the {@link LoadingScreen}.
*
* @param children The children in the React dom that should be shown once the application is loaded.
*/
export const ApplicationLoader: React.FC<PropsWithChildren> = ({ children }) => {
const { error, loading } = useAsync(async () => { const { error, loading } = useAsync(async () => {
const initTasks = createSetUpTaskList() const initTasks = createSetUpTaskList()
for (const task of initTasks) { for (const task of initTasks) {

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -7,6 +7,9 @@
import { getConfig } from '../../../api/config' import { getConfig } from '../../../api/config'
import { setConfig } from '../../../redux/config/methods' import { setConfig } from '../../../redux/config/methods'
/**
* Get the {@link Config frontend config} and save it in the global application state.
*/
export const fetchFrontendConfig = async (): Promise<void> => { export const fetchFrontendConfig = async (): Promise<void> => {
const config = await getConfig() const config = await getConfig()
if (!config) { if (!config) {

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -12,6 +12,9 @@ import { fetchFrontendConfig } from './fetch-frontend-config'
import { loadDarkMode } from './load-dark-mode' import { loadDarkMode } from './load-dark-mode'
import { isDevMode, isTestMode } from '../../../utils/test-modes' import { isDevMode, isTestMode } from '../../../utils/test-modes'
/**
* Create a custom delay in the loading of the application.
*/
const customDelay: () => Promise<void> = async () => { const customDelay: () => Promise<void> = async () => {
if ( if (
(isDevMode || isTestMode) && (isDevMode || isTestMode) &&
@ -30,6 +33,9 @@ export interface InitTask {
task: () => Promise<void> task: () => Promise<void>
} }
/**
* Create a list of tasks, that need to be fulfilled on startup.
*/
export const createSetUpTaskList = (): InitTask[] => { export const createSetUpTaskList = (): InitTask[] => {
return [ return [
{ {

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -31,7 +31,8 @@ export const loadDarkMode = async (): Promise<void> => {
/** /**
* Tries to read the saved dark mode settings from the browser local storage. * Tries to read the saved dark mode settings from the browser local storage.
* *
* @return {@code true} if the local storage has saved that the user prefers dark mode. {@code false} if the user doesn't or if the value could be read from local storage. * @return {@link true} if the local storage has saved that the user prefers dark mode.
* {@link false} if the user doesn't prefer dark mode or if the value couldn't be read from local storage.
*/ */
const fetchDarkModeFromLocalStorage = (): boolean => { const fetchDarkModeFromLocalStorage = (): boolean => {
if (!isClientSideRendering()) { if (!isClientSideRendering()) {
@ -48,7 +49,9 @@ const fetchDarkModeFromLocalStorage = (): boolean => {
/** /**
* Tries to read the preferred dark mode setting from the browser settings. * Tries to read the preferred dark mode setting from the browser settings.
* *
* @return {@code true} if the browser has reported that the user prefers dark mode. {@code false} if the user doesn't or if the browser doesn't support the `prefers-color-scheme` media query. * @return {@link true} if the browser has reported that the user prefers dark mode.
* {@link false} if the browser doesn't prefer dark mode.
* {@link undefined} if the browser doesn't support the `prefers-color-scheme` media query.
*/ */
const determineDarkModeBrowserSettings = (): DarkModeConfig | undefined => { const determineDarkModeBrowserSettings = (): DarkModeConfig | undefined => {
if (!isClientSideRendering()) { if (!isClientSideRendering()) {

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -12,6 +12,9 @@ import { Settings } from 'luxon'
import { initReactI18next } from 'react-i18next' import { initReactI18next } from 'react-i18next'
import { isDevMode } from '../../../utils/test-modes' import { isDevMode } from '../../../utils/test-modes'
/**
* Set up the internationalisation framework i18n.
*/
export const setUpI18n = async (): Promise<void> => { export const setUpI18n = async (): Promise<void> => {
await i18nUse( await i18nUse(
resourcesToBackend((language, namespace, callback) => { resourcesToBackend((language, namespace, callback) => {

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -14,6 +14,13 @@ export interface BrandingProps {
delimiter?: boolean delimiter?: boolean
} }
/**
* Show the branding of the HedgeDoc instance.
* This branding can either be a text, a logo or both (in that case the text is used as the title and alt of the image).
*
* @param inline If the logo should be using the inline-size or the regular-size css class.
* @param delimiter If the delimiter between the HedgeDoc logo and the branding should be shown.
*/
export const Branding: React.FC<BrandingProps> = ({ inline = false, delimiter = true }) => { export const Branding: React.FC<BrandingProps> = ({ inline = false, delimiter = true }) => {
const branding = useApplicationState((state) => state.config.branding) const branding = useApplicationState((state) => state.config.branding)
const showBranding = !!branding.name || !!branding.logo const showBranding = !!branding.name || !!branding.logo

View file

@ -1,91 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Cache } from './cache'
describe('Test caching functionality', () => {
let testCache: Cache<string, number>
beforeAll(() => {
jest.useFakeTimers()
})
afterAll(() => {
jest.useRealTimers()
})
beforeEach(() => {
testCache = new Cache<string, number>(1000)
})
it('initialize with right lifetime, no entry limit', () => {
const lifetime = 1000
const lifetimedCache = new Cache<string, string>(lifetime)
expect(lifetimedCache.entryLifetime).toEqual(lifetime)
expect(lifetimedCache.maxEntries).toEqual(0)
})
it('initialize with right lifetime, given entry limit', () => {
const lifetime = 1000
const maxEntries = 10
const limitedCache = new Cache<string, string>(lifetime, maxEntries)
expect(limitedCache.entryLifetime).toEqual(lifetime)
expect(limitedCache.maxEntries).toEqual(maxEntries)
})
it('entry exists after inserting', () => {
testCache.put('test', 123)
expect(testCache.has('test')).toBe(true)
})
it('entry does not exist prior inserting', () => {
expect(testCache.has('test')).toBe(false)
})
it('entry does expire', () => {
const shortLivingCache = new Cache<string, number>(2)
shortLivingCache.put('test', 123)
expect(shortLivingCache.has('test')).toBe(true)
setTimeout(() => {
expect(shortLivingCache.has('test')).toBe(false)
}, 2000)
})
it('entry value does not change', () => {
const testValue = Date.now()
testCache.put('test', testValue)
expect(testCache.get('test')).toEqual(testValue)
})
it('error is thrown on non-existent entry', () => {
const accessNonExistentEntry = () => {
testCache.get('test')
}
expect(accessNonExistentEntry).toThrow(Error)
})
it('newer item replaces older item', () => {
testCache.put('test', 123)
testCache.put('test', 456)
expect(testCache.get('test')).toEqual(456)
})
it('entry limit is respected', () => {
const limitedCache = new Cache<string, number>(1000, 2)
limitedCache.put('first', 1)
expect(limitedCache.has('first')).toBe(true)
expect(limitedCache.has('second')).toBe(false)
expect(limitedCache.has('third')).toBe(false)
limitedCache.put('second', 2)
expect(limitedCache.has('first')).toBe(true)
expect(limitedCache.has('second')).toBe(true)
expect(limitedCache.has('third')).toBe(false)
limitedCache.put('third', 3)
expect(limitedCache.has('first')).toBe(false)
expect(limitedCache.has('second')).toBe(true)
expect(limitedCache.has('third')).toBe(true)
})
})

View file

@ -1,50 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export interface CacheEntry<T> {
entryCreated: number
data: T
}
export class Cache<K, V> {
readonly entryLifetime: number
readonly maxEntries: number
private store = new Map<K, CacheEntry<V>>()
constructor(lifetime: number, maxEntries = 0) {
if (lifetime < 0) {
throw new Error('Cache entry lifetime can not be less than 0 seconds.')
}
this.entryLifetime = lifetime
this.maxEntries = maxEntries
}
has(key: K): boolean {
if (!this.store.has(key)) {
return false
}
const entry = this.store.get(key)
return !!entry && entry.entryCreated >= Date.now() - this.entryLifetime * 1000
}
get(key: K): V {
const entry = this.store.get(key)
if (!entry) {
throw new Error('This cache entry does not exist. Check with ".has()" before using ".get()".')
}
return entry.data
}
put(key: K, value: V): void {
if (this.maxEntries > 0 && this.store.size === this.maxEntries) {
this.store.delete(this.store.keys().next().value as K)
}
this.store.set(key, {
entryCreated: Date.now(),
data: value
})
}
}

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -21,7 +21,7 @@ export interface CopyableFieldProps {
const log = new Logger('CopyableField') const log = new Logger('CopyableField')
/** /**
* Provides an input field with an attached copy button and a share button (if supported by the browser) * Provides an input field with an attached copy button and a share button (if supported by the browser).
* *
* @param content The content to present * @param content The content to present
* @param shareOriginUrl The URL of the page to which the shared content should be linked. If this value is omitted then the share button won't be shown. * @param shareOriginUrl The URL of the page to which the shared content should be linked. If this value is omitted then the share button won't be shown.

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -15,7 +15,10 @@ export interface CountdownButtonProps extends ButtonProps {
/** /**
* Button that starts a countdown on render and is only clickable after the countdown has finished. * Button that starts a countdown on render and is only clickable after the countdown has finished.
*
* @param countdownStartSeconds The initial amount of seconds for the countdown. * @param countdownStartSeconds The initial amount of seconds for the countdown.
* @param children The children that should be displayed after the countdown has elapsed.
* @param props Additional props given to the {@link Button}.
*/ */
export const CountdownButton: React.FC<CountdownButtonProps> = ({ countdownStartSeconds, children, ...props }) => { export const CountdownButton: React.FC<CountdownButtonProps> = ({ countdownStartSeconds, children, ...props }) => {
const [secondsRemaining, setSecondsRemaining] = useState(countdownStartSeconds) const [secondsRemaining, setSecondsRemaining] = useState(countdownStartSeconds)

View file

@ -1,15 +1,22 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
/**
* Download a given {@link BlobPart file} from memory<.
*
* @param data The file to download.
* @param fileName Which filename does the file have.
* @param mimeType What is the files mimetype.
*/
export const download = (data: BlobPart, fileName: string, mimeType: string): void => { export const download = (data: BlobPart, fileName: string, mimeType: string): void => {
const file = new Blob([data], { type: mimeType }) const file = new Blob([data], { type: mimeType })
downloadLink(URL.createObjectURL(file), fileName) downloadLink(URL.createObjectURL(file), fileName)
} }
export const downloadLink = (url: string, fileName: string): void => { const downloadLink = (url: string, fileName: string): void => {
const helperElement = document.createElement('a') const helperElement = document.createElement('a')
helperElement.href = url helperElement.href = url
helperElement.download = fileName helperElement.download = fileName

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -11,6 +11,7 @@ import { Trans, useTranslation } from 'react-i18next'
/** /**
* Renders an input field for the current password when changing passwords. * Renders an input field for the current password when changing passwords.
*
* @param onChange Hook that is called when the entered password changes. * @param onChange Hook that is called when the entered password changes.
*/ */
export const CurrentPasswordField: React.FC<CommonFieldProps> = ({ onChange }) => { export const CurrentPasswordField: React.FC<CommonFieldProps> = ({ onChange }) => {

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -15,6 +15,7 @@ interface DisplayNameFieldProps extends CommonFieldProps {
/** /**
* Renders an input field for the display name when registering. * Renders an input field for the display name when registering.
*
* @param onChange Hook that is called when the entered display name changes. * @param onChange Hook that is called when the entered display name changes.
* @param value The currently entered display name. * @param value The currently entered display name.
* @param initialValue The initial input field value. * @param initialValue The initial input field value.

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -11,6 +11,7 @@ import { Trans, useTranslation } from 'react-i18next'
/** /**
* Renders an input field for the new password when registering. * Renders an input field for the new password when registering.
*
* @param onChange Hook that is called when the entered password changes. * @param onChange Hook that is called when the entered password changes.
* @param value The currently entered password. * @param value The currently entered password.
*/ */

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -15,6 +15,7 @@ interface PasswordAgainFieldProps extends CommonFieldProps {
/** /**
* Renders an input field for typing the new password again when registering. * Renders an input field for typing the new password again when registering.
*
* @param onChange Hook that is called when the entered retype of the password changes. * @param onChange Hook that is called when the entered retype of the password changes.
* @param value The currently entered retype of the password. * @param value The currently entered retype of the password.
* @param password The password entered into the password input field. * @param password The password entered into the password input field.

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -11,6 +11,7 @@ import { Trans, useTranslation } from 'react-i18next'
/** /**
* Renders an input field for the username when registering. * Renders an input field for the username when registering.
*
* @param onChange Hook that is called when the entered username changes. * @param onChange Hook that is called when the entered username changes.
* @param value The currently entered username. * @param value The currently entered username.
*/ */

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -15,6 +15,16 @@ export interface ForkAwesomeIconProps {
stacked?: boolean stacked?: boolean
} }
/**
* Renders a fork awesome icon.
*
* @param icon The icon that should be rendered.
* @param fixedWidth If the icon should be rendered with a fixed width.
* @param size The size class the icon should be rendered in.
* @param className Additional classes the icon should get.
* @param stacked If the icon is part of a {@link ForkAwesomeStack stack}.
* @see https://forkaweso.me
*/
export const ForkAwesomeIcon: React.FC<ForkAwesomeIconProps> = ({ export const ForkAwesomeIcon: React.FC<ForkAwesomeIconProps> = ({
icon, icon,
fixedWidth = false, fixedWidth = false,

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -15,6 +15,12 @@ export interface ForkAwesomeStackProps {
children: ReactElement<ForkAwesomeIconProps> | Array<ReactElement<ForkAwesomeIconProps>> children: ReactElement<ForkAwesomeIconProps> | Array<ReactElement<ForkAwesomeIconProps>>
} }
/**
* A stack of {@link ForkAwesomeIcon ForkAwesomeIcons}.
*
* @param size Which size the stack should have.
* @param children One or more {@link ForkAwesomeIcon ForkAwesomeIcons} to be stacked.
*/
export const ForkAwesomeStack: React.FC<ForkAwesomeStackProps> = ({ size, children }) => { export const ForkAwesomeStack: React.FC<ForkAwesomeStackProps> = ({ size, children }) => {
return ( return (
<span className={`fa-stack ${size ? 'fa-' : ''}${size ?? ''}`}> <span className={`fa-stack ${size ? 'fa-' : ''}${size ?? ''}`}>

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -27,6 +27,12 @@ export enum HedgeDocLogoType {
WB_HORIZONTAL WB_HORIZONTAL
} }
/**
* Renders the HedgeDoc logo with the app name in different types.
*
* @param size The size the logo should have.
* @param logoType The logo type to be used.
*/
export const HedgeDocLogoWithText: React.FC<HedgeDocLogoProps> = ({ size = HedgeDocLogoSize.MEDIUM, logoType }) => { export const HedgeDocLogoWithText: React.FC<HedgeDocLogoProps> = ({ size = HedgeDocLogoSize.MEDIUM, logoType }) => {
const { t } = useTranslation() const { t } = useTranslation()
const altText = useMemo(() => t('app.icon'), [t]) const altText = useMemo(() => t('app.icon'), [t])

View file

@ -20,6 +20,16 @@ export interface IconButtonProps extends ButtonProps {
iconFixedWidth?: boolean iconFixedWidth?: boolean
} }
/**
* A generic {@link Button button} with an {@link ForkAwesomeIcon icon} in it.
*
* @param icon Which icon should be used
* @param children The children that will be added as the content of the button.
* @param iconFixedWidth If the icon should be of fixed width.
* @param border Should the button have a border.
* @param className Additional class names added to the button.
* @param props Additional props for the button.
*/
export const IconButton: React.FC<IconButtonProps> = ({ export const IconButton: React.FC<IconButtonProps> = ({
icon, icon,
children, children,

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -13,6 +13,12 @@ export interface TranslatedIconButtonProps extends IconButtonProps {
i18nKey: string i18nKey: string
} }
/**
* Renders an {@link IconButton icon button} with a translation inside.
*
* @param i18nKey The key for the translated string.
* @param props Additional props directly given to the {@link IconButton}.
*/
export const TranslatedIconButton: React.FC<TranslatedIconButtonProps> = ({ i18nKey, ...props }) => { export const TranslatedIconButton: React.FC<TranslatedIconButtonProps> = ({ i18nKey, ...props }) => {
return ( return (
<IconButton {...props}> <IconButton {...props}>

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -10,6 +10,18 @@ import type { IconName } from '../fork-awesome/types'
import { ShowIf } from '../show-if/show-if' import { ShowIf } from '../show-if/show-if'
import type { LinkWithTextProps } from './types' import type { LinkWithTextProps } from './types'
/**
* An external link.
* This should be used for linking pages that are not part of the HedgeDoc instance.
* The links will be opened in a new tab.
*
* @param href The links location
* @param text The links text
* @param icon An optional icon to be shown before the links text
* @param id An id for the link object
* @param className Additional class names added to the link object
* @param title The title of the link
*/
export const ExternalLink: React.FC<LinkWithTextProps> = ({ export const ExternalLink: React.FC<LinkWithTextProps> = ({
href, href,
text, text,

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -11,6 +11,17 @@ import type { IconName } from '../fork-awesome/types'
import { ShowIf } from '../show-if/show-if' import { ShowIf } from '../show-if/show-if'
import type { LinkWithTextProps } from './types' import type { LinkWithTextProps } from './types'
/**
* An internal link.
* This should be used for linking pages of the HedgeDoc instance.
*
* @param href The links location
* @param text The links text
* @param icon An optional icon to be shown before the links text
* @param id An id for the link object
* @param className Additional class names added to the link object
* @param title The title of the link
*/
export const InternalLink: React.FC<LinkWithTextProps> = ({ export const InternalLink: React.FC<LinkWithTextProps> = ({
href, href,
text, text,

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -9,6 +9,13 @@ import { useTranslation } from 'react-i18next'
import { ExternalLink } from './external-link' import { ExternalLink } from './external-link'
import type { TranslatedLinkProps } from './types' import type { TranslatedLinkProps } from './types'
/**
* An {@link ExternalLink external link} with translated text.
*
* @param i18nKey The key of the translation
* @param i18nOption The translation options
* @param props Additional props directly given to the {@link ExternalLink}
*/
export const TranslatedExternalLink: React.FC<TranslatedLinkProps> = ({ i18nKey, i18nOption, ...props }) => { export const TranslatedExternalLink: React.FC<TranslatedLinkProps> = ({ i18nKey, i18nOption, ...props }) => {
const { t } = useTranslation() const { t } = useTranslation()
return <ExternalLink text={t(i18nKey, i18nOption)} {...props} /> return <ExternalLink text={t(i18nKey, i18nOption)} {...props} />

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -8,7 +8,13 @@ import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { InternalLink } from './internal-link' import { InternalLink } from './internal-link'
import type { TranslatedLinkProps } from './types' import type { TranslatedLinkProps } from './types'
/**
* An {@link InternalLink internal link} with translated text.
*
* @param i18nKey The key of the translation
* @param i18nOption The translation options
* @param props Additional props directly given to the {@link InternalLink}
*/
export const TranslatedInternalLink: React.FC<TranslatedLinkProps> = ({ i18nKey, i18nOption, ...props }) => { export const TranslatedInternalLink: React.FC<TranslatedLinkProps> = ({ i18nKey, i18nOption, ...props }) => {
const { t } = useTranslation() const { t } = useTranslation()
return <InternalLink text={t(i18nKey, i18nOption)} {...props} /> return <InternalLink text={t(i18nKey, i18nOption)} {...props} />

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -14,6 +14,13 @@ export interface LockButtonProps {
title: string title: string
} }
/**
* A button with a lock icon.
*
* @param locked If the button should be shown as locked or not
* @param onLockedChanged The callback to change the state from locked to unlocked and vise-versa.
* @param title The title for the button.
*/
export const LockButton: React.FC<LockButtonProps> = ({ locked, onLockedChanged, title }) => { export const LockButton: React.FC<LockButtonProps> = ({ locked, onLockedChanged, title }) => {
return ( return (
<Button variant='dark' size='sm' onClick={() => onLockedChanged(!locked)} title={title}> <Button variant='dark' size='sm' onClick={() => onLockedChanged(!locked)} title={title}>

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -30,6 +30,20 @@ export interface ModalContentProps {
export type CommonModalProps = PropsWithDataCypressId & ModalVisibilityProps & ModalContentProps export type CommonModalProps = PropsWithDataCypressId & ModalVisibilityProps & ModalContentProps
/**
* Renders a generic modal.
*
* @param show If the modal should be shown or not.
* @param onHide The callback to hide the modal again
* @param title The title in the header of the modal
* @param showCloseButton If a close button should be shown
* @param titleIcon An optional title icon
* @param additionalClasses Additional class names for the modal
* @param modalSize The modal size
* @param children The children to render into the modal.
* @param titleIsI18nKey If the title is a i18n key and should be used as such
* @param props Additional props directly given to the modal
*/
export const CommonModal: React.FC<PropsWithChildren<CommonModalProps>> = ({ export const CommonModal: React.FC<PropsWithChildren<CommonModalProps>> = ({
show, show,
onHide, onHide,

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -17,6 +17,19 @@ export interface DeletionModalProps extends CommonModalProps {
deletionButtonI18nKey: string deletionButtonI18nKey: string
} }
/**
* Renders a generic modal for deletion.
* This means in addition to most things for the {@link CommonModal} there is also a button to confirm the deletion and a corresponding callback.
*
* @param show If the modal should be shown or not.
* @param onHide The callback to hide the modal again
* @param title The title in the header of the modal
* @param onConfirm The callback for the delete button.
* @param deletionButtonI18nKey The i18n key for the deletion button.
* @param titleIcon An optional title icon
* @param children The children to render into the modal.
* @param props Additional props directly given to the modal
*/
export const DeletionModal: React.FC<PropsWithChildren<DeletionModalProps>> = ({ export const DeletionModal: React.FC<PropsWithChildren<DeletionModalProps>> = ({
show, show,
onHide, onHide,

View file

@ -1,9 +1,15 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
/**
* Create an array with numbers from 1 to n.
*
* @param length The length of the array (or the number n)
* @return An array of numbers from 1 to n
*/
export const createNumberRangeArray = (length: number): number[] => { export const createNumberRangeArray = (length: number): number[] => {
return Array.from(Array(length).keys()) return Array.from(Array(length).keys())
} }

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -11,6 +11,12 @@ export interface PageItemProps {
index: number index: number
} }
/**
* Renders a number and adds an onClick handler to it.
*
* @param index The number to render
* @param onClick The onClick Handler
*/
export const PagerItem: React.FC<PageItemProps> = ({ index, onClick }) => { export const PagerItem: React.FC<PageItemProps> = ({ index, onClick }) => {
return ( return (
<li className='page-item'> <li className='page-item'>

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -15,6 +15,13 @@ export interface PaginationProps {
lastPageIndex: number lastPageIndex: number
} }
/**
* Renders a pagination menu to move back and forth between pages.
*
* @param numberOfPageButtonsToShowAfterAndBeforeCurrent The number of buttons that should be shown before and after the current button.
* @param onPageChange The callback when one of the buttons is clicked
* @param lastPageIndex The index of the last page
*/
export const PagerPagination: React.FC<PaginationProps> = ({ export const PagerPagination: React.FC<PaginationProps> = ({
numberOfPageButtonsToShowAfterAndBeforeCurrent, numberOfPageButtonsToShowAfterAndBeforeCurrent,
onPageChange, onPageChange,

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -13,6 +13,14 @@ export interface PagerPageProps {
onLastPageIndexChange: (lastPageIndex: number) => void onLastPageIndexChange: (lastPageIndex: number) => void
} }
/**
* Renders a limited number of the given children.
*
* @param children The children to render
* @param numberOfElementsPerPage The number of elements per page
* @param pageIndex Which page of the children to render
* @param onLastPageIndexChange A callback to notify about changes to the maximal page number
*/
export const Pager: React.FC<PropsWithChildren<PagerPageProps>> = ({ export const Pager: React.FC<PropsWithChildren<PagerPageProps>> = ({
children, children,
numberOfElementsPerPage, numberOfElementsPerPage,

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -11,6 +11,12 @@ export interface ShowIfProps {
condition: boolean condition: boolean
} }
/**
* Renders the children if the condition is met.
*
* @param children The children to show if the condition is met.
* @param condition If the children should be shown
*/
export const ShowIf: React.FC<PropsWithChildren<ShowIfProps>> = ({ children, condition }) => { export const ShowIf: React.FC<PropsWithChildren<ShowIfProps>> = ({ children, condition }) => {
return condition ? <Fragment>{children}</Fragment> : null return condition ? <Fragment>{children}</Fragment> : null
} }

View file

@ -21,9 +21,10 @@ export interface UserAvatarForUsernameProps extends Omit<UserAvatarProps, 'user'
* Renders the user avatar for a given username. * Renders the user avatar for a given username.
* When no username is given, the guest user will be used as fallback. * When no username is given, the guest user will be used as fallback.
* *
* @see {UserAvatar} * @see UserAvatar
* *
* @param username The username for which to show the avatar or null to show the guest user avatar. * @param username The username for which to show the avatar or null to show the guest user avatar.
* @param props Additional props directly given to the {@link UserAvatar}
*/ */
export const UserAvatarForUsername: React.FC<UserAvatarForUsernameProps> = ({ username, ...props }) => { export const UserAvatarForUsername: React.FC<UserAvatarForUsernameProps> = ({ username, ...props }) => {
const { t } = useTranslation() const { t } = useTranslation()

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -7,6 +7,9 @@
import React from 'react' import React from 'react'
import { ForkAwesomeIcon } from '../fork-awesome/fork-awesome-icon' import { ForkAwesomeIcon } from '../fork-awesome/fork-awesome-icon'
/**
* Renders a indefinitely spinning spinner.
*/
export const WaitSpinner: React.FC = () => { export const WaitSpinner: React.FC = () => {
return ( return (
<div className={'m-3 d-flex align-items-center justify-content-center'}> <div className={'m-3 d-flex align-items-center justify-content-center'}>

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -13,7 +13,7 @@ import { NoteInfoLineCreated } from '../editor-page/document-bar/note-info/note-
import { NoteInfoLineUpdated } from '../editor-page/document-bar/note-info/note-info-line-updated' import { NoteInfoLineUpdated } from '../editor-page/document-bar/note-info/note-info-line-updated'
/** /**
* Renders an infobar with metadata about the current note. * Renders an info bar with metadata about the current note.
*/ */
export const DocumentInfobar: React.FC = () => { export const DocumentInfobar: React.FC = () => {
const { t } = useTranslation() const { t } = useTranslation()

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -29,6 +29,11 @@ export interface AppBarProps {
mode: AppBarMode mode: AppBarMode
} }
/**
* Renders the app bar.
*
* @param mode Which mode the app bar should be rendered in. This mainly adds / removes buttons for the editor.
*/
export const AppBar: React.FC<AppBarProps> = ({ mode }) => { export const AppBar: React.FC<AppBarProps> = ({ mode }) => {
const userExists = useApplicationState((state) => !!state.user) const userExists = useApplicationState((state) => !!state.user)
const noteFrontmatter = useApplicationState((state) => state.noteDetails.frontmatter) const noteFrontmatter = useApplicationState((state) => state.noteDetails.frontmatter)

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -16,6 +16,9 @@ enum DarkModeState {
LIGHT LIGHT
} }
/**
* Renders a button group to activate / deactivate the dark mode.
*/
const DarkModeButton: React.FC = () => { const DarkModeButton: React.FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const darkModeEnabled = useIsDarkModeActivated() ? DarkModeState.DARK : DarkModeState.LIGHT const darkModeEnabled = useIsDarkModeActivated() ? DarkModeState.DARK : DarkModeState.LIGHT

View file

@ -18,6 +18,10 @@ export enum EditorMode {
EDITOR = 'edit' EDITOR = 'edit'
} }
/**
* Renders the button group to set the editor mode.
* @see EditorMode
*/
export const EditorViewMode: React.FC = () => { export const EditorViewMode: React.FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const editorMode = useApplicationState((state) => state.editorConfig.editorMode) const editorMode = useApplicationState((state) => state.editorConfig.editorMode)

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -17,6 +17,13 @@ const HighlightedCode = React.lazy(
) )
const DocumentMarkdownRenderer = React.lazy(() => import('../../../markdown-renderer/document-markdown-renderer')) const DocumentMarkdownRenderer = React.lazy(() => import('../../../markdown-renderer/document-markdown-renderer'))
/**
* Renders one line in the {@link CheatsheetTabContent cheat sheet}.
* This line shows an minimal markdown example and how it would be rendered.
*
* @param markdown The markdown to be shown and rendered
* @param onTaskCheckedChange A callback to call if a task would be clicked
*/
export const CheatsheetLine: React.FC<CheatsheetLineProps> = ({ markdown, onTaskCheckedChange }) => { export const CheatsheetLine: React.FC<CheatsheetLineProps> = ({ markdown, onTaskCheckedChange }) => {
const lines = useMemo(() => markdown.split('\n'), [markdown]) const lines = useMemo(() => markdown.split('\n'), [markdown])
const checkboxClick = useCallback( const checkboxClick = useCallback(

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -10,6 +10,9 @@ import { Trans, useTranslation } from 'react-i18next'
import { CheatsheetLine } from './cheatsheet-line' import { CheatsheetLine } from './cheatsheet-line'
import styles from './cheatsheet.module.scss' import styles from './cheatsheet.module.scss'
/**
* Renders the content of the cheat sheet for the {@link HelpModal}.
*/
export const CheatsheetTabContent: React.FC = () => { export const CheatsheetTabContent: React.FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const [checked, setChecked] = useState<boolean>(false) const [checked, setChecked] = useState<boolean>(false)

View file

@ -12,6 +12,9 @@ import { HelpModal } from './help-modal'
import { cypressId } from '../../../../utils/cypress-attribute' import { cypressId } from '../../../../utils/cypress-attribute'
import { useBooleanState } from '../../../../hooks/common/use-boolean-state' import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
/**
* Renders the button to open the {@link HelpModal}.
*/
export const HelpButton: React.FC = () => { export const HelpButton: React.FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const [modalVisibility, showModal, closeModal] = useBooleanState() const [modalVisibility, showModal, closeModal] = useBooleanState()

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -19,6 +19,17 @@ export enum HelpTabStatus {
Links = 'links.title' Links = 'links.title'
} }
/**
* Renders the help modal.
* This modal shows the user the markdown cheatsheet, shortcuts and different links with further help.
*
* @see CheatsheetTabContent
* @see ShortcutTabContent
* @see LinksTabContent
*
* @param show If the modal should be shown
* @param onHide A callback when the modal should be closed again
*/
export const HelpModal: React.FC<ModalVisibilityProps> = ({ show, onHide }) => { export const HelpModal: React.FC<ModalVisibilityProps> = ({ show, onHide }) => {
const [tab, setTab] = useState<HelpTabStatus>(HelpTabStatus.Cheatsheet) const [tab, setTab] = useState<HelpTabStatus>(HelpTabStatus.Cheatsheet)
const { t } = useTranslation() const { t } = useTranslation()

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -11,6 +11,9 @@ import links from '../../../../links.json'
import { TranslatedExternalLink } from '../../../common/links/translated-external-link' import { TranslatedExternalLink } from '../../../common/links/translated-external-link'
import { TranslatedInternalLink } from '../../../common/links/translated-internal-link' import { TranslatedInternalLink } from '../../../common/links/translated-internal-link'
/**
* Renders a bunch of links, where further help can be requested.
*/
export const LinksTabContent: React.FC = () => { export const LinksTabContent: React.FC = () => {
useTranslation() useTranslation()

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -9,6 +9,9 @@ import { Card, ListGroup, Row } from 'react-bootstrap'
import { Trans } from 'react-i18next' import { Trans } from 'react-i18next'
import { isMac } from '../../utils' import { isMac } from '../../utils'
/**
* Renders a list of shortcuts usable in HedgeDoc.
*/
export const ShortcutTabContent: React.FC = () => { export const ShortcutTabContent: React.FC = () => {
const modifierKey = useMemo(() => (isMac() ? <kbd></kbd> : <kbd>Ctrl</kbd>), []) const modifierKey = useMemo(() => (isMac() ? <kbd></kbd> : <kbd>Ctrl</kbd>), [])
const altKey = useMemo(() => (isMac() ? <kbd></kbd> : <kbd>Alt</kbd>), []) const altKey = useMemo(() => (isMac() ? <kbd></kbd> : <kbd>Alt</kbd>), [])

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -15,6 +15,9 @@ import {
HedgeDocLogoWithText HedgeDocLogoWithText
} from '../../common/hedge-doc-logo/hedge-doc-logo-with-text' } from '../../common/hedge-doc-logo/hedge-doc-logo-with-text'
/**
* Renders the branding for the {@link AppBar}
*/
export const NavbarBranding: React.FC = () => { export const NavbarBranding: React.FC = () => {
const darkModeActivated = useIsDarkModeActivated() const darkModeActivated = useIsDarkModeActivated()

View file

@ -10,6 +10,9 @@ import { Trans, useTranslation } from 'react-i18next'
import { Button } from 'react-bootstrap' import { Button } from 'react-bootstrap'
import Link from 'next/link' import Link from 'next/link'
/**
* Renders a button to create a new note.
*/
export const NewNoteButton: React.FC = () => { export const NewNoteButton: React.FC = () => {
useTranslation() useTranslation()

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -18,6 +18,10 @@ enum SyncScrollState {
UNSYNCED UNSYNCED
} }
/**
* Renders a button group with two states for the sync scroll buttons.
* This makes it possible to activate or deactivate sync scrolling.
*/
export const SyncScrollButtons: React.FC = () => { export const SyncScrollButtons: React.FC = () => {
const syncScrollEnabled = useApplicationState((state) => state.editorConfig.syncScroll) const syncScrollEnabled = useApplicationState((state) => state.editorConfig.syncScroll)
? SyncScrollState.SYNCED ? SyncScrollState.SYNCED

View file

@ -24,7 +24,9 @@ type ChangeEditorContentContext = [CodeMirrorReference, SetCodeMirrorReference]
const changeEditorContentContext = createContext<ChangeEditorContentContext | undefined>(undefined) const changeEditorContentContext = createContext<ChangeEditorContentContext | undefined>(undefined)
/** /**
* Extracts the code mirror reference from the parent context * Extracts the {@link CodeMirrorReference code mirror reference} from the parent context.
*
* @return The {@link CodeMirrorReference} from the parent context.
*/ */
export const useCodeMirrorReference = (): CodeMirrorReference => { export const useCodeMirrorReference = (): CodeMirrorReference => {
const contextContent = Optional.ofNullable(useContext(changeEditorContentContext)).orElseThrow( const contextContent = Optional.ofNullable(useContext(changeEditorContentContext)).orElseThrow(
@ -34,7 +36,9 @@ export const useCodeMirrorReference = (): CodeMirrorReference => {
} }
/** /**
* Extracts the code mirror reference from the parent context * Provides a function to set the {@link CodeMirrorReference code mirror reference} in the current context.
*
* @return A function to set a {@link CodeMirrorReference code mirror reference}.
*/ */
export const useSetCodeMirrorReference = (): SetCodeMirrorReference => { export const useSetCodeMirrorReference = (): SetCodeMirrorReference => {
const contextContent = Optional.ofNullable(useContext(changeEditorContentContext)).orElseThrow( const contextContent = Optional.ofNullable(useContext(changeEditorContentContext)).orElseThrow(

View file

@ -13,6 +13,7 @@ import { DateTime } from 'luxon'
/** /**
* Renders an info line about the creation of the current note. * Renders an info line about the creation of the current note.
*
* @param size The size in which the line should be displayed. * @param size The size in which the line should be displayed.
*/ */
export const NoteInfoLineCreated: React.FC<NoteInfoTimeLineProps> = ({ size }) => { export const NoteInfoLineCreated: React.FC<NoteInfoTimeLineProps> = ({ size }) => {

View file

@ -15,6 +15,7 @@ import { DateTime } from 'luxon'
/** /**
* Renders an info line about the last update of the current note. * Renders an info line about the last update of the current note.
*
* @param size The size in which line and user avatar should be displayed. * @param size The size in which line and user avatar should be displayed.
*/ */
export const NoteInfoLineUpdated: React.FC<NoteInfoTimeLineProps> = ({ size }) => { export const NoteInfoLineUpdated: React.FC<NoteInfoTimeLineProps> = ({ size }) => {

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -14,6 +14,14 @@ export interface NoteInfoLineProps {
size?: '2x' | '3x' | '4x' | '5x' | undefined size?: '2x' | '3x' | '4x' | '5x' | undefined
} }
/**
* This is the base component for all note info lines.
* It renders an icon and some children in italic.
*
* @param icon The icon be shown
* @param size Which size the icon should be
* @param children The children to render
*/
export const NoteInfoLine: React.FC<PropsWithChildren<NoteInfoLineProps>> = ({ icon, size, children }) => { export const NoteInfoLine: React.FC<PropsWithChildren<NoteInfoLineProps>> = ({ icon, size, children }) => {
return ( return (
<span className={'d-flex align-items-center'}> <span className={'d-flex align-items-center'}>

View file

@ -17,6 +17,7 @@ import { NoteInfoLineContributors } from './note-info-line-contributors'
/** /**
* Modal that shows informational data about the current note. * Modal that shows informational data about the current note.
*
* @param show true when the modal should be visible, false otherwise. * @param show true when the modal should be visible, false otherwise.
* @param onHide Callback that is fired when the modal is going to be closed. * @param onHide Callback that is fired when the modal is going to be closed.
*/ */

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -12,6 +12,11 @@ export interface TimeFromNowProps {
time: DateTime time: DateTime
} }
/**
* Renders a given time relative to the current time.
*
* @param time The time to be rendered.
*/
export const TimeFromNow: React.FC<TimeFromNowProps> = ({ time }) => { export const TimeFromNow: React.FC<TimeFromNowProps> = ({ time }) => {
return ( return (
<time className={'mx-1'} title={time.toFormat('DDDD T')} dateTime={time.toString()}> <time className={'mx-1'} title={time.toFormat('DDDD T')} dateTime={time.toString()}>

View file

@ -6,15 +6,19 @@
import type { PropsWithChildren } from 'react' import type { PropsWithChildren } from 'react'
import React from 'react' import React from 'react'
import type { PropsWithDataCypressId } from '../../../../utils/cypress-attribute'
import { cypressId } from '../../../../utils/cypress-attribute' import { cypressId } from '../../../../utils/cypress-attribute'
export interface UnitalicBoldContentProps { export interface UnitalicBoldContentProps extends PropsWithDataCypressId {
text?: string | number text?: string | number
} }
/** /**
* Renders the children elements in a non-italic but bold style. * Renders the children elements in a non-italic but bold style.
*
* @param text Optional text content that should be rendered. * @param text Optional text content that should be rendered.
* @param children Children that may be rendered.
* @param props Additional props for cypressId
*/ */
export const UnitalicBoldContent: React.FC<PropsWithChildren<UnitalicBoldContentProps>> = ({ export const UnitalicBoldContent: React.FC<PropsWithChildren<UnitalicBoldContentProps>> = ({
text, text,

View file

@ -17,6 +17,7 @@ export interface PermissionAddEntryFieldProps {
/** /**
* Permission entry row containing a field for adding new user permission entries. * Permission entry row containing a field for adding new user permission entries.
*
* @param onAddEntry Callback that is fired with the entered username as identifier of the entry to add. * @param onAddEntry Callback that is fired with the entered username as identifier of the entry to add.
* @param i18nKey The localization key for the submit button. * @param i18nKey The localization key for the submit button.
*/ */

View file

@ -31,6 +31,7 @@ export interface PermissionEntryButtonsProps {
/** /**
* Buttons next to a user or group permission entry to change the permissions or remove the entry. * Buttons next to a user or group permission entry to change the permissions or remove the entry.
*
* @param name The name of the user or group. * @param name The name of the user or group.
* @param type The type of the entry. Either {@link PermissionType.USER} or {@link PermissionType.GROUP}. * @param type The type of the entry. Either {@link PermissionType.USER} or {@link PermissionType.GROUP}.
* @param currentSetting How the permission is currently set. * @param currentSetting How the permission is currently set.

View file

@ -20,6 +20,7 @@ export interface PermissionEntrySpecialGroupProps {
/** /**
* Permission entry that represents one of the built-in special groups. * Permission entry that represents one of the built-in special groups.
*
* @param level The access level that is currently set for the group. * @param level The access level that is currently set for the group.
* @param type The type of the special group. Must be either {@link SpecialGroup.EVERYONE} or {@link SpecialGroup.LOGGED_IN}. * @param type The type of the special group. Must be either {@link SpecialGroup.EVERYONE} or {@link SpecialGroup.LOGGED_IN}.
*/ */

View file

@ -22,6 +22,7 @@ export interface PermissionEntryUserProps {
/** /**
* Permission entry for a user that can be set to read-only or writeable and can be removed. * Permission entry for a user that can be set to read-only or writeable and can be removed.
*
* @param entry The permission entry. * @param entry The permission entry.
*/ */
export const PermissionEntryUser: React.FC<PermissionEntryUserProps> = ({ entry }) => { export const PermissionEntryUser: React.FC<PermissionEntryUserProps> = ({ entry }) => {

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -14,6 +14,7 @@ import { PermissionSectionSpecialGroups } from './permission-section-special-gro
/** /**
* Modal for viewing and managing the permissions of the note. * Modal for viewing and managing the permissions of the note.
*
* @param show true to show the modal, false otherwise. * @param show true to show the modal, false otherwise.
* @param onHide Callback that is fired when the modal is about to be closed. * @param onHide Callback that is fired when the modal is about to be closed.
*/ */

View file

@ -14,6 +14,11 @@ export interface PermissionOwnerChangeProps {
onConfirmOwnerChange: (newOwner: string) => void onConfirmOwnerChange: (newOwner: string) => void
} }
/**
* Renders an input group to change the permission owner.
*
* @param onConfirmOwnerChange The callback to call if the owner was changed.
*/
export const PermissionOwnerChange: React.FC<PermissionOwnerChangeProps> = ({ onConfirmOwnerChange }) => { export const PermissionOwnerChange: React.FC<PermissionOwnerChangeProps> = ({ onConfirmOwnerChange }) => {
const { t } = useTranslation() const { t } = useTranslation()
const [ownerFieldValue, setOwnerFieldValue] = useState('') const [ownerFieldValue, setOwnerFieldValue] = useState('')

View file

@ -17,6 +17,7 @@ export interface PermissionOwnerInfoProps {
/** /**
* Content for the owner section of the permission modal that shows the current note owner. * Content for the owner section of the permission modal that shows the current note owner.
*
* @param onEditOwner Callback that is fired when the user chooses to change the note owner. * @param onEditOwner Callback that is fired when the user chooses to change the note owner.
*/ */
export const PermissionOwnerInfo: React.FC<PermissionOwnerInfoProps> = ({ onEditOwner }) => { export const PermissionOwnerInfo: React.FC<PermissionOwnerInfoProps> = ({ onEditOwner }) => {

View file

@ -19,6 +19,7 @@ export interface RevisionModalFooterProps {
/** /**
* Renders the footer of the revision modal that includes buttons to download the currently selected revision or to * Renders the footer of the revision modal that includes buttons to download the currently selected revision or to
* revert the note content back to that revision. * revert the note content back to that revision.
*
* @param selectedRevisionId The currently selected revision id or undefined if no revision was selected. * @param selectedRevisionId The currently selected revision id or undefined if no revision was selected.
* @param onHide Callback that is fired when the modal is about to be closed. * @param onHide Callback that is fired when the modal is about to be closed.
*/ */

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -13,6 +13,7 @@ const DISPLAY_MAX_USERS_PER_REVISION = 9
/** /**
* Downloads a given revision's content as markdown document in the browser. * Downloads a given revision's content as markdown document in the browser.
*
* @param noteId The id of the note from which to download the revision. * @param noteId The id of the note from which to download the revision.
* @param revision The revision details object containing the content to download. * @param revision The revision details object containing the content to download.
*/ */
@ -25,6 +26,7 @@ export const downloadRevision = (noteId: string, revision: RevisionDetails | nul
/** /**
* Fetches user details for the given usernames while returning a maximum of 9 users. * Fetches user details for the given usernames while returning a maximum of 9 users.
*
* @param usernames The list of usernames to fetch. * @param usernames The list of usernames to fetch.
* @throws {Error} in case the user-data request failed. * @throws {Error} in case the user-data request failed.
* @return An array of user details. * @return An array of user details.

View file

@ -15,6 +15,12 @@ import { useApplicationState } from '../../../../hooks/common/use-application-st
import { NoteType } from '../../../../redux/note-details/types/note-details' import { NoteType } from '../../../../redux/note-details/types/note-details'
import { useFrontendBaseUrl } from '../../../../hooks/common/use-frontend-base-url' import { useFrontendBaseUrl } from '../../../../hooks/common/use-frontend-base-url'
/**
* Renders a modal which provides shareable URLs of this note.
*
* @param show If the modal should be shown
* @param onHide The callback when the modal should be closed
*/
export const ShareModal: React.FC<ModalVisibilityProps> = ({ show, onHide }) => { export const ShareModal: React.FC<ModalVisibilityProps> = ({ show, onHide }) => {
useTranslation() useTranslation()
const noteFrontmatter = useApplicationState((state) => state.noteDetails.frontmatter) const noteFrontmatter = useApplicationState((state) => state.noteDetails.frontmatter)

View file

@ -37,6 +37,15 @@ import { useOnFirstEditorUpdateExtension } from './hooks/yjs/use-on-first-editor
import { useIsConnectionSynced } from './hooks/yjs/use-is-connection-synced' import { useIsConnectionSynced } from './hooks/yjs/use-is-connection-synced'
import { useMarkdownContentYText } from './hooks/yjs/use-markdown-content-y-text' import { useMarkdownContentYText } from './hooks/yjs/use-markdown-content-y-text'
/**
* Renders the text editor pane of the editor.
* The used editor is {@link ReactCodeMirror code mirror}.
*
* @param scrollState The current {@link ScrollState}
* @param onScroll The callback to update the {@link ScrollState}
* @param onMakeScrollSource The callback to request to become the scroll source.
* @external {ReactCodeMirror} https://npmjs.com/@uiw/react-codemirror
*/
export const EditorPane: React.FC<ScrollProps> = ({ scrollState, onScroll, onMakeScrollSource }) => { export const EditorPane: React.FC<ScrollProps> = ({ scrollState, onScroll, onMakeScrollSource }) => {
const ligaturesEnabled = useApplicationState((state) => state.editorConfig.ligatures) const ligaturesEnabled = useApplicationState((state) => state.editorConfig.ligatures)

View file

@ -9,7 +9,7 @@
* *
* @param markdownContent The markdown content whose content should be checked * @param markdownContent The markdown content whose content should be checked
* @param cursorPosition The cursor position that may or may not be in a code fence * @param cursorPosition The cursor position that may or may not be in a code fence
* @return {@code true} if the given cursor position is in a code fence * @return {@link true} if the given cursor position is in a code fence
*/ */
export const isCursorInCodeFence = (markdownContent: string, cursorPosition: number): boolean => { export const isCursorInCodeFence = (markdownContent: string, cursorPosition: number): boolean => {
const lines = markdownContent.slice(0, cursorPosition).split('\n') const lines = markdownContent.slice(0, cursorPosition).split('\n')

View file

@ -8,7 +8,9 @@ import { createNumberRangeArray } from '../../../../common/number-range/number-r
/** /**
* Checks if the given text is a tab-and-new-line-separated table. * Checks if the given text is a tab-and-new-line-separated table.
*
* @param text The text to check * @param text The text to check
* @return If the text is a table or not
*/ */
export const isTable = (text: string): boolean => { export const isTable = (text: string): boolean => {
// Tables must consist of multiple rows and columns // Tables must consist of multiple rows and columns
@ -32,7 +34,8 @@ export const isTable = (text: string): boolean => {
} }
/** /**
* Reformat the given text as Markdown table * Reformat the given text as Markdown table.
*
* @param pasteData The plain text table separated by tabs and new-lines * @param pasteData The plain text table separated by tabs and new-lines
* @return the formatted Markdown table * @return the formatted Markdown table
*/ */

View file

@ -34,7 +34,6 @@ const applyScrollState = (view: EditorView, scrollState: ScrollState): void => {
/** /**
* Monitors the given scroll state and scrolls the editor to the state if changed. * Monitors the given scroll state and scrolls the editor to the state if changed.
* *
* @param editorRef The editor that should be manipulated
* @param scrollState The scroll state that should be monitored * @param scrollState The scroll state that should be monitored
*/ */
export const useApplyScrollState = (scrollState?: ScrollState): void => { export const useApplyScrollState = (scrollState?: ScrollState): void => {

View file

@ -11,7 +11,7 @@ import { EditorView } from '@codemirror/view'
import type { Extension, SelectionRange } from '@codemirror/state' import type { Extension, SelectionRange } from '@codemirror/state'
/** /**
* Provides a callback for codemirror that handles cursor changes * Provides a callback for codemirror that handles cursor changes.
* *
* @return the generated callback * @return the generated callback
*/ */

View file

@ -19,7 +19,7 @@ import type { ContentFormatter } from '../../change-content-context/change-conte
import { useCodeMirrorReference } from '../../change-content-context/change-content-context' import { useCodeMirrorReference } from '../../change-content-context/change-content-context'
/** /**
* Processes the upload of the given file and inserts the correct Markdown code * Processes the upload of the given file and inserts the correct Markdown code.
* *
* @param view the codemirror instance that is used to insert the Markdown code * @param view the codemirror instance that is used to insert the Markdown code
* @param file The file to upload * @param file The file to upload

View file

@ -22,7 +22,7 @@ const calculateLineBasedPosition = (absolutePosition: number, lineStartIndexes:
} }
/** /**
* Returns the line+character based position of the to cursor, if available. * Returns the line+character based position of the to-cursor, if available.
*/ */
export const useLineBasedToPosition = (): LineBasedPosition | undefined => { export const useLineBasedToPosition = (): LineBasedPosition | undefined => {
const lineStartIndexes = useApplicationState((state) => state.noteDetails.markdownContent.lineStartIndexes) const lineStartIndexes = useApplicationState((state) => state.noteDetails.markdownContent.lineStartIndexes)
@ -38,7 +38,7 @@ export const useLineBasedToPosition = (): LineBasedPosition | undefined => {
} }
/** /**
* Returns the line+character based position of the from cursor. * Returns the line+character based position of the from-cursor.
*/ */
export const useLineBasedFromPosition = (): LineBasedPosition => { export const useLineBasedFromPosition = (): LineBasedPosition => {
const lineStartIndexes = useApplicationState((state) => state.noteDetails.markdownContent.lineStartIndexes) const lineStartIndexes = useApplicationState((state) => state.noteDetails.markdownContent.lineStartIndexes)

View file

@ -10,7 +10,7 @@ import type { YText } from 'yjs/dist/src/types/YText'
/** /**
* One-Way-synchronizes the text of the given {@link YText y-text} into the global application state. * One-Way-synchronizes the text of the given {@link YText y-text} into the global application state.
*4 *
* @param yText The source text * @param yText The source text
*/ */
export const useBindYTextToRedux = (yText: YText): void => { export const useBindYTextToRedux = (yText: YText): void => {

View file

@ -11,6 +11,7 @@ import type { YDocMessageTransporter } from '@hedgedoc/realtime'
* Checks if the given message transporter has received at least one full synchronisation. * Checks if the given message transporter has received at least one full synchronisation.
* *
* @param connection The connection whose sync status should be checked * @param connection The connection whose sync status should be checked
* @return If at least one full synchronisation is occurred.
*/ */
export const useIsConnectionSynced = (connection: YDocMessageTransporter): boolean => { export const useIsConnectionSynced = (connection: YDocMessageTransporter): boolean => {
const [editorEnabled, setEditorEnabled] = useState<boolean>(false) const [editorEnabled, setEditorEnabled] = useState<boolean>(false)

View file

@ -11,7 +11,7 @@ import type { Extension } from '@codemirror/state'
/** /**
* Provides an extension that checks when the code mirror, that loads the extension, has its first update. * Provides an extension that checks when the code mirror, that loads the extension, has its first update.
* *
* @return [Extension, boolean] The extension that listens for editor updates and a boolean that defines if the first update already happened * @return The extension that listens for editor updates and a boolean that defines if the first update already happened
*/ */
export const useOnFirstEditorUpdateExtension = (): [Extension, boolean] => { export const useOnFirstEditorUpdateExtension = (): [Extension, boolean] => {
const [firstUpdateHappened, setFirstUpdateHappened] = useState<boolean>(false) const [firstUpdateHappened, setFirstUpdateHappened] = useState<boolean>(false)

View file

@ -14,7 +14,7 @@ import type { Doc } from 'yjs'
import type { Awareness } from 'y-protocols/awareness' import type { Awareness } from 'y-protocols/awareness'
/** /**
* Handles the communication with the realtime endpoint of the backend and synchronizes the given y-doc and awareness with other clients.. * Handles the communication with the realtime endpoint of the backend and synchronizes the given y-doc and awareness with other clients.
*/ */
export class WebsocketConnection extends WebsocketTransporter { export class WebsocketConnection extends WebsocketTransporter {
constructor(url: URL, doc: Doc, awareness: Awareness) { constructor(url: URL, doc: Doc, awareness: Awareness) {

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