mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-25 03:06:31 -05:00
fix(frontend): improve performance by caching translated texts
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
ced4cd953c
commit
76242330fd
81 changed files with 341 additions and 292 deletions
|
@ -9,8 +9,7 @@ import { createNumberRangeArray } from '../../common/number-range/number-range'
|
||||||
import styles from './animations.module.scss'
|
import styles from './animations.module.scss'
|
||||||
import { IconRow } from './icon-row'
|
import { IconRow } from './icon-row'
|
||||||
import React, { useMemo } from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import { Pencil as IconPencil } from 'react-bootstrap-icons'
|
import { Pencil as IconPencil, PencilFill as IconPencilFill } from 'react-bootstrap-icons'
|
||||||
import { PencilFill as IconPencilFill } from 'react-bootstrap-icons'
|
|
||||||
|
|
||||||
export interface HedgeDocLogoProps {
|
export interface HedgeDocLogoProps {
|
||||||
error: boolean
|
error: boolean
|
||||||
|
|
|
@ -6,18 +6,20 @@
|
||||||
import styles from './animations.module.scss'
|
import styles from './animations.module.scss'
|
||||||
import React, { Fragment, useEffect, useState } from 'react'
|
import React, { Fragment, useEffect, useState } from 'react'
|
||||||
import type { Icon } from 'react-bootstrap-icons'
|
import type { Icon } from 'react-bootstrap-icons'
|
||||||
import { FileText as IconFileText } from 'react-bootstrap-icons'
|
import {
|
||||||
import { File as IconFile } from 'react-bootstrap-icons'
|
File as IconFile,
|
||||||
import { Fonts as IconFonts } from 'react-bootstrap-icons'
|
FileText as IconFileText,
|
||||||
import { Gear as IconGear } from 'react-bootstrap-icons'
|
Fonts as IconFonts,
|
||||||
import { KeyboardFill as IconKeyboardFill } from 'react-bootstrap-icons'
|
Gear as IconGear,
|
||||||
import { ListCheck as IconListCheck } from 'react-bootstrap-icons'
|
KeyboardFill as IconKeyboardFill,
|
||||||
import { Markdown as IconMarkdown } from 'react-bootstrap-icons'
|
ListCheck as IconListCheck,
|
||||||
import { Pencil as IconPencil } from 'react-bootstrap-icons'
|
Markdown as IconMarkdown,
|
||||||
import { Person as IconPerson } from 'react-bootstrap-icons'
|
Pencil as IconPencil,
|
||||||
import { Tag as IconTag } from 'react-bootstrap-icons'
|
Person as IconPerson,
|
||||||
import { TypeBold as IconTypeBold } from 'react-bootstrap-icons'
|
Tag as IconTag,
|
||||||
import { TypeItalic as IconTypeItalic } from 'react-bootstrap-icons'
|
TypeBold as IconTypeBold,
|
||||||
|
TypeItalic as IconTypeItalic
|
||||||
|
} from 'react-bootstrap-icons'
|
||||||
|
|
||||||
const elements: Icon[] = [
|
const elements: Icon[] = [
|
||||||
IconFileText,
|
IconFileText,
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
import { useTranslatedText } from '../../../../hooks/common/use-translated-text'
|
||||||
import type { PropsWithDataCypressId } from '../../../../utils/cypress-attribute'
|
import type { PropsWithDataCypressId } from '../../../../utils/cypress-attribute'
|
||||||
import { cypressId } from '../../../../utils/cypress-attribute'
|
import { cypressId } from '../../../../utils/cypress-attribute'
|
||||||
import { UiIcon } from '../../icons/ui-icon'
|
import { UiIcon } from '../../icons/ui-icon'
|
||||||
|
@ -11,7 +12,6 @@ import React, { Fragment, useRef } from 'react'
|
||||||
import { Button } from 'react-bootstrap'
|
import { Button } from 'react-bootstrap'
|
||||||
import { Files as IconFiles } from 'react-bootstrap-icons'
|
import { Files as IconFiles } from 'react-bootstrap-icons'
|
||||||
import type { Variant } from 'react-bootstrap/types'
|
import type { Variant } from 'react-bootstrap/types'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
export interface CopyToClipboardButtonProps extends PropsWithDataCypressId {
|
export interface CopyToClipboardButtonProps extends PropsWithDataCypressId {
|
||||||
content: string
|
content: string
|
||||||
|
@ -33,10 +33,9 @@ export const CopyToClipboardButton: React.FC<CopyToClipboardButtonProps> = ({
|
||||||
variant = 'dark',
|
variant = 'dark',
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
|
||||||
const button = useRef<HTMLButtonElement>(null)
|
const button = useRef<HTMLButtonElement>(null)
|
||||||
|
|
||||||
const [copyToClipboard, overlayElement] = useCopyOverlay(button, content)
|
const [copyToClipboard, overlayElement] = useCopyOverlay(button, content)
|
||||||
|
const buttonTitle = useTranslatedText('renderer.highlightCode.copyCode')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
@ -44,7 +43,7 @@ export const CopyToClipboardButton: React.FC<CopyToClipboardButtonProps> = ({
|
||||||
ref={button}
|
ref={button}
|
||||||
size={size}
|
size={size}
|
||||||
variant={variant}
|
variant={variant}
|
||||||
title={t('renderer.highlightCode.copyCode') ?? undefined}
|
title={buttonTitle}
|
||||||
onClick={copyToClipboard}
|
onClick={copyToClipboard}
|
||||||
{...cypressId(props)}>
|
{...cypressId(props)}>
|
||||||
<UiIcon icon={IconFiles} />
|
<UiIcon icon={IconFiles} />
|
||||||
|
|
|
@ -3,10 +3,11 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
|
||||||
import type { CommonFieldProps } from './fields'
|
import type { CommonFieldProps } from './fields'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Form } from 'react-bootstrap'
|
import { Form } from 'react-bootstrap'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans } 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.
|
||||||
|
@ -14,7 +15,7 @@ import { Trans, useTranslation } from 'react-i18next'
|
||||||
* @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 }) => {
|
||||||
const { t } = useTranslation()
|
const placeholderText = useTranslatedText('login.auth.password')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
|
@ -25,7 +26,7 @@ export const CurrentPasswordField: React.FC<CommonFieldProps> = ({ onChange }) =
|
||||||
type='password'
|
type='password'
|
||||||
size='sm'
|
size='sm'
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
placeholder={t('login.auth.password') ?? undefined}
|
placeholder={placeholderText}
|
||||||
autoComplete='current-password'
|
autoComplete='current-password'
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -3,10 +3,11 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
|
||||||
import type { CommonFieldProps } from './fields'
|
import type { CommonFieldProps } from './fields'
|
||||||
import React, { useMemo } from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import { Form } from 'react-bootstrap'
|
import { Form } from 'react-bootstrap'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans } from 'react-i18next'
|
||||||
|
|
||||||
interface DisplayNameFieldProps extends CommonFieldProps {
|
interface DisplayNameFieldProps extends CommonFieldProps {
|
||||||
initialValue?: string
|
initialValue?: string
|
||||||
|
@ -20,11 +21,8 @@ interface DisplayNameFieldProps extends CommonFieldProps {
|
||||||
* @param initialValue The initial input field value.
|
* @param initialValue The initial input field value.
|
||||||
*/
|
*/
|
||||||
export const DisplayNameField: React.FC<DisplayNameFieldProps> = ({ onChange, value, initialValue }) => {
|
export const DisplayNameField: React.FC<DisplayNameFieldProps> = ({ onChange, value, initialValue }) => {
|
||||||
const { t } = useTranslation()
|
const isValid = useMemo(() => value.trim() !== '' && value !== initialValue, [value, initialValue])
|
||||||
|
const placeholderText = useTranslatedText('profile.displayName')
|
||||||
const isValid = useMemo(() => {
|
|
||||||
return value.trim() !== '' && value !== initialValue
|
|
||||||
}, [value, initialValue])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
|
@ -37,7 +35,7 @@ export const DisplayNameField: React.FC<DisplayNameFieldProps> = ({ onChange, va
|
||||||
value={value}
|
value={value}
|
||||||
isValid={isValid}
|
isValid={isValid}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
placeholder={t('profile.displayName') ?? undefined}
|
placeholder={placeholderText}
|
||||||
autoComplete='name'
|
autoComplete='name'
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -3,10 +3,11 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
|
||||||
import type { CommonFieldProps } from './fields'
|
import type { CommonFieldProps } from './fields'
|
||||||
import React, { useMemo } from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import { Form } from 'react-bootstrap'
|
import { Form } from 'react-bootstrap'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans } from 'react-i18next'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders an input field for the new password when registering.
|
* Renders an input field for the new password when registering.
|
||||||
|
@ -15,11 +16,9 @@ import { Trans, useTranslation } from 'react-i18next'
|
||||||
* @param value The currently entered password.
|
* @param value The currently entered password.
|
||||||
*/
|
*/
|
||||||
export const NewPasswordField: React.FC<CommonFieldProps> = ({ onChange, value, hasError = false }) => {
|
export const NewPasswordField: React.FC<CommonFieldProps> = ({ onChange, value, hasError = false }) => {
|
||||||
const { t } = useTranslation()
|
const isValid = useMemo(() => value.trim() !== '', [value])
|
||||||
|
|
||||||
const isValid = useMemo(() => {
|
const placeholderText = useTranslatedText('login.auth.password')
|
||||||
return value.trim() !== ''
|
|
||||||
}, [value])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
|
@ -32,7 +31,7 @@ export const NewPasswordField: React.FC<CommonFieldProps> = ({ onChange, value,
|
||||||
isValid={isValid}
|
isValid={isValid}
|
||||||
isInvalid={hasError}
|
isInvalid={hasError}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
placeholder={t('login.auth.password') ?? undefined}
|
placeholder={placeholderText}
|
||||||
autoComplete='new-password'
|
autoComplete='new-password'
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -3,10 +3,11 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
|
||||||
import type { CommonFieldProps } from './fields'
|
import type { CommonFieldProps } from './fields'
|
||||||
import React, { useMemo } from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import { Form } from 'react-bootstrap'
|
import { Form } from 'react-bootstrap'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans } from 'react-i18next'
|
||||||
|
|
||||||
interface PasswordAgainFieldProps extends CommonFieldProps {
|
interface PasswordAgainFieldProps extends CommonFieldProps {
|
||||||
password: string
|
password: string
|
||||||
|
@ -15,9 +16,10 @@ 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
|
||||||
|
* @param hasError Defines if the password should be shown as invalid
|
||||||
*/
|
*/
|
||||||
export const PasswordAgainField: React.FC<PasswordAgainFieldProps> = ({
|
export const PasswordAgainField: React.FC<PasswordAgainFieldProps> = ({
|
||||||
onChange,
|
onChange,
|
||||||
|
@ -25,15 +27,9 @@ export const PasswordAgainField: React.FC<PasswordAgainFieldProps> = ({
|
||||||
password,
|
password,
|
||||||
hasError = false
|
hasError = false
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const isInvalid = useMemo(() => value !== '' && password !== value && hasError, [password, value, hasError])
|
||||||
|
const isValid = useMemo(() => password !== '' && password === value && !hasError, [password, value, hasError])
|
||||||
const isInvalid = useMemo(() => {
|
const placeholderText = useTranslatedText('login.register.passwordAgain')
|
||||||
return value !== '' && password !== value && hasError
|
|
||||||
}, [password, value, hasError])
|
|
||||||
|
|
||||||
const isValid = useMemo(() => {
|
|
||||||
return password !== '' && password === value && !hasError
|
|
||||||
}, [password, value, hasError])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
|
@ -46,7 +42,7 @@ export const PasswordAgainField: React.FC<PasswordAgainFieldProps> = ({
|
||||||
isInvalid={isInvalid}
|
isInvalid={isInvalid}
|
||||||
isValid={isValid}
|
isValid={isValid}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
placeholder={t('login.register.passwordAgain') ?? undefined}
|
placeholder={placeholderText}
|
||||||
autoComplete='new-password'
|
autoComplete='new-password'
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -3,10 +3,10 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
|
||||||
import type { CommonFieldProps } from './fields'
|
import type { CommonFieldProps } from './fields'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Form } from 'react-bootstrap'
|
import { Form } from 'react-bootstrap'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
export interface UsernameFieldProps extends CommonFieldProps {
|
export interface UsernameFieldProps extends CommonFieldProps {
|
||||||
isInvalid?: boolean
|
isInvalid?: boolean
|
||||||
|
@ -22,7 +22,7 @@ export interface UsernameFieldProps extends CommonFieldProps {
|
||||||
* @param isInvalid Adds error style to label
|
* @param isInvalid Adds error style to label
|
||||||
*/
|
*/
|
||||||
export const UsernameField: React.FC<UsernameFieldProps> = ({ onChange, value, isValid, isInvalid }) => {
|
export const UsernameField: React.FC<UsernameFieldProps> = ({ onChange, value, isValid, isInvalid }) => {
|
||||||
const { t } = useTranslation()
|
const placeholderText = useTranslatedText('login.auth.username')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Control
|
<Form.Control
|
||||||
|
@ -32,7 +32,7 @@ export const UsernameField: React.FC<UsernameFieldProps> = ({ onChange, value, i
|
||||||
isValid={isValid}
|
isValid={isValid}
|
||||||
isInvalid={isInvalid}
|
isInvalid={isInvalid}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
placeholder={t('login.auth.username') ?? undefined}
|
placeholder={placeholderText}
|
||||||
autoComplete='username'
|
autoComplete='username'
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
required
|
required
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { UsernameField } from './username-field'
|
|
||||||
import type { UsernameFieldProps } from './username-field'
|
import type { UsernameFieldProps } from './username-field'
|
||||||
|
import { UsernameField } from './username-field'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Form } from 'react-bootstrap'
|
import { Form } from 'react-bootstrap'
|
||||||
import { Trans } from 'react-i18next'
|
import { Trans } from 'react-i18next'
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { testId } from '../../../utils/test-id'
|
import { testId } from '../../../utils/test-id'
|
||||||
import { BootstrapLazyIcons } from './bootstrap-icons'
|
|
||||||
import type { BootstrapIconName } from './bootstrap-icons'
|
import type { BootstrapIconName } from './bootstrap-icons'
|
||||||
|
import { BootstrapLazyIcons } from './bootstrap-icons'
|
||||||
import React, { Suspense, useMemo } from 'react'
|
import React, { Suspense, useMemo } from 'react'
|
||||||
|
|
||||||
export interface LazyBootstrapIconProps {
|
export interface LazyBootstrapIconProps {
|
||||||
|
|
|
@ -3,10 +3,10 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
|
||||||
import { ExternalLink } from './external-link'
|
import { ExternalLink } from './external-link'
|
||||||
import type { TranslatedLinkProps } from './types'
|
import type { TranslatedLinkProps } from './types'
|
||||||
import React, { useMemo } from 'react'
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An {@link ExternalLink external link} with translated text.
|
* An {@link ExternalLink external link} with translated text.
|
||||||
|
@ -16,7 +16,6 @@ import { useTranslation } from 'react-i18next'
|
||||||
* @param props Additional props directly given to the {@link ExternalLink}
|
* @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 text = useTranslatedText(i18nKey, i18nOption)
|
||||||
const text = useMemo(() => (i18nOption ? t(i18nKey, i18nOption) : t(i18nKey)), [i18nKey, i18nOption, t])
|
|
||||||
return <ExternalLink text={text} {...props} />
|
return <ExternalLink text={text} {...props} />
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,10 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
|
||||||
import { InternalLink } from './internal-link'
|
import { InternalLink } from './internal-link'
|
||||||
import type { TranslatedLinkProps } from './types'
|
import type { TranslatedLinkProps } from './types'
|
||||||
import React, { useMemo } from 'react'
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An {@link InternalLink internal link} with translated text.
|
* An {@link InternalLink internal link} with translated text.
|
||||||
|
@ -16,7 +16,7 @@ import { useTranslation } from 'react-i18next'
|
||||||
* @param props Additional props directly given to the {@link InternalLink}
|
* @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 text = useTranslatedText(i18nKey, i18nOption)
|
||||||
const text = useMemo(() => (i18nOption ? t(i18nKey, i18nOption) : t(i18nKey)), [i18nKey, i18nOption, t])
|
|
||||||
return <InternalLink text={text} {...props} />
|
return <InternalLink text={text} {...props} />
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { RendererType } from '../../render-page/window-post-message-communicator
|
||||||
import { CommonModal } from '../modals/common-modal'
|
import { CommonModal } from '../modals/common-modal'
|
||||||
import { RendererIframe } from '../renderer-iframe/renderer-iframe'
|
import { RendererIframe } from '../renderer-iframe/renderer-iframe'
|
||||||
import { fetchMotd, MOTD_LOCAL_STORAGE_KEY } from './fetch-motd'
|
import { fetchMotd, MOTD_LOCAL_STORAGE_KEY } from './fetch-motd'
|
||||||
import React, { useCallback, useMemo, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { Button, Modal } from 'react-bootstrap'
|
import { Button, Modal } from 'react-bootstrap'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { useAsync } from 'react-use'
|
import { useAsync } from 'react-use'
|
||||||
|
|
|
@ -10,9 +10,11 @@ import { UiIcon } from '../icons/ui-icon'
|
||||||
import { ShowIf } from '../show-if/show-if'
|
import { ShowIf } from '../show-if/show-if'
|
||||||
import React, { useCallback, useEffect } from 'react'
|
import React, { useCallback, useEffect } from 'react'
|
||||||
import { Alert, Button } from 'react-bootstrap'
|
import { Alert, Button } from 'react-bootstrap'
|
||||||
import { ArrowRepeat as IconArrowRepeat } from 'react-bootstrap-icons'
|
import {
|
||||||
import { CheckCircle as IconCheckCircle } from 'react-bootstrap-icons'
|
ArrowRepeat as IconArrowRepeat,
|
||||||
import { ExclamationTriangle as IconExclamationTriangle } from 'react-bootstrap-icons'
|
CheckCircle as IconCheckCircle,
|
||||||
|
ExclamationTriangle as IconExclamationTriangle
|
||||||
|
} from 'react-bootstrap-icons'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { useAsyncFn } from 'react-use'
|
import { useAsyncFn } from 'react-use'
|
||||||
|
|
||||||
|
|
|
@ -15,9 +15,9 @@ import { useEditorReceiveHandler } from '../../render-page/window-post-message-c
|
||||||
import type {
|
import type {
|
||||||
ExtensionEvent,
|
ExtensionEvent,
|
||||||
OnHeightChangeMessage,
|
OnHeightChangeMessage,
|
||||||
|
RendererType,
|
||||||
SetScrollStateMessage
|
SetScrollStateMessage
|
||||||
} from '../../render-page/window-post-message-communicator/rendering-message'
|
} from '../../render-page/window-post-message-communicator/rendering-message'
|
||||||
import type { RendererType } from '../../render-page/window-post-message-communicator/rendering-message'
|
|
||||||
import { CommunicationMessageType } from '../../render-page/window-post-message-communicator/rendering-message'
|
import { CommunicationMessageType } from '../../render-page/window-post-message-communicator/rendering-message'
|
||||||
import { ShowIf } from '../show-if/show-if'
|
import { ShowIf } from '../show-if/show-if'
|
||||||
import { WaitSpinner } from '../wait-spinner/wait-spinner'
|
import { WaitSpinner } from '../wait-spinner/wait-spinner'
|
||||||
|
|
|
@ -3,13 +3,13 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
|
||||||
import { ShowIf } from '../show-if/show-if'
|
import { ShowIf } from '../show-if/show-if'
|
||||||
import defaultAvatar from './default-avatar.png'
|
import defaultAvatar from './default-avatar.png'
|
||||||
import styles from './user-avatar.module.scss'
|
import styles from './user-avatar.module.scss'
|
||||||
import React, { useCallback, useMemo } from 'react'
|
import React, { useCallback, useMemo } from 'react'
|
||||||
import { OverlayTrigger, Tooltip } from 'react-bootstrap'
|
import { OverlayTrigger, Tooltip } from 'react-bootstrap'
|
||||||
import type { OverlayInjectedProps } from 'react-bootstrap/Overlay'
|
import type { OverlayInjectedProps } from 'react-bootstrap/Overlay'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
export interface UserAvatarProps {
|
export interface UserAvatarProps {
|
||||||
size?: 'sm' | 'lg'
|
size?: 'sm' | 'lg'
|
||||||
|
@ -34,8 +34,6 @@ export const UserAvatar: React.FC<UserAvatarProps> = ({
|
||||||
additionalClasses = '',
|
additionalClasses = '',
|
||||||
showName = true
|
showName = true
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const imageSize = useMemo(() => {
|
const imageSize = useMemo(() => {
|
||||||
switch (size) {
|
switch (size) {
|
||||||
case 'sm':
|
case 'sm':
|
||||||
|
@ -51,7 +49,13 @@ export const UserAvatar: React.FC<UserAvatarProps> = ({
|
||||||
return photoUrl || defaultAvatar.src
|
return photoUrl || defaultAvatar.src
|
||||||
}, [photoUrl])
|
}, [photoUrl])
|
||||||
|
|
||||||
const imgDescription = useMemo(() => t('common.avatarOf', { name: displayName }), [t, displayName])
|
const imageTranslateOptions = useMemo(
|
||||||
|
() => ({
|
||||||
|
name: displayName
|
||||||
|
}),
|
||||||
|
[displayName]
|
||||||
|
)
|
||||||
|
const imgDescription = useTranslatedText('common.avatarOf', imageTranslateOptions)
|
||||||
|
|
||||||
const tooltip = useCallback(
|
const tooltip = useCallback(
|
||||||
(overlayInjectedProps: OverlayInjectedProps) => (
|
(overlayInjectedProps: OverlayInjectedProps) => (
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { useApplicationState } from '../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../hooks/common/use-application-state'
|
||||||
|
import { useTranslatedText } from '../../hooks/common/use-translated-text'
|
||||||
import { InternalLink } from '../common/links/internal-link'
|
import { InternalLink } from '../common/links/internal-link'
|
||||||
import { ShowIf } from '../common/show-if/show-if'
|
import { ShowIf } from '../common/show-if/show-if'
|
||||||
import { NoteInfoLineCreatedAt } from '../editor-page/sidebar/specific-sidebar-entries/note-info-sidebar-menu/note-info-line/note-info-line-created-at'
|
import { NoteInfoLineCreatedAt } from '../editor-page/sidebar/specific-sidebar-entries/note-info-sidebar-menu/note-info-line/note-info-line-created-at'
|
||||||
|
@ -11,16 +12,16 @@ import { NoteInfoLineUpdatedBy } from '../editor-page/sidebar/specific-sidebar-e
|
||||||
import styles from './document-infobar.module.scss'
|
import styles from './document-infobar.module.scss'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Pencil as IconPencil } from 'react-bootstrap-icons'
|
import { Pencil as IconPencil } from 'react-bootstrap-icons'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans } from 'react-i18next'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders an info bar 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 noteDetails = useApplicationState((state) => state.noteDetails)
|
const noteDetails = useApplicationState((state) => state.noteDetails)
|
||||||
|
|
||||||
// TODO Check permissions ("writability") of note and show edit link depending on that.
|
// TODO Check permissions ("writability") of note and show edit link depending on that.
|
||||||
|
const linkTitle = useTranslatedText('views.readOnly.editNote')
|
||||||
return (
|
return (
|
||||||
<div className={`d-flex flex-row my-3 ${styles['document-infobar']}`}>
|
<div className={`d-flex flex-row my-3 ${styles['document-infobar']}`}>
|
||||||
<div className={'col-md'}> </div>
|
<div className={'col-md'}> </div>
|
||||||
|
@ -38,7 +39,7 @@ export const DocumentInfobar: React.FC = () => {
|
||||||
href={`/n/${noteDetails.primaryAddress}`}
|
href={`/n/${noteDetails.primaryAddress}`}
|
||||||
icon={IconPencil}
|
icon={IconPencil}
|
||||||
className={'text-primary text-decoration-none mx-1'}
|
className={'text-primary text-decoration-none mx-1'}
|
||||||
title={t('views.readOnly.editNote') ?? undefined}
|
title={linkTitle}
|
||||||
/>
|
/>
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
|
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
|
||||||
|
import { useTranslatedText } from '../../../../hooks/common/use-translated-text'
|
||||||
import { useOutlineButtonVariant } from '../../../../hooks/dark-mode/use-outline-button-variant'
|
import { useOutlineButtonVariant } from '../../../../hooks/dark-mode/use-outline-button-variant'
|
||||||
import { cypressId } from '../../../../utils/cypress-attribute'
|
import { cypressId } from '../../../../utils/cypress-attribute'
|
||||||
import { CommonModal } from '../../../common/modals/common-modal'
|
import { CommonModal } from '../../../common/modals/common-modal'
|
||||||
|
@ -11,21 +12,21 @@ import { CheatsheetContent } from './cheatsheet-content'
|
||||||
import { CheatsheetInNewTabButton } from './cheatsheet-in-new-tab-button'
|
import { CheatsheetInNewTabButton } from './cheatsheet-in-new-tab-button'
|
||||||
import React, { Fragment } from 'react'
|
import React, { Fragment } from 'react'
|
||||||
import { Button, Modal } from 'react-bootstrap'
|
import { Button, Modal } from 'react-bootstrap'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans } from 'react-i18next'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows a button that opens the cheatsheet dialog.
|
* Shows a button that opens the cheatsheet dialog.
|
||||||
*/
|
*/
|
||||||
export const CheatsheetButton: React.FC = () => {
|
export const CheatsheetButton: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
|
||||||
const [modalVisibility, showModal, closeModal] = useBooleanState()
|
const [modalVisibility, showModal, closeModal] = useBooleanState()
|
||||||
const buttonVariant = useOutlineButtonVariant()
|
const buttonVariant = useOutlineButtonVariant()
|
||||||
|
const buttonTitle = useTranslatedText('cheatsheet.button')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Button
|
<Button
|
||||||
{...cypressId('open.cheatsheet-button')}
|
{...cypressId('open.cheatsheet-button')}
|
||||||
title={t('cheatsheet.button') ?? undefined}
|
title={buttonTitle}
|
||||||
className={'mx-2'}
|
className={'mx-2'}
|
||||||
variant={buttonVariant}
|
variant={buttonVariant}
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
|
|
|
@ -3,12 +3,12 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
import { useTranslatedText } from '../../../../hooks/common/use-translated-text'
|
||||||
import { useOutlineButtonVariant } from '../../../../hooks/dark-mode/use-outline-button-variant'
|
import { useOutlineButtonVariant } from '../../../../hooks/dark-mode/use-outline-button-variant'
|
||||||
import { IconButton } from '../../../common/icon-button/icon-button'
|
import { IconButton } from '../../../common/icon-button/icon-button'
|
||||||
import type { MouseEvent } from 'react'
|
import type { MouseEvent } from 'react'
|
||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react'
|
||||||
import { BoxArrowUpRight } from 'react-bootstrap-icons'
|
import { BoxArrowUpRight } from 'react-bootstrap-icons'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
export interface CheatsheetInNewTabButtonProps {
|
export interface CheatsheetInNewTabButtonProps {
|
||||||
onClick?: () => void
|
onClick?: () => void
|
||||||
|
@ -26,10 +26,8 @@ export const CheatsheetInNewTabButton: React.FC<CheatsheetInNewTabButtonProps> =
|
||||||
},
|
},
|
||||||
[onClick]
|
[onClick]
|
||||||
)
|
)
|
||||||
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const buttonVariant = useOutlineButtonVariant()
|
const buttonVariant = useOutlineButtonVariant()
|
||||||
|
const buttonTitle = useTranslatedText('cheatsheet.modal.popup')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
|
@ -41,7 +39,7 @@ export const CheatsheetInNewTabButton: React.FC<CheatsheetInNewTabButtonProps> =
|
||||||
className={'p-2 border-0'}
|
className={'p-2 border-0'}
|
||||||
variant={buttonVariant}
|
variant={buttonVariant}
|
||||||
target={'_blank'}
|
target={'_blank'}
|
||||||
title={t('cheatsheet.modal.popup') ?? ''}
|
title={buttonTitle}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,28 +4,29 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
|
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
|
||||||
|
import { useTranslatedText } from '../../../../hooks/common/use-translated-text'
|
||||||
import { useOutlineButtonVariant } from '../../../../hooks/dark-mode/use-outline-button-variant'
|
import { useOutlineButtonVariant } from '../../../../hooks/dark-mode/use-outline-button-variant'
|
||||||
import { cypressId } from '../../../../utils/cypress-attribute'
|
import { cypressId } from '../../../../utils/cypress-attribute'
|
||||||
import { IconButton } from '../../../common/icon-button/icon-button'
|
import { IconButton } from '../../../common/icon-button/icon-button'
|
||||||
import { HelpModal } from './help-modal'
|
import { HelpModal } from './help-modal'
|
||||||
import React, { Fragment } from 'react'
|
import React, { Fragment } from 'react'
|
||||||
import { QuestionCircle as IconQuestionCircle } from 'react-bootstrap-icons'
|
import { QuestionCircle as IconQuestionCircle } from 'react-bootstrap-icons'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans } from 'react-i18next'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the button to open the {@link HelpModal}.
|
* Renders the button to open the {@link HelpModal}.
|
||||||
*/
|
*/
|
||||||
export const HelpButton: React.FC = () => {
|
export const HelpButton: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
|
||||||
const [modalVisibility, showModal, closeModal] = useBooleanState()
|
const [modalVisibility, showModal, closeModal] = useBooleanState()
|
||||||
const buttonVariant = useOutlineButtonVariant()
|
const buttonVariant = useOutlineButtonVariant()
|
||||||
|
const buttonTitle = useTranslatedText('editor.documentBar.help')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={IconQuestionCircle}
|
icon={IconQuestionCircle}
|
||||||
{...cypressId('editor-help-button')}
|
{...cypressId('editor-help-button')}
|
||||||
title={t('editor.documentBar.help') ?? undefined}
|
title={buttonTitle}
|
||||||
className='ms-2'
|
className='ms-2'
|
||||||
size='sm'
|
size='sm'
|
||||||
variant={buttonVariant}
|
variant={buttonVariant}
|
||||||
|
|
|
@ -9,10 +9,7 @@ import { TranslatedExternalLink } from '../../../common/links/translated-externa
|
||||||
import { TranslatedInternalLink } from '../../../common/links/translated-internal-link'
|
import { TranslatedInternalLink } from '../../../common/links/translated-internal-link'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Col, Row } from 'react-bootstrap'
|
import { Col, Row } from 'react-bootstrap'
|
||||||
import { Dot as IconDot } from 'react-bootstrap-icons'
|
import { Dot as IconDot, Flag as IconFlag, PeopleFill as IconPeopleFill, Tag as IconTag } from 'react-bootstrap-icons'
|
||||||
import { Flag as IconFlag } from 'react-bootstrap-icons'
|
|
||||||
import { PeopleFill as IconPeopleFill } from 'react-bootstrap-icons'
|
|
||||||
import { Tag as IconTag } from 'react-bootstrap-icons'
|
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -4,25 +4,25 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
||||||
|
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
|
||||||
import { useOutlineButtonVariant } from '../../../hooks/dark-mode/use-outline-button-variant'
|
import { useOutlineButtonVariant } from '../../../hooks/dark-mode/use-outline-button-variant'
|
||||||
import { UiIcon } from '../../common/icons/ui-icon'
|
import { UiIcon } from '../../common/icons/ui-icon'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Button } from 'react-bootstrap'
|
import { Button } from 'react-bootstrap'
|
||||||
import { FileEarmarkTextFill as IconFileEarmarkTextFill } from 'react-bootstrap-icons'
|
import { FileEarmarkTextFill as IconFileEarmarkTextFill } from 'react-bootstrap-icons'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Button that links to the read-only version of a note.
|
* Button that links to the read-only version of a note.
|
||||||
*/
|
*/
|
||||||
export const ReadOnlyModeButton: React.FC = () => {
|
export const ReadOnlyModeButton: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
|
||||||
const noteIdentifier = useApplicationState((state) => state.noteDetails.primaryAddress)
|
const noteIdentifier = useApplicationState((state) => state.noteDetails.primaryAddress)
|
||||||
const buttonVariant = useOutlineButtonVariant()
|
const buttonVariant = useOutlineButtonVariant()
|
||||||
|
const buttonTitle = useTranslatedText('editor.documentBar.readOnlyMode')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link href={`/s/${noteIdentifier}`} target='_blank'>
|
<Link href={`/s/${noteIdentifier}`} target='_blank'>
|
||||||
<Button title={t('editor.documentBar.readOnlyMode') ?? undefined} size='sm' variant={buttonVariant}>
|
<Button title={buttonTitle} size='sm' variant={buttonVariant}>
|
||||||
<UiIcon icon={IconFileEarmarkTextFill} />
|
<UiIcon icon={IconFileEarmarkTextFill} />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -4,25 +4,25 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
||||||
|
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
|
||||||
import { useOutlineButtonVariant } from '../../../hooks/dark-mode/use-outline-button-variant'
|
import { useOutlineButtonVariant } from '../../../hooks/dark-mode/use-outline-button-variant'
|
||||||
import { UiIcon } from '../../common/icons/ui-icon'
|
import { UiIcon } from '../../common/icons/ui-icon'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Button } from 'react-bootstrap'
|
import { Button } from 'react-bootstrap'
|
||||||
import { Tv as IconTv } from 'react-bootstrap-icons'
|
import { Tv as IconTv } from 'react-bootstrap-icons'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Button that links to the slide-show presentation of the current note.
|
* Button that links to the slide-show presentation of the current note.
|
||||||
*/
|
*/
|
||||||
export const SlideModeButton: React.FC = () => {
|
export const SlideModeButton: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
|
||||||
const noteIdentifier = useApplicationState((state) => state.noteDetails.primaryAddress)
|
const noteIdentifier = useApplicationState((state) => state.noteDetails.primaryAddress)
|
||||||
const buttonVariant = useOutlineButtonVariant()
|
const buttonVariant = useOutlineButtonVariant()
|
||||||
|
const buttonTitle = useTranslatedText('editor.documentBar.slideMode')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link href={`/p/${noteIdentifier}`} target='_blank'>
|
<Link href={`/p/${noteIdentifier}`} target='_blank'>
|
||||||
<Button title={t('editor.documentBar.slideMode') ?? undefined} className='ms-2' size='sm' variant={buttonVariant}>
|
<Button title={buttonTitle} className='ms-2' size='sm' variant={buttonVariant}>
|
||||||
<UiIcon icon={IconTv} />
|
<UiIcon icon={IconTv} />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -5,10 +5,8 @@
|
||||||
*/
|
*/
|
||||||
import type { ChangeSpec, Transaction } from '@codemirror/state'
|
import type { ChangeSpec, Transaction } from '@codemirror/state'
|
||||||
import { Annotation } from '@codemirror/state'
|
import { Annotation } from '@codemirror/state'
|
||||||
import type { EditorView, PluginValue } from '@codemirror/view'
|
import type { EditorView, PluginValue, ViewUpdate } from '@codemirror/view'
|
||||||
import type { ViewUpdate } from '@codemirror/view'
|
import type { Text as YText, Transaction as YTransaction, YTextEvent } from 'yjs'
|
||||||
import type { Text as YText } from 'yjs'
|
|
||||||
import type { Transaction as YTransaction, YTextEvent } from 'yjs'
|
|
||||||
|
|
||||||
const syncAnnotation = Annotation.define()
|
const syncAnnotation = Annotation.define()
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { concatCssClasses } from '../../../../../utils/concat-css-classes'
|
||||||
import { createCursorCssClass } from './create-cursor-css-class'
|
import { createCursorCssClass } from './create-cursor-css-class'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
import type { SelectionRange } from '@codemirror/state'
|
import type { SelectionRange } from '@codemirror/state'
|
||||||
import type { LayerMarker, EditorView, Rect } from '@codemirror/view'
|
import type { EditorView, LayerMarker, Rect } from '@codemirror/view'
|
||||||
import { Direction } from '@codemirror/view'
|
import { Direction } from '@codemirror/view'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
||||||
import { ORIGIN, useBaseUrl } from '../../../hooks/common/use-base-url'
|
import { ORIGIN, useBaseUrl } from '../../../hooks/common/use-base-url'
|
||||||
import { useMayEdit } from '../../../hooks/common/use-may-edit'
|
import { useMayEdit } from '../../../hooks/common/use-may-edit'
|
||||||
|
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
|
||||||
import { useDarkModeState } from '../../../hooks/dark-mode/use-dark-mode-state'
|
import { useDarkModeState } from '../../../hooks/dark-mode/use-dark-mode-state'
|
||||||
import { cypressAttribute, cypressId } from '../../../utils/cypress-attribute'
|
import { cypressAttribute, cypressId } from '../../../utils/cypress-attribute'
|
||||||
import { findLanguageByCodeBlockName } from '../../markdown-renderer/extensions/_base-classes/code-block-markdown-extension/find-language-by-code-block-name'
|
import { findLanguageByCodeBlockName } from '../../markdown-renderer/extensions/_base-classes/code-block-markdown-extension/find-language-by-code-block-name'
|
||||||
|
@ -42,7 +43,6 @@ import { lintGutter } from '@codemirror/lint'
|
||||||
import { oneDark } from '@codemirror/theme-one-dark'
|
import { oneDark } from '@codemirror/theme-one-dark'
|
||||||
import ReactCodeMirror from '@uiw/react-codemirror'
|
import ReactCodeMirror from '@uiw/react-codemirror'
|
||||||
import React, { useEffect, useMemo } from 'react'
|
import React, { useEffect, useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
export type EditorPaneProps = ScrollProps
|
export type EditorPaneProps = ScrollProps
|
||||||
|
|
||||||
|
@ -129,7 +129,6 @@ export const EditorPane: React.FC<EditorPaneProps> = ({ scrollState, onScroll, o
|
||||||
[ligaturesEnabled]
|
[ligaturesEnabled]
|
||||||
)
|
)
|
||||||
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const darkModeActivated = useDarkModeState()
|
const darkModeActivated = useDarkModeState()
|
||||||
const editorOrigin = useBaseUrl(ORIGIN.EDITOR)
|
const editorOrigin = useBaseUrl(ORIGIN.EDITOR)
|
||||||
const isSynced = useApplicationState((state) => state.realtimeStatus.isSynced)
|
const isSynced = useApplicationState((state) => state.realtimeStatus.isSynced)
|
||||||
|
@ -142,6 +141,9 @@ export const EditorPane: React.FC<EditorPaneProps> = ({ scrollState, onScroll, o
|
||||||
}
|
}
|
||||||
}, [messageTransporter])
|
}, [messageTransporter])
|
||||||
|
|
||||||
|
const translateOptions = useMemo(() => ({ host: editorOrigin }), [editorOrigin])
|
||||||
|
const placeholderText = useTranslatedText('editor.placeholder', translateOptions)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`d-flex flex-column h-100 position-relative`}
|
className={`d-flex flex-column h-100 position-relative`}
|
||||||
|
@ -153,7 +155,7 @@ export const EditorPane: React.FC<EditorPaneProps> = ({ scrollState, onScroll, o
|
||||||
<ToolBar />
|
<ToolBar />
|
||||||
<ReactCodeMirror
|
<ReactCodeMirror
|
||||||
editable={updateViewContextExtension !== null && isSynced && mayEdit}
|
editable={updateViewContextExtension !== null && isSynced && mayEdit}
|
||||||
placeholder={t('editor.placeholder', { host: editorOrigin }) ?? ''}
|
placeholder={placeholderText}
|
||||||
extensions={extensions}
|
extensions={extensions}
|
||||||
width={'100%'}
|
width={'100%'}
|
||||||
height={'100%'}
|
height={'100%'}
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { ConnectionState } from '@hedgedoc/commons'
|
|
||||||
import type { TransportAdapter } from '@hedgedoc/commons'
|
import type { TransportAdapter } from '@hedgedoc/commons'
|
||||||
|
import { ConnectionState } from '@hedgedoc/commons'
|
||||||
import type { Message, MessageType } from '@hedgedoc/commons/dist'
|
import type { Message, MessageType } from '@hedgedoc/commons/dist'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -7,8 +7,7 @@ import { useApplicationState } from '../../../../../hooks/common/use-application
|
||||||
import { YTextSyncViewPlugin } from '../../codemirror-extensions/document-sync/y-text-sync-view-plugin'
|
import { YTextSyncViewPlugin } from '../../codemirror-extensions/document-sync/y-text-sync-view-plugin'
|
||||||
import type { Extension } from '@codemirror/state'
|
import type { Extension } from '@codemirror/state'
|
||||||
import { ViewPlugin } from '@codemirror/view'
|
import { ViewPlugin } from '@codemirror/view'
|
||||||
import type { YDocSyncClientAdapter } from '@hedgedoc/commons'
|
import type { RealtimeDoc, YDocSyncClientAdapter } from '@hedgedoc/commons'
|
||||||
import type { RealtimeDoc } from '@hedgedoc/commons'
|
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -7,8 +7,7 @@ import fontStyles from '../../../../../../global-styles/variables.module.scss'
|
||||||
import { useDarkModeState } from '../../../../../hooks/dark-mode/use-dark-mode-state'
|
import { useDarkModeState } from '../../../../../hooks/dark-mode/use-dark-mode-state'
|
||||||
import styles from './emoji-picker.module.scss'
|
import styles from './emoji-picker.module.scss'
|
||||||
import { Picker } from 'emoji-picker-element'
|
import { Picker } from 'emoji-picker-element'
|
||||||
import type { EmojiClickEvent, EmojiClickEventDetail } from 'emoji-picker-element/shared'
|
import type { EmojiClickEvent, EmojiClickEventDetail, PickerConstructorOptions } from 'emoji-picker-element/shared'
|
||||||
import type { PickerConstructorOptions } from 'emoji-picker-element/shared'
|
|
||||||
import React, { useEffect, useRef } from 'react'
|
import React, { useEffect, useRef } from 'react'
|
||||||
import { Popover } from 'react-bootstrap'
|
import { Popover } from 'react-bootstrap'
|
||||||
import type { PopoverProps } from 'react-bootstrap/Popover'
|
import type { PopoverProps } from 'react-bootstrap/Popover'
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
import { useTranslatedText } from '../../../../../hooks/common/use-translated-text'
|
||||||
import { cypressId } from '../../../../../utils/cypress-attribute'
|
import { cypressId } from '../../../../../utils/cypress-attribute'
|
||||||
import { UiIcon } from '../../../../common/icons/ui-icon'
|
import { UiIcon } from '../../../../common/icons/ui-icon'
|
||||||
import { CommonModal } from '../../../../common/modals/common-modal'
|
import { CommonModal } from '../../../../common/modals/common-modal'
|
||||||
|
@ -10,9 +11,8 @@ import type { TableSize } from './table-size-picker-popover'
|
||||||
import type { ChangeEvent } from 'react'
|
import type { ChangeEvent } from 'react'
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import { Button, Form, ModalFooter } from 'react-bootstrap'
|
import { Button, Form, ModalFooter } from 'react-bootstrap'
|
||||||
import { Table as IconTable } from 'react-bootstrap-icons'
|
import { Table as IconTable, X as IconX } from 'react-bootstrap-icons'
|
||||||
import { X as IconX } from 'react-bootstrap-icons'
|
import { Trans } from 'react-i18next'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
export interface CustomTableSizeModalProps {
|
export interface CustomTableSizeModalProps {
|
||||||
showModal: boolean
|
showModal: boolean
|
||||||
|
@ -33,7 +33,6 @@ const initialTableSize: TableSize = {
|
||||||
* @param onSizeSelect is called if the user entered and confirmed a custom table size
|
* @param onSizeSelect is called if the user entered and confirmed a custom table size
|
||||||
*/
|
*/
|
||||||
export const CustomTableSizeModal: React.FC<CustomTableSizeModalProps> = ({ showModal, onDismiss, onSizeSelect }) => {
|
export const CustomTableSizeModal: React.FC<CustomTableSizeModalProps> = ({ showModal, onDismiss, onSizeSelect }) => {
|
||||||
const { t } = useTranslation()
|
|
||||||
const [tableSize, setTableSize] = useState<TableSize>(() => initialTableSize)
|
const [tableSize, setTableSize] = useState<TableSize>(() => initialTableSize)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -63,6 +62,9 @@ export const CustomTableSizeModal: React.FC<CustomTableSizeModalProps> = ({ show
|
||||||
}))
|
}))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const columnPlaceholderText = useTranslatedText('editor.editorToolbar.table.cols')
|
||||||
|
const rowsPlaceholderText = useTranslatedText('editor.editorToolbar.table.rows')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommonModal
|
<CommonModal
|
||||||
show={showModal}
|
show={showModal}
|
||||||
|
@ -75,7 +77,7 @@ export const CustomTableSizeModal: React.FC<CustomTableSizeModalProps> = ({ show
|
||||||
<Form.Control
|
<Form.Control
|
||||||
type={'number'}
|
type={'number'}
|
||||||
min={1}
|
min={1}
|
||||||
placeholder={t('editor.editorToolbar.table.cols') ?? undefined}
|
placeholder={columnPlaceholderText}
|
||||||
isInvalid={tableSize.columns <= 0}
|
isInvalid={tableSize.columns <= 0}
|
||||||
onChange={onColChange}
|
onChange={onColChange}
|
||||||
/>
|
/>
|
||||||
|
@ -83,7 +85,7 @@ export const CustomTableSizeModal: React.FC<CustomTableSizeModalProps> = ({ show
|
||||||
<Form.Control
|
<Form.Control
|
||||||
type={'number'}
|
type={'number'}
|
||||||
min={1}
|
min={1}
|
||||||
placeholder={t('editor.editorToolbar.table.rows') ?? undefined}
|
placeholder={rowsPlaceholderText}
|
||||||
isInvalid={tableSize.rows <= 0}
|
isInvalid={tableSize.rows <= 0}
|
||||||
onChange={onRowChange}
|
onChange={onRowChange}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -61,7 +61,7 @@ export const TableSizePickerPopover = React.forwardRef<HTMLDivElement, TableSize
|
||||||
{...cypressAttribute('col', `${col + 1}`)}
|
{...cypressAttribute('col', `${col + 1}`)}
|
||||||
{...cypressAttribute('row', `${row + 1}`)}
|
{...cypressAttribute('row', `${row + 1}`)}
|
||||||
onMouseEnter={onSizeHover(row + 1, col + 1)}
|
onMouseEnter={onSizeHover(row + 1, col + 1)}
|
||||||
title={t('editor.editorToolbar.table.titleWithSize', { cols: col + 1, rows: row + 1 }) ?? undefined}
|
title={t('editor.editorToolbar.table.titleWithSize', { cols: col + 1, rows: row + 1 })}
|
||||||
onClick={() => onTableSizeSelected(row + 1, col + 1)}
|
onClick={() => onTableSizeSelected(row + 1, col + 1)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,8 +4,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { getGlobalState } from '../../../../redux'
|
import { getGlobalState } from '../../../../redux'
|
||||||
import type { ScrollState } from '../../synced-scroll/scroll-props'
|
import type { ScrollCallback, ScrollState } from '../../synced-scroll/scroll-props'
|
||||||
import type { ScrollCallback } from '../../synced-scroll/scroll-props'
|
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { mockI18n } from '../../../../../../test-utils/mock-i18n'
|
||||||
import { mockNoteOwnership } from '../../../../../../test-utils/note-ownership'
|
import { mockNoteOwnership } from '../../../../../../test-utils/note-ownership'
|
||||||
import * as useUiNotificationsModule from '../../../../../notifications/ui-notification-boundary'
|
import * as useUiNotificationsModule from '../../../../../notifications/ui-notification-boundary'
|
||||||
import { AliasesAddForm } from './aliases-add-form'
|
import { AliasesAddForm } from './aliases-add-form'
|
||||||
import { render, act, screen } from '@testing-library/react'
|
import { act, render, screen } from '@testing-library/react'
|
||||||
import testEvent from '@testing-library/user-event'
|
import testEvent from '@testing-library/user-event'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { addAlias } from '../../../../../../api/alias'
|
||||||
import { useApplicationState } from '../../../../../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../../../../../hooks/common/use-application-state'
|
||||||
import { useIsOwner } from '../../../../../../hooks/common/use-is-owner'
|
import { useIsOwner } from '../../../../../../hooks/common/use-is-owner'
|
||||||
import { useOnInputChange } from '../../../../../../hooks/common/use-on-input-change'
|
import { useOnInputChange } from '../../../../../../hooks/common/use-on-input-change'
|
||||||
|
import { useTranslatedText } from '../../../../../../hooks/common/use-translated-text'
|
||||||
import { updateMetadata } from '../../../../../../redux/note-details/methods'
|
import { updateMetadata } from '../../../../../../redux/note-details/methods'
|
||||||
import { testId } from '../../../../../../utils/test-id'
|
import { testId } from '../../../../../../utils/test-id'
|
||||||
import { UiIcon } from '../../../../../common/icons/ui-icon'
|
import { UiIcon } from '../../../../../common/icons/ui-icon'
|
||||||
|
@ -15,7 +16,6 @@ import type { FormEvent } from 'react'
|
||||||
import React, { useCallback, useMemo, useState } from 'react'
|
import React, { useCallback, useMemo, useState } from 'react'
|
||||||
import { Button, Form, InputGroup } from 'react-bootstrap'
|
import { Button, Form, InputGroup } from 'react-bootstrap'
|
||||||
import { Plus as IconPlus } from 'react-bootstrap-icons'
|
import { Plus as IconPlus } from 'react-bootstrap-icons'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
const validAliasRegex = /^[a-z0-9_-]*$/
|
const validAliasRegex = /^[a-z0-9_-]*$/
|
||||||
|
|
||||||
|
@ -23,7 +23,6 @@ const validAliasRegex = /^[a-z0-9_-]*$/
|
||||||
* Form for adding a new alias to a note.
|
* Form for adding a new alias to a note.
|
||||||
*/
|
*/
|
||||||
export const AliasesAddForm: React.FC = () => {
|
export const AliasesAddForm: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
|
||||||
const { showErrorNotification } = useUiNotifications()
|
const { showErrorNotification } = useUiNotifications()
|
||||||
const noteId = useApplicationState((state) => state.noteDetails.id)
|
const noteId = useApplicationState((state) => state.noteDetails.id)
|
||||||
const isOwner = useIsOwner()
|
const isOwner = useIsOwner()
|
||||||
|
@ -48,12 +47,14 @@ export const AliasesAddForm: React.FC = () => {
|
||||||
return validAliasRegex.test(newAlias)
|
return validAliasRegex.test(newAlias)
|
||||||
}, [newAlias])
|
}, [newAlias])
|
||||||
|
|
||||||
|
const addAliasText = useTranslatedText('editor.modal.aliases.addAlias')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={onAddAlias}>
|
<form onSubmit={onAddAlias}>
|
||||||
<InputGroup className={'me-1 mb-1'} hasValidation={true}>
|
<InputGroup className={'me-1 mb-1'} hasValidation={true}>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
value={newAlias}
|
value={newAlias}
|
||||||
placeholder={t('editor.modal.aliases.addAlias') ?? undefined}
|
placeholder={addAliasText}
|
||||||
onChange={onNewAliasInputChange}
|
onChange={onNewAliasInputChange}
|
||||||
isInvalid={!newAliasValid}
|
isInvalid={!newAliasValid}
|
||||||
disabled={!isOwner}
|
disabled={!isOwner}
|
||||||
|
@ -65,7 +66,7 @@ export const AliasesAddForm: React.FC = () => {
|
||||||
variant='light'
|
variant='light'
|
||||||
className={'text-secondary ms-2'}
|
className={'text-secondary ms-2'}
|
||||||
disabled={!isOwner || !newAliasValid || newAlias === ''}
|
disabled={!isOwner || !newAliasValid || newAlias === ''}
|
||||||
title={t('editor.modal.aliases.addAlias') ?? undefined}
|
title={addAliasText}
|
||||||
{...testId('addAliasButton')}>
|
{...testId('addAliasButton')}>
|
||||||
<UiIcon icon={IconPlus} />
|
<UiIcon icon={IconPlus} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { mockI18n } from '../../../../../../test-utils/mock-i18n'
|
||||||
import { mockNoteOwnership } from '../../../../../../test-utils/note-ownership'
|
import { mockNoteOwnership } from '../../../../../../test-utils/note-ownership'
|
||||||
import * as useUiNotificationsModule from '../../../../../notifications/ui-notification-boundary'
|
import * as useUiNotificationsModule from '../../../../../notifications/ui-notification-boundary'
|
||||||
import { AliasesListEntry } from './aliases-list-entry'
|
import { AliasesListEntry } from './aliases-list-entry'
|
||||||
import { render, act, screen } from '@testing-library/react'
|
import { act, render, screen } from '@testing-library/react'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
jest.mock('../../../../../../api/alias')
|
jest.mock('../../../../../../api/alias')
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
import { deleteAlias, markAliasAsPrimary } from '../../../../../../api/alias'
|
import { deleteAlias, markAliasAsPrimary } from '../../../../../../api/alias'
|
||||||
import type { Alias } from '../../../../../../api/alias/types'
|
import type { Alias } from '../../../../../../api/alias/types'
|
||||||
import { useIsOwner } from '../../../../../../hooks/common/use-is-owner'
|
import { useIsOwner } from '../../../../../../hooks/common/use-is-owner'
|
||||||
|
import { useTranslatedText } from '../../../../../../hooks/common/use-translated-text'
|
||||||
import { updateMetadata } from '../../../../../../redux/note-details/methods'
|
import { updateMetadata } from '../../../../../../redux/note-details/methods'
|
||||||
import { testId } from '../../../../../../utils/test-id'
|
import { testId } from '../../../../../../utils/test-id'
|
||||||
import { UiIcon } from '../../../../../common/icons/ui-icon'
|
import { UiIcon } from '../../../../../common/icons/ui-icon'
|
||||||
|
@ -13,9 +14,7 @@ import { ShowIf } from '../../../../../common/show-if/show-if'
|
||||||
import { useUiNotifications } from '../../../../../notifications/ui-notification-boundary'
|
import { useUiNotifications } from '../../../../../notifications/ui-notification-boundary'
|
||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react'
|
||||||
import { Button } from 'react-bootstrap'
|
import { Button } from 'react-bootstrap'
|
||||||
import { StarFill as IconStarFill } from 'react-bootstrap-icons'
|
import { Star as IconStar, StarFill as IconStarFill, X as IconX } from 'react-bootstrap-icons'
|
||||||
import { Star as IconStar } from 'react-bootstrap-icons'
|
|
||||||
import { X as IconX } from 'react-bootstrap-icons'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
export interface AliasesListEntryProps {
|
export interface AliasesListEntryProps {
|
||||||
|
@ -44,6 +43,10 @@ export const AliasesListEntry: React.FC<AliasesListEntryProps> = ({ alias }) =>
|
||||||
.catch(showErrorNotification(t('editor.modal.aliases.errorMakingPrimary')))
|
.catch(showErrorNotification(t('editor.modal.aliases.errorMakingPrimary')))
|
||||||
}, [alias, t, showErrorNotification])
|
}, [alias, t, showErrorNotification])
|
||||||
|
|
||||||
|
const isPrimaryText = useTranslatedText('editor.modal.aliases.isPrimary')
|
||||||
|
const makePrimaryText = useTranslatedText('editor.modal.aliases.makePrimary')
|
||||||
|
const removeAliasText = useTranslatedText('editor.modal.aliases.removeAlias')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className={'list-group-item d-flex flex-row justify-content-between align-items-center'}>
|
<li className={'list-group-item d-flex flex-row justify-content-between align-items-center'}>
|
||||||
{alias.name}
|
{alias.name}
|
||||||
|
@ -53,7 +56,7 @@ export const AliasesListEntry: React.FC<AliasesListEntryProps> = ({ alias }) =>
|
||||||
className={'me-2 text-warning'}
|
className={'me-2 text-warning'}
|
||||||
variant='light'
|
variant='light'
|
||||||
disabled={true}
|
disabled={true}
|
||||||
title={t('editor.modal.aliases.isPrimary') ?? undefined}
|
title={isPrimaryText}
|
||||||
{...testId('aliasIsPrimary')}>
|
{...testId('aliasIsPrimary')}>
|
||||||
<UiIcon icon={IconStar} />
|
<UiIcon icon={IconStar} />
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -63,7 +66,7 @@ export const AliasesListEntry: React.FC<AliasesListEntryProps> = ({ alias }) =>
|
||||||
className={'me-2'}
|
className={'me-2'}
|
||||||
variant='light'
|
variant='light'
|
||||||
disabled={!isOwner}
|
disabled={!isOwner}
|
||||||
title={t('editor.modal.aliases.makePrimary') ?? undefined}
|
title={makePrimaryText}
|
||||||
onClick={onMakePrimaryClick}
|
onClick={onMakePrimaryClick}
|
||||||
{...testId('aliasButtonMakePrimary')}>
|
{...testId('aliasButtonMakePrimary')}>
|
||||||
<UiIcon icon={IconStarFill} />
|
<UiIcon icon={IconStarFill} />
|
||||||
|
@ -73,7 +76,7 @@ export const AliasesListEntry: React.FC<AliasesListEntryProps> = ({ alias }) =>
|
||||||
variant='light'
|
variant='light'
|
||||||
className={'text-danger'}
|
className={'text-danger'}
|
||||||
disabled={!isOwner}
|
disabled={!isOwner}
|
||||||
title={t('editor.modal.aliases.removeAlias') ?? undefined}
|
title={removeAliasText}
|
||||||
onClick={onRemoveClick}
|
onClick={onRemoveClick}
|
||||||
{...testId('aliasButtonRemove')}>
|
{...testId('aliasButtonRemove')}>
|
||||||
<UiIcon icon={IconX} />
|
<UiIcon icon={IconX} />
|
||||||
|
|
|
@ -7,8 +7,8 @@ import type { Alias } from '../../../../../../api/alias/types'
|
||||||
import * as useApplicationStateModule from '../../../../../../hooks/common/use-application-state'
|
import * as useApplicationStateModule from '../../../../../../hooks/common/use-application-state'
|
||||||
import { mockI18n } from '../../../../../../test-utils/mock-i18n'
|
import { mockI18n } from '../../../../../../test-utils/mock-i18n'
|
||||||
import { AliasesList } from './aliases-list'
|
import { AliasesList } from './aliases-list'
|
||||||
import * as AliasesListEntryModule from './aliases-list-entry'
|
|
||||||
import type { AliasesListEntryProps } from './aliases-list-entry'
|
import type { AliasesListEntryProps } from './aliases-list-entry'
|
||||||
|
import * as AliasesListEntryModule from './aliases-list-entry'
|
||||||
import { render } from '@testing-library/react'
|
import { render } from '@testing-library/react'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
|
|
@ -11,8 +11,8 @@ import * as AliasesAddFormModule from './aliases-add-form'
|
||||||
import * as AliasesListModule from './aliases-list'
|
import * as AliasesListModule from './aliases-list'
|
||||||
import { AliasesModal } from './aliases-modal'
|
import { AliasesModal } from './aliases-modal'
|
||||||
import { render } from '@testing-library/react'
|
import { render } from '@testing-library/react'
|
||||||
import React from 'react'
|
|
||||||
import type { PropsWithChildren } from 'react'
|
import type { PropsWithChildren } from 'react'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
jest.mock('./aliases-list')
|
jest.mock('./aliases-list')
|
||||||
jest.mock('./aliases-add-form')
|
jest.mock('./aliases-add-form')
|
||||||
|
|
|
@ -11,10 +11,12 @@ import type { SpecificSidebarMenuProps } from '../types'
|
||||||
import { DocumentSidebarMenuSelection } from '../types'
|
import { DocumentSidebarMenuSelection } from '../types'
|
||||||
import { ExportMarkdownSidebarEntry } from './export-markdown-sidebar-entry'
|
import { ExportMarkdownSidebarEntry } from './export-markdown-sidebar-entry'
|
||||||
import React, { Fragment, useCallback } from 'react'
|
import React, { Fragment, useCallback } from 'react'
|
||||||
import { ArrowLeft as IconArrowLeft } from 'react-bootstrap-icons'
|
import {
|
||||||
import { CloudDownload as IconCloudDownload } from 'react-bootstrap-icons'
|
ArrowLeft as IconArrowLeft,
|
||||||
import { FileCode as IconFileCode } from 'react-bootstrap-icons'
|
CloudDownload as IconCloudDownload,
|
||||||
import { Github as IconGithub } from 'react-bootstrap-icons'
|
FileCode as IconFileCode,
|
||||||
|
Github as IconGithub
|
||||||
|
} from 'react-bootstrap-icons'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -11,10 +11,12 @@ import type { SpecificSidebarMenuProps } from '../types'
|
||||||
import { DocumentSidebarMenuSelection } from '../types'
|
import { DocumentSidebarMenuSelection } from '../types'
|
||||||
import { ImportMarkdownSidebarEntry } from './import-markdown-sidebar-entry'
|
import { ImportMarkdownSidebarEntry } from './import-markdown-sidebar-entry'
|
||||||
import React, { Fragment, useCallback } from 'react'
|
import React, { Fragment, useCallback } from 'react'
|
||||||
import { ArrowLeft as IconArrowLeft } from 'react-bootstrap-icons'
|
import {
|
||||||
import { Clipboard as IconClipboard } from 'react-bootstrap-icons'
|
ArrowLeft as IconArrowLeft,
|
||||||
import { CloudUpload as IconCloudUpload } from 'react-bootstrap-icons'
|
Clipboard as IconClipboard,
|
||||||
import { Github as IconGithub } from 'react-bootstrap-icons'
|
CloudUpload as IconCloudUpload,
|
||||||
|
Github as IconGithub
|
||||||
|
} from 'react-bootstrap-icons'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -4,12 +4,12 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { useLowercaseOnInputChange } from '../../../../../../hooks/common/use-lowercase-on-input-change'
|
import { useLowercaseOnInputChange } from '../../../../../../hooks/common/use-lowercase-on-input-change'
|
||||||
|
import { useTranslatedText } from '../../../../../../hooks/common/use-translated-text'
|
||||||
import { UiIcon } from '../../../../../common/icons/ui-icon'
|
import { UiIcon } from '../../../../../common/icons/ui-icon'
|
||||||
import type { PermissionDisabledProps } from './permission-disabled.prop'
|
import type { PermissionDisabledProps } from './permission-disabled.prop'
|
||||||
import React, { useCallback, useState } from 'react'
|
import React, { useCallback, useState } from 'react'
|
||||||
import { Button, FormControl, InputGroup } from 'react-bootstrap'
|
import { Button, FormControl, InputGroup } from 'react-bootstrap'
|
||||||
import { Plus as IconPlus } from 'react-bootstrap-icons'
|
import { Plus as IconPlus } from 'react-bootstrap-icons'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
export interface PermissionAddEntryFieldProps {
|
export interface PermissionAddEntryFieldProps {
|
||||||
onAddEntry: (identifier: string) => void
|
onAddEntry: (identifier: string) => void
|
||||||
|
@ -28,8 +28,6 @@ export const PermissionAddEntryField: React.FC<PermissionAddEntryFieldProps & Pe
|
||||||
i18nKey,
|
i18nKey,
|
||||||
disabled
|
disabled
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const [newEntryIdentifier, setNewEntryIdentifier] = useState('')
|
const [newEntryIdentifier, setNewEntryIdentifier] = useState('')
|
||||||
const onChange = useLowercaseOnInputChange(setNewEntryIdentifier)
|
const onChange = useLowercaseOnInputChange(setNewEntryIdentifier)
|
||||||
|
|
||||||
|
@ -37,19 +35,16 @@ export const PermissionAddEntryField: React.FC<PermissionAddEntryFieldProps & Pe
|
||||||
onAddEntry(newEntryIdentifier)
|
onAddEntry(newEntryIdentifier)
|
||||||
}, [newEntryIdentifier, onAddEntry])
|
}, [newEntryIdentifier, onAddEntry])
|
||||||
|
|
||||||
|
const placeholderText = useTranslatedText(i18nKey)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className={'list-group-item'}>
|
<li className={'list-group-item'}>
|
||||||
<InputGroup className={'me-1 mb-1'}>
|
<InputGroup className={'me-1 mb-1'}>
|
||||||
<FormControl
|
<FormControl value={newEntryIdentifier} placeholder={placeholderText} onChange={onChange} disabled={disabled} />
|
||||||
value={newEntryIdentifier}
|
|
||||||
placeholder={t(i18nKey) ?? undefined}
|
|
||||||
onChange={onChange}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
<Button
|
<Button
|
||||||
variant='light'
|
variant='light'
|
||||||
className={'text-secondary ms-2'}
|
className={'text-secondary ms-2'}
|
||||||
title={t(i18nKey) ?? undefined}
|
title={placeholderText}
|
||||||
onClick={onSubmit}
|
onClick={onSubmit}
|
||||||
disabled={disabled}>
|
disabled={disabled}>
|
||||||
<UiIcon icon={IconPlus} />
|
<UiIcon icon={IconPlus} />
|
||||||
|
|
|
@ -3,15 +3,13 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
import { useTranslatedText } from '../../../../../../hooks/common/use-translated-text'
|
||||||
import { UiIcon } from '../../../../../common/icons/ui-icon'
|
import { UiIcon } from '../../../../../common/icons/ui-icon'
|
||||||
import type { PermissionDisabledProps } from './permission-disabled.prop'
|
import type { PermissionDisabledProps } from './permission-disabled.prop'
|
||||||
import { AccessLevel } from '@hedgedoc/commons'
|
import { AccessLevel } from '@hedgedoc/commons'
|
||||||
import React, { useMemo } from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import { Button, ToggleButtonGroup } from 'react-bootstrap'
|
import { Button, ToggleButtonGroup } from 'react-bootstrap'
|
||||||
import { Eye as IconEye } from 'react-bootstrap-icons'
|
import { Eye as IconEye, Pencil as IconPencil, X as IconX } from 'react-bootstrap-icons'
|
||||||
import { Pencil as IconPencil } from 'react-bootstrap-icons'
|
|
||||||
import { X as IconX } from 'react-bootstrap-icons'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
interface PermissionEntryButtonI18nKeys {
|
interface PermissionEntryButtonI18nKeys {
|
||||||
remove: string
|
remove: string
|
||||||
|
@ -53,8 +51,6 @@ export const PermissionEntryButtons: React.FC<PermissionEntryButtonsProps & Perm
|
||||||
onRemove,
|
onRemove,
|
||||||
disabled
|
disabled
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const i18nKeys: PermissionEntryButtonI18nKeys = useMemo(() => {
|
const i18nKeys: PermissionEntryButtonI18nKeys = useMemo(() => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case PermissionType.USER:
|
case PermissionType.USER:
|
||||||
|
@ -72,27 +68,27 @@ export const PermissionEntryButtons: React.FC<PermissionEntryButtonsProps & Perm
|
||||||
}
|
}
|
||||||
}, [type])
|
}, [type])
|
||||||
|
|
||||||
|
const translateOptions = useMemo(() => ({ name }), [name])
|
||||||
|
const removeTitle = useTranslatedText(i18nKeys.remove, translateOptions)
|
||||||
|
const setReadOnlyTitle = useTranslatedText(i18nKeys.setReadOnly, translateOptions)
|
||||||
|
const setWritableTitle = useTranslatedText(i18nKeys.setWriteable, translateOptions)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button variant='light' className={'text-danger me-2'} disabled={disabled} title={removeTitle} onClick={onRemove}>
|
||||||
variant='light'
|
|
||||||
className={'text-danger me-2'}
|
|
||||||
disabled={disabled}
|
|
||||||
title={t(i18nKeys.remove, { name }) ?? undefined}
|
|
||||||
onClick={onRemove}>
|
|
||||||
<UiIcon icon={IconX} />
|
<UiIcon icon={IconX} />
|
||||||
</Button>
|
</Button>
|
||||||
<ToggleButtonGroup type='radio' name='edit-mode' value={currentSetting}>
|
<ToggleButtonGroup type='radio' name='edit-mode' value={currentSetting}>
|
||||||
<Button
|
<Button
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
title={t(i18nKeys.setReadOnly, { name }) ?? undefined}
|
title={setReadOnlyTitle}
|
||||||
variant={currentSetting === AccessLevel.READ_ONLY ? 'secondary' : 'outline-secondary'}
|
variant={currentSetting === AccessLevel.READ_ONLY ? 'secondary' : 'outline-secondary'}
|
||||||
onClick={onSetReadOnly}>
|
onClick={onSetReadOnly}>
|
||||||
<UiIcon icon={IconEye} />
|
<UiIcon icon={IconEye} />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
title={t(i18nKeys.setWriteable, { name }) ?? undefined}
|
title={setWritableTitle}
|
||||||
variant={currentSetting === AccessLevel.WRITEABLE ? 'secondary' : 'outline-secondary'}
|
variant={currentSetting === AccessLevel.WRITEABLE ? 'secondary' : 'outline-secondary'}
|
||||||
onClick={onSetWriteable}>
|
onClick={onSetWriteable}>
|
||||||
<UiIcon icon={IconPencil} />
|
<UiIcon icon={IconPencil} />
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
*/
|
*/
|
||||||
import { removeGroupPermission, setGroupPermission } from '../../../../../../api/permissions'
|
import { removeGroupPermission, setGroupPermission } from '../../../../../../api/permissions'
|
||||||
import { useApplicationState } from '../../../../../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../../../../../hooks/common/use-application-state'
|
||||||
|
import { useTranslatedText } from '../../../../../../hooks/common/use-translated-text'
|
||||||
import { setNotePermissionsFromServer } from '../../../../../../redux/note-details/methods'
|
import { setNotePermissionsFromServer } from '../../../../../../redux/note-details/methods'
|
||||||
import { IconButton } from '../../../../../common/icon-button/icon-button'
|
import { IconButton } from '../../../../../common/icon-button/icon-button'
|
||||||
import { useUiNotifications } from '../../../../../notifications/ui-notification-boundary'
|
import { useUiNotifications } from '../../../../../notifications/ui-notification-boundary'
|
||||||
|
@ -12,9 +13,7 @@ import type { PermissionDisabledProps } from './permission-disabled.prop'
|
||||||
import { AccessLevel, SpecialGroup } from '@hedgedoc/commons'
|
import { AccessLevel, SpecialGroup } from '@hedgedoc/commons'
|
||||||
import React, { useCallback, useMemo } from 'react'
|
import React, { useCallback, useMemo } from 'react'
|
||||||
import { ToggleButtonGroup } from 'react-bootstrap'
|
import { ToggleButtonGroup } from 'react-bootstrap'
|
||||||
import { Eye as IconEye } from 'react-bootstrap-icons'
|
import { Eye as IconEye, Pencil as IconPencil, SlashCircle as IconSlashCircle } from 'react-bootstrap-icons'
|
||||||
import { Pencil as IconPencil } from 'react-bootstrap-icons'
|
|
||||||
import { SlashCircle as IconSlashCircle } from 'react-bootstrap-icons'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
export interface PermissionEntrySpecialGroupProps {
|
export interface PermissionEntrySpecialGroupProps {
|
||||||
|
@ -71,6 +70,11 @@ export const PermissionEntrySpecialGroup: React.FC<PermissionEntrySpecialGroupPr
|
||||||
}
|
}
|
||||||
}, [type, t])
|
}, [type, t])
|
||||||
|
|
||||||
|
const translateOptions = useMemo(() => ({ name }), [name])
|
||||||
|
const denyGroupText = useTranslatedText('editor.modal.permissions.denyGroup', translateOptions)
|
||||||
|
const viewOnlyGroupText = useTranslatedText('editor.modal.permissions.viewOnlyGroup', translateOptions)
|
||||||
|
const editGroupText = useTranslatedText('editor.modal.permissions.editGroup', translateOptions)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className={'list-group-item d-flex flex-row justify-content-between align-items-center'}>
|
<li className={'list-group-item d-flex flex-row justify-content-between align-items-center'}>
|
||||||
<span>{name}</span>
|
<span>{name}</span>
|
||||||
|
@ -78,7 +82,7 @@ export const PermissionEntrySpecialGroup: React.FC<PermissionEntrySpecialGroupPr
|
||||||
<ToggleButtonGroup type='radio' name='edit-mode'>
|
<ToggleButtonGroup type='radio' name='edit-mode'>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={IconSlashCircle}
|
icon={IconSlashCircle}
|
||||||
title={t('editor.modal.permissions.denyGroup', { name }) ?? undefined}
|
title={denyGroupText}
|
||||||
variant={level === AccessLevel.NONE ? 'secondary' : 'outline-secondary'}
|
variant={level === AccessLevel.NONE ? 'secondary' : 'outline-secondary'}
|
||||||
onClick={onSetEntryDenied}
|
onClick={onSetEntryDenied}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
@ -86,7 +90,7 @@ export const PermissionEntrySpecialGroup: React.FC<PermissionEntrySpecialGroupPr
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={IconEye}
|
icon={IconEye}
|
||||||
title={t('editor.modal.permissions.viewOnlyGroup', { name }) ?? undefined}
|
title={viewOnlyGroupText}
|
||||||
variant={level === AccessLevel.READ_ONLY ? 'secondary' : 'outline-secondary'}
|
variant={level === AccessLevel.READ_ONLY ? 'secondary' : 'outline-secondary'}
|
||||||
onClick={onSetEntryReadOnly}
|
onClick={onSetEntryReadOnly}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
@ -94,7 +98,7 @@ export const PermissionEntrySpecialGroup: React.FC<PermissionEntrySpecialGroupPr
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={IconPencil}
|
icon={IconPencil}
|
||||||
title={t('editor.modal.permissions.editGroup', { name }) ?? undefined}
|
title={editGroupText}
|
||||||
variant={level === AccessLevel.WRITEABLE ? 'secondary' : 'outline-secondary'}
|
variant={level === AccessLevel.WRITEABLE ? 'secondary' : 'outline-secondary'}
|
||||||
onClick={onSetEntryWriteable}
|
onClick={onSetEntryWriteable}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|
|
@ -4,11 +4,11 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { useOnInputChange } from '../../../../../../hooks/common/use-on-input-change'
|
import { useOnInputChange } from '../../../../../../hooks/common/use-on-input-change'
|
||||||
|
import { useTranslatedText } from '../../../../../../hooks/common/use-translated-text'
|
||||||
import { UiIcon } from '../../../../../common/icons/ui-icon'
|
import { UiIcon } from '../../../../../common/icons/ui-icon'
|
||||||
import React, { useCallback, useMemo, useState } from 'react'
|
import React, { useCallback, useMemo, useState } from 'react'
|
||||||
import { Button, FormControl, InputGroup } from 'react-bootstrap'
|
import { Button, FormControl, InputGroup } from 'react-bootstrap'
|
||||||
import { Check as IconCheck } from 'react-bootstrap-icons'
|
import { Check as IconCheck } from 'react-bootstrap-icons'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
export interface PermissionOwnerChangeProps {
|
export interface PermissionOwnerChangeProps {
|
||||||
onConfirmOwnerChange: (newOwner: string) => void
|
onConfirmOwnerChange: (newOwner: string) => void
|
||||||
|
@ -20,7 +20,6 @@ export interface PermissionOwnerChangeProps {
|
||||||
* @param onConfirmOwnerChange The callback to call if the owner was changed.
|
* @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 [ownerFieldValue, setOwnerFieldValue] = useState('')
|
const [ownerFieldValue, setOwnerFieldValue] = useState('')
|
||||||
|
|
||||||
const onChangeField = useOnInputChange(setOwnerFieldValue)
|
const onChangeField = useOnInputChange(setOwnerFieldValue)
|
||||||
|
@ -32,16 +31,15 @@ export const PermissionOwnerChange: React.FC<PermissionOwnerChangeProps> = ({ on
|
||||||
return ownerFieldValue.trim() === ''
|
return ownerFieldValue.trim() === ''
|
||||||
}, [ownerFieldValue])
|
}, [ownerFieldValue])
|
||||||
|
|
||||||
|
const placeholderText = useTranslatedText('editor.modal.permissions.ownerChange.placeholder')
|
||||||
|
const buttonTitleText = useTranslatedText('common.save')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InputGroup className={'me-1 mb-1'}>
|
<InputGroup className={'me-1 mb-1'}>
|
||||||
<FormControl
|
<FormControl value={ownerFieldValue} placeholder={placeholderText} onChange={onChangeField} />
|
||||||
value={ownerFieldValue}
|
|
||||||
placeholder={t('editor.modal.permissions.ownerChange.placeholder') ?? undefined}
|
|
||||||
onChange={onChangeField}
|
|
||||||
/>
|
|
||||||
<Button
|
<Button
|
||||||
variant='light'
|
variant='light'
|
||||||
title={t('common.save') ?? undefined}
|
title={buttonTitleText}
|
||||||
onClick={onClickConfirm}
|
onClick={onClickConfirm}
|
||||||
className={'ms-2'}
|
className={'ms-2'}
|
||||||
disabled={confirmButtonDisabled}>
|
disabled={confirmButtonDisabled}>
|
||||||
|
|
|
@ -4,13 +4,13 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { useApplicationState } from '../../../../../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../../../../../hooks/common/use-application-state'
|
||||||
|
import { useTranslatedText } from '../../../../../../hooks/common/use-translated-text'
|
||||||
import { UiIcon } from '../../../../../common/icons/ui-icon'
|
import { UiIcon } from '../../../../../common/icons/ui-icon'
|
||||||
import { UserAvatarForUsername } from '../../../../../common/user-avatar/user-avatar-for-username'
|
import { UserAvatarForUsername } from '../../../../../common/user-avatar/user-avatar-for-username'
|
||||||
import type { PermissionDisabledProps } from './permission-disabled.prop'
|
import type { PermissionDisabledProps } from './permission-disabled.prop'
|
||||||
import React, { Fragment } from 'react'
|
import React, { Fragment } from 'react'
|
||||||
import { Button } from 'react-bootstrap'
|
import { Button } from 'react-bootstrap'
|
||||||
import { Pencil as IconPencil } from 'react-bootstrap-icons'
|
import { Pencil as IconPencil } from 'react-bootstrap-icons'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
export interface PermissionOwnerInfoProps {
|
export interface PermissionOwnerInfoProps {
|
||||||
onEditOwner: () => void
|
onEditOwner: () => void
|
||||||
|
@ -26,17 +26,13 @@ export const PermissionOwnerInfo: React.FC<PermissionOwnerInfoProps & Permission
|
||||||
onEditOwner,
|
onEditOwner,
|
||||||
disabled
|
disabled
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
|
||||||
const noteOwner = useApplicationState((state) => state.noteDetails.permissions.owner)
|
const noteOwner = useApplicationState((state) => state.noteDetails.permissions.owner)
|
||||||
|
const buttonTitle = useTranslatedText('editor.modal.permissions.ownerChange.button')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<UserAvatarForUsername username={noteOwner} />
|
<UserAvatarForUsername username={noteOwner} />
|
||||||
<Button
|
<Button variant='light' disabled={disabled} title={buttonTitle} onClick={onEditOwner}>
|
||||||
variant='light'
|
|
||||||
disabled={disabled}
|
|
||||||
title={t('editor.modal.permissions.ownerChange.button') ?? undefined}
|
|
||||||
onClick={onEditOwner}>
|
|
||||||
<UiIcon icon={IconPencil} />
|
<UiIcon icon={IconPencil} />
|
||||||
</Button>
|
</Button>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
|
|
@ -14,10 +14,12 @@ import { getUserDataForRevision } from './utils'
|
||||||
import { DateTime } from 'luxon'
|
import { DateTime } from 'luxon'
|
||||||
import React, { useMemo } from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import { ListGroup } from 'react-bootstrap'
|
import { ListGroup } from 'react-bootstrap'
|
||||||
import { Clock as IconClock } from 'react-bootstrap-icons'
|
import {
|
||||||
import { FileText as IconFileText } from 'react-bootstrap-icons'
|
Clock as IconClock,
|
||||||
import { Person as IconPerson } from 'react-bootstrap-icons'
|
FileText as IconFileText,
|
||||||
import { PersonPlus as IconPersonPlus } from 'react-bootstrap-icons'
|
Person as IconPerson,
|
||||||
|
PersonPlus as IconPersonPlus
|
||||||
|
} from 'react-bootstrap-icons'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { useAsync } from 'react-use'
|
import { useAsync } from 'react-use'
|
||||||
|
|
||||||
|
|
|
@ -13,8 +13,7 @@ import styles from './online-counter.module.scss'
|
||||||
import { OwnUserLine } from './own-user-line'
|
import { OwnUserLine } from './own-user-line'
|
||||||
import { UserLine } from './user-line/user-line'
|
import { UserLine } from './user-line/user-line'
|
||||||
import React, { Fragment, useCallback, useEffect, useMemo, useRef } from 'react'
|
import React, { Fragment, useCallback, useEffect, useMemo, useRef } from 'react'
|
||||||
import { ArrowLeft as IconArrowLeft } from 'react-bootstrap-icons'
|
import { ArrowLeft as IconArrowLeft, People as IconPeople } from 'react-bootstrap-icons'
|
||||||
import { People as IconPeople } from 'react-bootstrap-icons'
|
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -9,9 +9,11 @@ import { UiIcon } from '../../../common/icons/ui-icon'
|
||||||
import styles from './split-divider.module.scss'
|
import styles from './split-divider.module.scss'
|
||||||
import React, { useMemo } from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import { Button } from 'react-bootstrap'
|
import { Button } from 'react-bootstrap'
|
||||||
import { ArrowLeftRight as IconArrowLeftRight } from 'react-bootstrap-icons'
|
import {
|
||||||
import { ArrowLeft as IconArrowLeft } from 'react-bootstrap-icons'
|
ArrowLeft as IconArrowLeft,
|
||||||
import { ArrowRight as IconArrowRight } from 'react-bootstrap-icons'
|
ArrowLeftRight as IconArrowLeftRight,
|
||||||
|
ArrowRight as IconArrowRight
|
||||||
|
} from 'react-bootstrap-icons'
|
||||||
|
|
||||||
export enum DividerButtonsShift {
|
export enum DividerButtonsShift {
|
||||||
SHIFT_TO_LEFT = 'shift-left',
|
SHIFT_TO_LEFT = 'shift-left',
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { ShowIf } from '../../common/show-if/show-if'
|
||||||
import { useKeyboardShortcuts } from './hooks/use-keyboard-shortcuts'
|
import { useKeyboardShortcuts } from './hooks/use-keyboard-shortcuts'
|
||||||
import { DividerButtonsShift, SplitDivider } from './split-divider/split-divider'
|
import { DividerButtonsShift, SplitDivider } from './split-divider/split-divider'
|
||||||
import styles from './splitter.module.scss'
|
import styles from './splitter.module.scss'
|
||||||
import type { ReactElement, TouchEvent, MouseEvent } from 'react'
|
import type { MouseEvent, ReactElement, TouchEvent } from 'react'
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
|
||||||
export interface SplitterProps {
|
export interface SplitterProps {
|
||||||
|
|
|
@ -13,9 +13,7 @@ import styles from './entry-menu.module.scss'
|
||||||
import { RemoveNoteEntryItem } from './remove-note-entry-item'
|
import { RemoveNoteEntryItem } from './remove-note-entry-item'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Dropdown } from 'react-bootstrap'
|
import { Dropdown } from 'react-bootstrap'
|
||||||
import { Cloud as IconCloud } from 'react-bootstrap-icons'
|
import { Cloud as IconCloud, Laptop as IconLaptop, ThreeDots as IconThreeDots } from 'react-bootstrap-icons'
|
||||||
import { Laptop as IconLaptop } from 'react-bootstrap-icons'
|
|
||||||
import { ThreeDots as IconThreeDots } from 'react-bootstrap-icons'
|
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
export interface EntryMenuProps {
|
export interface EntryMenuProps {
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { useBooleanState } from '../../../hooks/common/use-boolean-state'
|
import { useBooleanState } from '../../../hooks/common/use-boolean-state'
|
||||||
|
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
|
||||||
import { deleteAllHistoryEntries } from '../../../redux/history/methods'
|
import { deleteAllHistoryEntries } from '../../../redux/history/methods'
|
||||||
import { cypressId } from '../../../utils/cypress-attribute'
|
import { cypressId } from '../../../utils/cypress-attribute'
|
||||||
import { UiIcon } from '../../common/icons/ui-icon'
|
import { UiIcon } from '../../common/icons/ui-icon'
|
||||||
|
@ -13,14 +14,13 @@ import { useSafeRefreshHistoryStateCallback } from './hooks/use-safe-refresh-his
|
||||||
import React, { Fragment, useCallback } from 'react'
|
import React, { Fragment, useCallback } from 'react'
|
||||||
import { Button } from 'react-bootstrap'
|
import { Button } from 'react-bootstrap'
|
||||||
import { Trash as IconTrash } from 'react-bootstrap-icons'
|
import { Trash as IconTrash } from 'react-bootstrap-icons'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans } from 'react-i18next'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a button to clear the complete history of the user.
|
* Renders a button to clear the complete history of the user.
|
||||||
* A confirmation modal will be presented to the user after clicking the button.
|
* A confirmation modal will be presented to the user after clicking the button.
|
||||||
*/
|
*/
|
||||||
export const ClearHistoryButton: React.FC = () => {
|
export const ClearHistoryButton: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
|
||||||
const [modalVisibility, showModal, closeModal] = useBooleanState()
|
const [modalVisibility, showModal, closeModal] = useBooleanState()
|
||||||
const { showErrorNotification } = useUiNotifications()
|
const { showErrorNotification } = useUiNotifications()
|
||||||
const safeRefreshHistoryState = useSafeRefreshHistoryStateCallback()
|
const safeRefreshHistoryState = useSafeRefreshHistoryStateCallback()
|
||||||
|
@ -33,13 +33,11 @@ export const ClearHistoryButton: React.FC = () => {
|
||||||
closeModal()
|
closeModal()
|
||||||
}, [closeModal, safeRefreshHistoryState, showErrorNotification])
|
}, [closeModal, safeRefreshHistoryState, showErrorNotification])
|
||||||
|
|
||||||
|
const buttonTitle = useTranslatedText('landing.history.toolbar.clear')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Button
|
<Button variant={'secondary'} title={buttonTitle} onClick={showModal} {...cypressId('history-clear-button')}>
|
||||||
variant={'secondary'}
|
|
||||||
title={t('landing.history.toolbar.clear') ?? undefined}
|
|
||||||
onClick={showModal}
|
|
||||||
{...cypressId('history-clear-button')}>
|
|
||||||
<UiIcon icon={IconTrash} />
|
<UiIcon icon={IconTrash} />
|
||||||
</Button>
|
</Button>
|
||||||
<DeletionModal
|
<DeletionModal
|
||||||
|
|
|
@ -3,21 +3,21 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
|
||||||
import { downloadHistory } from '../../../redux/history/methods'
|
import { downloadHistory } from '../../../redux/history/methods'
|
||||||
import { UiIcon } from '../../common/icons/ui-icon'
|
import { UiIcon } from '../../common/icons/ui-icon'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Button } from 'react-bootstrap'
|
import { Button } from 'react-bootstrap'
|
||||||
import { Download as IconDownload } from 'react-bootstrap-icons'
|
import { Download as IconDownload } from 'react-bootstrap-icons'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a button to export the history.
|
* Renders a button to export the history.
|
||||||
*/
|
*/
|
||||||
export const ExportHistoryButton: React.FC = () => {
|
export const ExportHistoryButton: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
const buttonTitle = useTranslatedText('landing.history.toolbar.export')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button variant={'secondary'} title={t('landing.history.toolbar.export') ?? undefined} onClick={downloadHistory}>
|
<Button variant={'secondary'} title={buttonTitle} onClick={downloadHistory}>
|
||||||
<UiIcon icon={IconDownload} />
|
<UiIcon icon={IconDownload} />
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,23 +3,22 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
|
||||||
import { UiIcon } from '../../common/icons/ui-icon'
|
import { UiIcon } from '../../common/icons/ui-icon'
|
||||||
import { useSafeRefreshHistoryStateCallback } from './hooks/use-safe-refresh-history-state'
|
import { useSafeRefreshHistoryStateCallback } from './hooks/use-safe-refresh-history-state'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Button } from 'react-bootstrap'
|
import { Button } from 'react-bootstrap'
|
||||||
import { ArrowRepeat as IconArrowRepeat } from 'react-bootstrap-icons'
|
import { ArrowRepeat as IconArrowRepeat } from 'react-bootstrap-icons'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the current history from the server.
|
* Fetches the current history from the server.
|
||||||
*/
|
*/
|
||||||
export const HistoryRefreshButton: React.FC = () => {
|
export const HistoryRefreshButton: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const refreshHistory = useSafeRefreshHistoryStateCallback()
|
const refreshHistory = useSafeRefreshHistoryStateCallback()
|
||||||
|
const buttonTitle = useTranslatedText('landing.history.toolbar.refresh')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button variant={'secondary'} title={t('landing.history.toolbar.refresh') ?? undefined} onClick={refreshHistory}>
|
<Button variant={'secondary'} title={buttonTitle} onClick={refreshHistory}>
|
||||||
<UiIcon icon={IconArrowRepeat} />
|
<UiIcon icon={IconArrowRepeat} />
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
*/
|
*/
|
||||||
import { HistoryEntryOrigin } from '../../../api/history/types'
|
import { HistoryEntryOrigin } from '../../../api/history/types'
|
||||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
||||||
|
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
|
||||||
import { importHistoryEntries, setHistoryEntries } from '../../../redux/history/methods'
|
import { importHistoryEntries, setHistoryEntries } from '../../../redux/history/methods'
|
||||||
import { UiIcon } from '../../common/icons/ui-icon'
|
import { UiIcon } from '../../common/icons/ui-icon'
|
||||||
import { ShowIf } from '../../common/show-if/show-if'
|
import { ShowIf } from '../../common/show-if/show-if'
|
||||||
|
@ -23,7 +24,6 @@ import { useSyncToolbarStateToUrlEffect } from './toolbar-context/use-sync-toolb
|
||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react'
|
||||||
import { Button, Col } from 'react-bootstrap'
|
import { Button, Col } from 'react-bootstrap'
|
||||||
import { CloudUpload as IconCloudUpload } from 'react-bootstrap-icons'
|
import { CloudUpload as IconCloudUpload } from 'react-bootstrap-icons'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
export enum ViewStateEnum {
|
export enum ViewStateEnum {
|
||||||
CARD,
|
CARD,
|
||||||
|
@ -34,7 +34,6 @@ export enum ViewStateEnum {
|
||||||
* Renders the toolbar for the history page that contains controls for filtering and sorting.
|
* Renders the toolbar for the history page that contains controls for filtering and sorting.
|
||||||
*/
|
*/
|
||||||
export const HistoryToolbar: React.FC = () => {
|
export const HistoryToolbar: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
|
||||||
const historyEntries = useApplicationState((state) => state.history)
|
const historyEntries = useApplicationState((state) => state.history)
|
||||||
const userExists = useApplicationState((state) => !!state.user)
|
const userExists = useApplicationState((state) => !!state.user)
|
||||||
const { showErrorNotification } = useUiNotifications()
|
const { showErrorNotification } = useUiNotifications()
|
||||||
|
@ -61,6 +60,8 @@ export const HistoryToolbar: React.FC = () => {
|
||||||
})
|
})
|
||||||
}, [userExists, historyEntries, showErrorNotification, safeRefreshHistoryState])
|
}, [userExists, historyEntries, showErrorNotification, safeRefreshHistoryState])
|
||||||
|
|
||||||
|
const uploadAllButtonTitle = useTranslatedText('landing.history.toolbar.uploadAll')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className={'d-flex flex-row'}>
|
<Col className={'d-flex flex-row'}>
|
||||||
<div className={'me-1 mb-1'}>
|
<div className={'me-1 mb-1'}>
|
||||||
|
@ -89,10 +90,7 @@ export const HistoryToolbar: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
<ShowIf condition={userExists}>
|
<ShowIf condition={userExists}>
|
||||||
<div className={'me-1 mb-1'}>
|
<div className={'me-1 mb-1'}>
|
||||||
<Button
|
<Button variant={'secondary'} title={uploadAllButtonTitle} onClick={onUploadAllToRemote}>
|
||||||
variant={'secondary'}
|
|
||||||
title={t('landing.history.toolbar.uploadAll') ?? undefined}
|
|
||||||
onClick={onUploadAllToRemote}>
|
|
||||||
<UiIcon icon={IconCloudUpload} />
|
<UiIcon icon={IconCloudUpload} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,21 +3,19 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
|
||||||
import { cypressId } from '../../../utils/cypress-attribute'
|
import { cypressId } from '../../../utils/cypress-attribute'
|
||||||
import { UiIcon } from '../../common/icons/ui-icon'
|
import { UiIcon } from '../../common/icons/ui-icon'
|
||||||
import { ViewStateEnum } from './history-toolbar'
|
import { ViewStateEnum } from './history-toolbar'
|
||||||
import { useHistoryToolbarState } from './toolbar-context/use-history-toolbar-state'
|
import { useHistoryToolbarState } from './toolbar-context/use-history-toolbar-state'
|
||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react'
|
||||||
import { Button, ToggleButtonGroup } from 'react-bootstrap'
|
import { Button, ToggleButtonGroup } from 'react-bootstrap'
|
||||||
import { StickyFill as IconStickyFill } from 'react-bootstrap-icons'
|
import { StickyFill as IconStickyFill, Table as IconTable } from 'react-bootstrap-icons'
|
||||||
import { Table as IconTable } from 'react-bootstrap-icons'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggles the view mode of the history entries between list and card view.
|
* Toggles the view mode of the history entries between list and card view.
|
||||||
*/
|
*/
|
||||||
export const HistoryViewModeToggleButton: React.FC = () => {
|
export const HistoryViewModeToggleButton: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
|
||||||
const [historyToolbarState, setHistoryToolbarState] = useHistoryToolbarState()
|
const [historyToolbarState, setHistoryToolbarState] = useHistoryToolbarState()
|
||||||
|
|
||||||
const onViewStateChange = useCallback(
|
const onViewStateChange = useCallback(
|
||||||
|
@ -30,19 +28,25 @@ export const HistoryViewModeToggleButton: React.FC = () => {
|
||||||
[setHistoryToolbarState]
|
[setHistoryToolbarState]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const cardsButtonTitle = useTranslatedText('landing.history.toolbar.cards')
|
||||||
|
const tableButtonTitle = useTranslatedText('landing.history.toolbar.table')
|
||||||
|
|
||||||
|
const onCardsButtonClick = useCallback(() => onViewStateChange(ViewStateEnum.CARD), [onViewStateChange])
|
||||||
|
const onTableButtonClick = useCallback(() => onViewStateChange(ViewStateEnum.TABLE), [onViewStateChange])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToggleButtonGroup type='radio' name='options' dir='auto' className={'button-height'} onChange={onViewStateChange}>
|
<ToggleButtonGroup type='radio' name='options' dir='auto' className={'button-height'} onChange={onViewStateChange}>
|
||||||
<Button
|
<Button
|
||||||
title={t('landing.history.toolbar.cards') ?? undefined}
|
title={cardsButtonTitle}
|
||||||
variant={historyToolbarState.viewState === ViewStateEnum.CARD ? 'secondary' : 'outline-secondary'}
|
variant={historyToolbarState.viewState === ViewStateEnum.CARD ? 'secondary' : 'outline-secondary'}
|
||||||
onClick={() => onViewStateChange(ViewStateEnum.CARD)}>
|
onClick={onCardsButtonClick}>
|
||||||
<UiIcon icon={IconStickyFill} className={'fa-fix-line-height'} />
|
<UiIcon icon={IconStickyFill} className={'fa-fix-line-height'} />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
{...cypressId('history-mode-table')}
|
{...cypressId('history-mode-table')}
|
||||||
variant={historyToolbarState.viewState === ViewStateEnum.TABLE ? 'secondary' : 'outline-secondary'}
|
variant={historyToolbarState.viewState === ViewStateEnum.TABLE ? 'secondary' : 'outline-secondary'}
|
||||||
title={t('landing.history.toolbar.table') ?? undefined}
|
title={tableButtonTitle}
|
||||||
onClick={() => onViewStateChange(ViewStateEnum.TABLE)}>
|
onClick={onTableButtonClick}>
|
||||||
<UiIcon icon={IconTable} className={'fa-fix-line-height'} />
|
<UiIcon icon={IconTable} className={'fa-fix-line-height'} />
|
||||||
</Button>
|
</Button>
|
||||||
</ToggleButtonGroup>
|
</ToggleButtonGroup>
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
import type { HistoryEntryWithOrigin } from '../../../api/history/types'
|
import type { HistoryEntryWithOrigin } from '../../../api/history/types'
|
||||||
import { HistoryEntryOrigin } from '../../../api/history/types'
|
import { HistoryEntryOrigin } from '../../../api/history/types'
|
||||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
||||||
|
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
|
||||||
import { convertV1History, importHistoryEntries, mergeHistoryEntries } from '../../../redux/history/methods'
|
import { convertV1History, importHistoryEntries, mergeHistoryEntries } from '../../../redux/history/methods'
|
||||||
import type { HistoryExportJson, V1HistoryEntry } from '../../../redux/history/types'
|
import type { HistoryExportJson, V1HistoryEntry } from '../../../redux/history/types'
|
||||||
import { cypressId } from '../../../utils/cypress-attribute'
|
import { cypressId } from '../../../utils/cypress-attribute'
|
||||||
|
@ -15,13 +16,11 @@ import { useSafeRefreshHistoryStateCallback } from './hooks/use-safe-refresh-his
|
||||||
import React, { useCallback, useRef, useState } from 'react'
|
import React, { useCallback, useRef, useState } from 'react'
|
||||||
import { Button } from 'react-bootstrap'
|
import { Button } from 'react-bootstrap'
|
||||||
import { Upload as IconUpload } from 'react-bootstrap-icons'
|
import { Upload as IconUpload } from 'react-bootstrap-icons'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Button that lets the user select a history JSON file and uploads imports that into the history.
|
* Button that lets the user select a history JSON file and uploads imports that into the history.
|
||||||
*/
|
*/
|
||||||
export const ImportHistoryButton: React.FC = () => {
|
export const ImportHistoryButton: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
|
||||||
const userExists = useApplicationState((state) => !!state.user)
|
const userExists = useApplicationState((state) => !!state.user)
|
||||||
const historyState = useApplicationState((state) => state.history)
|
const historyState = useApplicationState((state) => state.history)
|
||||||
const uploadInput = useRef<HTMLInputElement>(null)
|
const uploadInput = useRef<HTMLInputElement>(null)
|
||||||
|
@ -117,6 +116,8 @@ export const ImportHistoryButton: React.FC = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const buttonTitle = useTranslatedText('landing.history.toolbar.import')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
|
@ -129,7 +130,7 @@ export const ImportHistoryButton: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant={'secondary'}
|
variant={'secondary'}
|
||||||
title={t('landing.history.toolbar.import') ?? undefined}
|
title={buttonTitle}
|
||||||
onClick={onUploadButtonClick}
|
onClick={onUploadButtonClick}
|
||||||
{...cypressId('import-history-file-button')}>
|
{...cypressId('import-history-file-button')}>
|
||||||
<UiIcon icon={IconUpload} />
|
<UiIcon icon={IconUpload} />
|
||||||
|
|
|
@ -4,16 +4,15 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { useOnInputChange } from '../../../hooks/common/use-on-input-change'
|
import { useOnInputChange } from '../../../hooks/common/use-on-input-change'
|
||||||
|
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
|
||||||
import { useHistoryToolbarState } from './toolbar-context/use-history-toolbar-state'
|
import { useHistoryToolbarState } from './toolbar-context/use-history-toolbar-state'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { FormControl } from 'react-bootstrap'
|
import { FormControl } from 'react-bootstrap'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A text input that is used to filter history entries for specific keywords.
|
* A text input that is used to filter history entries for specific keywords.
|
||||||
*/
|
*/
|
||||||
export const KeywordSearchInput: React.FC = () => {
|
export const KeywordSearchInput: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
|
||||||
const [historyToolbarState, setHistoryToolbarState] = useHistoryToolbarState()
|
const [historyToolbarState, setHistoryToolbarState] = useHistoryToolbarState()
|
||||||
|
|
||||||
const onChange = useOnInputChange((search) => {
|
const onChange = useOnInputChange((search) => {
|
||||||
|
@ -23,10 +22,12 @@ export const KeywordSearchInput: React.FC = () => {
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const searchKeywordsText = useTranslatedText('landing.history.toolbar.searchKeywords')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormControl
|
<FormControl
|
||||||
placeholder={t('landing.history.toolbar.searchKeywords') ?? undefined}
|
placeholder={searchKeywordsText}
|
||||||
aria-label={t('landing.history.toolbar.searchKeywords') ?? undefined}
|
aria-label={searchKeywordsText}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
value={historyToolbarState.search}
|
value={historyToolbarState.search}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -4,17 +4,16 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
||||||
|
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
|
||||||
import { useHistoryToolbarState } from './toolbar-context/use-history-toolbar-state'
|
import { useHistoryToolbarState } from './toolbar-context/use-history-toolbar-state'
|
||||||
import React, { useCallback, useMemo } from 'react'
|
import React, { useCallback, useMemo } from 'react'
|
||||||
import { Typeahead } from 'react-bootstrap-typeahead'
|
import { Typeahead } from 'react-bootstrap-typeahead'
|
||||||
import type { Option } from 'react-bootstrap-typeahead/types/types'
|
import type { Option } from 'react-bootstrap-typeahead/types/types'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders an input field that filters history entries by selected tags.
|
* Renders an input field that filters history entries by selected tags.
|
||||||
*/
|
*/
|
||||||
export const TagSelectionInput: React.FC = () => {
|
export const TagSelectionInput: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
|
||||||
const [historyToolbarState, setHistoryToolbarState] = useHistoryToolbarState()
|
const [historyToolbarState, setHistoryToolbarState] = useHistoryToolbarState()
|
||||||
|
|
||||||
const historyEntries = useApplicationState((state) => state.history)
|
const historyEntries = useApplicationState((state) => state.history)
|
||||||
|
@ -37,12 +36,13 @@ export const TagSelectionInput: React.FC = () => {
|
||||||
[setHistoryToolbarState]
|
[setHistoryToolbarState]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const placeholderText = useTranslatedText('landing.history.toolbar.selectTags')
|
||||||
return (
|
return (
|
||||||
<Typeahead
|
<Typeahead
|
||||||
id={'tagsSelection'}
|
id={'tagsSelection'}
|
||||||
options={tags}
|
options={tags}
|
||||||
multiple={true}
|
multiple={true}
|
||||||
placeholder={t('landing.history.toolbar.selectTags') ?? undefined}
|
placeholder={placeholderText}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
selected={historyToolbarState.selectedTags}
|
selected={historyToolbarState.selectedTags}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -6,9 +6,7 @@
|
||||||
import { IconButton } from '../../common/icon-button/icon-button'
|
import { IconButton } from '../../common/icon-button/icon-button'
|
||||||
import React, { useCallback, useMemo } from 'react'
|
import React, { useCallback, useMemo } from 'react'
|
||||||
import type { ButtonProps } from 'react-bootstrap'
|
import type { ButtonProps } from 'react-bootstrap'
|
||||||
import { SortAlphaDown as IconSortAlphaDown } from 'react-bootstrap-icons'
|
import { SortAlphaDown as IconSortAlphaDown, SortAlphaUp as IconSortAlphaUp, X as IconX } from 'react-bootstrap-icons'
|
||||||
import { SortAlphaUp as IconSortAlphaUp } from 'react-bootstrap-icons'
|
|
||||||
import { X as IconX } from 'react-bootstrap-icons'
|
|
||||||
|
|
||||||
export enum SortModeEnum {
|
export enum SortModeEnum {
|
||||||
up = 1,
|
up = 1,
|
||||||
|
|
|
@ -8,9 +8,7 @@ import { IconDiscourse } from '../../common/icons/additional/icon-discourse'
|
||||||
import { IconMatrixOrg } from '../../common/icons/additional/icon-matrix-org'
|
import { IconMatrixOrg } from '../../common/icons/additional/icon-matrix-org'
|
||||||
import { ExternalLink } from '../../common/links/external-link'
|
import { ExternalLink } from '../../common/links/external-link'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Github as IconGithub } from 'react-bootstrap-icons'
|
import { Github as IconGithub, Globe as IconGlobe, Mastodon as IconMastodon } from 'react-bootstrap-icons'
|
||||||
import { Globe as IconGlobe } from 'react-bootstrap-icons'
|
|
||||||
import { Mastodon as IconMastodon } from 'react-bootstrap-icons'
|
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
|
||||||
import { cypressId } from '../../../utils/cypress-attribute'
|
import { cypressId } from '../../../utils/cypress-attribute'
|
||||||
import { useFrontendConfig } from '../../common/frontend-config-context/use-frontend-config'
|
import { useFrontendConfig } from '../../common/frontend-config-context/use-frontend-config'
|
||||||
import { ShowIf } from '../../common/show-if/show-if'
|
import { ShowIf } from '../../common/show-if/show-if'
|
||||||
|
@ -12,7 +13,7 @@ import Link from 'next/link'
|
||||||
import React, { useMemo } from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import { Button } from 'react-bootstrap'
|
import { Button } from 'react-bootstrap'
|
||||||
import type { ButtonProps } from 'react-bootstrap/Button'
|
import type { ButtonProps } from 'react-bootstrap/Button'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans } from 'react-i18next'
|
||||||
|
|
||||||
export type SignInButtonProps = Omit<ButtonProps, 'href'>
|
export type SignInButtonProps = Omit<ButtonProps, 'href'>
|
||||||
|
|
||||||
|
@ -24,7 +25,6 @@ export type SignInButtonProps = Omit<ButtonProps, 'href'>
|
||||||
* @param props Further props inferred from the common button component.
|
* @param props Further props inferred from the common button component.
|
||||||
*/
|
*/
|
||||||
export const SignInButton: React.FC<SignInButtonProps> = ({ variant, ...props }) => {
|
export const SignInButton: React.FC<SignInButtonProps> = ({ variant, ...props }) => {
|
||||||
const { t } = useTranslation()
|
|
||||||
const authProviders = useFrontendConfig().authProviders
|
const authProviders = useFrontendConfig().authProviders
|
||||||
|
|
||||||
const loginLink = useMemo(() => {
|
const loginLink = useMemo(() => {
|
||||||
|
@ -35,15 +35,12 @@ export const SignInButton: React.FC<SignInButtonProps> = ({ variant, ...props })
|
||||||
}
|
}
|
||||||
return '/login'
|
return '/login'
|
||||||
}, [authProviders])
|
}, [authProviders])
|
||||||
|
const buttonTitle = useTranslatedText('login.signIn')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ShowIf condition={authProviders.length > 0}>
|
<ShowIf condition={authProviders.length > 0}>
|
||||||
<Link href={loginLink} passHref={true}>
|
<Link href={loginLink} passHref={true}>
|
||||||
<Button
|
<Button title={buttonTitle} {...cypressId('sign-in-button')} variant={variant || 'success'} {...props}>
|
||||||
title={t('login.signIn') ?? undefined}
|
|
||||||
{...cypressId('sign-in-button')}
|
|
||||||
variant={variant || 'success'}
|
|
||||||
{...props}>
|
|
||||||
<Trans i18nKey='login.signIn' />
|
<Trans i18nKey='login.signIn' />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
|
||||||
import type { CommonModalProps } from '../../common/modals/common-modal'
|
import type { CommonModalProps } from '../../common/modals/common-modal'
|
||||||
import { CommonModal } from '../../common/modals/common-modal'
|
import { CommonModal } from '../../common/modals/common-modal'
|
||||||
import { EditorSettingsTabContent } from './editor/editor-settings-tab-content'
|
import { EditorSettingsTabContent } from './editor/editor-settings-tab-content'
|
||||||
|
@ -10,7 +11,6 @@ import { GlobalSettingsTabContent } from './global/global-settings-tab-content'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Modal, Tab, Tabs } from 'react-bootstrap'
|
import { Modal, Tab, Tabs } from 'react-bootstrap'
|
||||||
import { Gear as IconGear } from 'react-bootstrap-icons'
|
import { Gear as IconGear } from 'react-bootstrap-icons'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows global and scope specific settings
|
* Shows global and scope specific settings
|
||||||
|
@ -19,7 +19,8 @@ import { useTranslation } from 'react-i18next'
|
||||||
* @param onHide callback that is executed if the modal should be closed
|
* @param onHide callback that is executed if the modal should be closed
|
||||||
*/
|
*/
|
||||||
export const SettingsModal: React.FC<CommonModalProps> = ({ show, onHide }) => {
|
export const SettingsModal: React.FC<CommonModalProps> = ({ show, onHide }) => {
|
||||||
const { t } = useTranslation()
|
const globalLabelTitle = useTranslatedText('settings.global.label')
|
||||||
|
const editorLabelTitle = useTranslatedText('settings.editor.label')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommonModal
|
<CommonModal
|
||||||
|
@ -31,10 +32,10 @@ export const SettingsModal: React.FC<CommonModalProps> = ({ show, onHide }) => {
|
||||||
showCloseButton={true}>
|
showCloseButton={true}>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<Tabs navbar={false} variant={'tabs'} defaultActiveKey={'global'}>
|
<Tabs navbar={false} variant={'tabs'} defaultActiveKey={'global'}>
|
||||||
<Tab title={t('settings.global.label')} eventKey={'global'}>
|
<Tab title={globalLabelTitle} eventKey={'global'}>
|
||||||
<GlobalSettingsTabContent />
|
<GlobalSettingsTabContent />
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab title={t('settings.editor.label')} eventKey={'editor'}>
|
<Tab title={editorLabelTitle} eventKey={'editor'}>
|
||||||
<EditorSettingsTabContent />
|
<EditorSettingsTabContent />
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
|
@ -3,11 +3,12 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
import { useTranslatedText } from '../../../../hooks/common/use-translated-text'
|
||||||
import type { PropsWithDataTestId } from '../../../../utils/test-id'
|
import type { PropsWithDataTestId } from '../../../../utils/test-id'
|
||||||
import React, { useCallback, useMemo } from 'react'
|
import React, { useCallback } from 'react'
|
||||||
import type { ButtonProps } from 'react-bootstrap'
|
import type { ButtonProps } from 'react-bootstrap'
|
||||||
import { Button } from 'react-bootstrap'
|
import { Button } from 'react-bootstrap'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans } from 'react-i18next'
|
||||||
|
|
||||||
type DarkModeToggleButtonProps = Omit<ButtonProps, 'onSelect'> &
|
type DarkModeToggleButtonProps = Omit<ButtonProps, 'onSelect'> &
|
||||||
PropsWithDataTestId & {
|
PropsWithDataTestId & {
|
||||||
|
@ -36,9 +37,7 @@ export const SettingsToggleButton = ({
|
||||||
value,
|
value,
|
||||||
...props
|
...props
|
||||||
}: DarkModeToggleButtonProps) => {
|
}: DarkModeToggleButtonProps) => {
|
||||||
const { t } = useTranslation()
|
const title = useTranslatedText(i18nKeyTooltip)
|
||||||
|
|
||||||
const title = useMemo(() => t(i18nKeyTooltip), [i18nKeyTooltip, t])
|
|
||||||
|
|
||||||
const onChange = useCallback(() => {
|
const onChange = useCallback(() => {
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
|
|
|
@ -3,10 +3,10 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
import { useTranslatedText } from '../../../../hooks/common/use-translated-text'
|
||||||
import type { AuthFieldProps } from './fields'
|
import type { AuthFieldProps } from './fields'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Form } from 'react-bootstrap'
|
import { Form } from 'react-bootstrap'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders an input field for the password of a user.
|
* Renders an input field for the password of a user.
|
||||||
|
@ -15,7 +15,7 @@ import { useTranslation } from 'react-i18next'
|
||||||
* @param invalid True when the entered password is invalid, false otherwise.
|
* @param invalid True when the entered password is invalid, false otherwise.
|
||||||
*/
|
*/
|
||||||
export const PasswordField: React.FC<AuthFieldProps> = ({ onChange, invalid }) => {
|
export const PasswordField: React.FC<AuthFieldProps> = ({ onChange, invalid }) => {
|
||||||
const { t } = useTranslation()
|
const placeholderText = useTranslatedText('login.auth.password')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
|
@ -23,7 +23,7 @@ export const PasswordField: React.FC<AuthFieldProps> = ({ onChange, invalid }) =
|
||||||
isInvalid={invalid}
|
isInvalid={invalid}
|
||||||
type='password'
|
type='password'
|
||||||
size='sm'
|
size='sm'
|
||||||
placeholder={t('login.auth.password') ?? undefined}
|
placeholder={placeholderText}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
autoComplete='current-password'
|
autoComplete='current-password'
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -9,14 +9,16 @@ import { Logger } from '../../../../utils/logger'
|
||||||
import { IconGitlab } from '../../../common/icons/additional/icon-gitlab'
|
import { IconGitlab } from '../../../common/icons/additional/icon-gitlab'
|
||||||
import styles from '../via-one-click.module.scss'
|
import styles from '../via-one-click.module.scss'
|
||||||
import type { Icon } from 'react-bootstrap-icons'
|
import type { Icon } from 'react-bootstrap-icons'
|
||||||
import { Dropbox as IconDropbox } from 'react-bootstrap-icons'
|
import {
|
||||||
import { Exclamation as IconExclamation } from 'react-bootstrap-icons'
|
Dropbox as IconDropbox,
|
||||||
import { Facebook as IconFacebook } from 'react-bootstrap-icons'
|
Exclamation as IconExclamation,
|
||||||
import { Github as IconGithub } from 'react-bootstrap-icons'
|
Facebook as IconFacebook,
|
||||||
import { Google as IconGoogle } from 'react-bootstrap-icons'
|
Github as IconGithub,
|
||||||
import { People as IconPeople } from 'react-bootstrap-icons'
|
Google as IconGoogle,
|
||||||
import { PersonRolodex as IconPersonRolodex } from 'react-bootstrap-icons'
|
People as IconPeople,
|
||||||
import { Twitter as IconTwitter } from 'react-bootstrap-icons'
|
PersonRolodex as IconPersonRolodex,
|
||||||
|
Twitter as IconTwitter
|
||||||
|
} from 'react-bootstrap-icons'
|
||||||
|
|
||||||
export interface OneClickMetadata {
|
export interface OneClickMetadata {
|
||||||
name: string
|
name: string
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { PasswordField } from './fields/password-field'
|
||||||
import { fetchAndSetUser } from './utils'
|
import { fetchAndSetUser } from './utils'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import type { FormEvent } from 'react'
|
import type { FormEvent } from 'react'
|
||||||
import React, { useCallback, useState } from 'react'
|
import React, { useCallback, useMemo, useState } from 'react'
|
||||||
import { Alert, Button, Card, Form } from 'react-bootstrap'
|
import { Alert, Button, Card, Form } from 'react-bootstrap'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
@ -48,11 +48,13 @@ export const ViaLocal: React.FC = () => {
|
||||||
const onUsernameChange = useLowercaseOnInputChange(setUsername)
|
const onUsernameChange = useLowercaseOnInputChange(setUsername)
|
||||||
const onPasswordChange = useOnInputChange(setPassword)
|
const onPasswordChange = useOnInputChange(setPassword)
|
||||||
|
|
||||||
|
const translationOptions = useMemo(() => ({ service: t('login.auth.username') }), [t])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className='mb-4'>
|
<Card className='mb-4'>
|
||||||
<Card.Body>
|
<Card.Body>
|
||||||
<Card.Title>
|
<Card.Title>
|
||||||
<Trans i18nKey='login.signInVia' values={{ service: t('login.auth.username') }} />
|
<Trans i18nKey='login.signInVia' values={translationOptions} />
|
||||||
</Card.Title>
|
</Card.Title>
|
||||||
<Form onSubmit={onLoginSubmit} className={'d-flex gap-3 flex-column'}>
|
<Form onSubmit={onLoginSubmit} className={'d-flex gap-3 flex-column'}>
|
||||||
<UsernameField onChange={onUsernameChange} isInvalid={!!error} value={username} />
|
<UsernameField onChange={onUsernameChange} isInvalid={!!error} value={username} />
|
||||||
|
|
|
@ -3,11 +3,12 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
import { useTranslatedText } from '../../../../hooks/common/use-translated-text'
|
||||||
import { cypressId } from '../../../../utils/cypress-attribute'
|
import { cypressId } from '../../../../utils/cypress-attribute'
|
||||||
import type { ChangeEvent } from 'react'
|
import type { ChangeEvent } from 'react'
|
||||||
import React, { useMemo } from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import { Form } from 'react-bootstrap'
|
import { Form } from 'react-bootstrap'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans } from 'react-i18next'
|
||||||
|
|
||||||
interface AccessTokenCreationFormLabelFieldProps extends AccessTokenCreationFormFieldProps {
|
interface AccessTokenCreationFormLabelFieldProps extends AccessTokenCreationFormFieldProps {
|
||||||
onChangeLabel: (event: ChangeEvent<HTMLInputElement>) => void
|
onChangeLabel: (event: ChangeEvent<HTMLInputElement>) => void
|
||||||
|
@ -23,11 +24,8 @@ export const AccessTokenCreationFormLabelField: React.FC<AccessTokenCreationForm
|
||||||
onChangeLabel,
|
onChangeLabel,
|
||||||
formValues
|
formValues
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const labelValid = useMemo(() => formValues.label.trim() !== '', [formValues])
|
||||||
|
const placeholderText = useTranslatedText('profile.accessTokens.label')
|
||||||
const labelValid = useMemo(() => {
|
|
||||||
return formValues.label.trim() !== ''
|
|
||||||
}, [formValues])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
|
@ -37,7 +35,7 @@ export const AccessTokenCreationFormLabelField: React.FC<AccessTokenCreationForm
|
||||||
<Form.Control
|
<Form.Control
|
||||||
type='text'
|
type='text'
|
||||||
size='sm'
|
size='sm'
|
||||||
placeholder={t('profile.accessTokens.label') ?? undefined}
|
placeholder={placeholderText}
|
||||||
value={formValues.label}
|
value={formValues.label}
|
||||||
onChange={onChangeLabel}
|
onChange={onChangeLabel}
|
||||||
isValid={labelValid}
|
isValid={labelValid}
|
||||||
|
|
|
@ -8,8 +8,7 @@ import { UiIcon } from '../../common/icons/ui-icon'
|
||||||
import { AccountDeletionModal } from './account-deletion-modal'
|
import { AccountDeletionModal } from './account-deletion-modal'
|
||||||
import React, { Fragment } from 'react'
|
import React, { Fragment } from 'react'
|
||||||
import { Button, Card, Row } from 'react-bootstrap'
|
import { Button, Card, Row } from 'react-bootstrap'
|
||||||
import { Trash as IconTrash } from 'react-bootstrap-icons'
|
import { CloudDownload as IconCloudDownload, Trash as IconTrash } from 'react-bootstrap-icons'
|
||||||
import { CloudDownload as IconCloudDownload } from 'react-bootstrap-icons'
|
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
import { TestMarkdownRenderer } from '../../../components/markdown-renderer/test-utils/test-markdown-renderer'
|
import { TestMarkdownRenderer } from '../../../components/markdown-renderer/test-utils/test-markdown-renderer'
|
||||||
import { BlockquoteExtraTagMarkdownExtension } from './blockquote-extra-tag-markdown-extension'
|
import { BlockquoteExtraTagMarkdownExtension } from './blockquote-extra-tag-markdown-extension'
|
||||||
import { screen, render } from '@testing-library/react'
|
import { render, screen } from '@testing-library/react'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
describe('blockquote extra tag', () => {
|
describe('blockquote extra tag', () => {
|
||||||
|
|
|
@ -8,8 +8,7 @@ import { codeFenceRegex } from '../../../components/editor-page/editor-pane/auto
|
||||||
import type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/_base-classes/markdown-renderer-extension'
|
import type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/_base-classes/markdown-renderer-extension'
|
||||||
import { AppExtension } from '../../_base-classes/app-extension'
|
import { AppExtension } from '../../_base-classes/app-extension'
|
||||||
import { HighlightedCodeMarkdownExtension } from './highlighted-code-markdown-extension'
|
import { HighlightedCodeMarkdownExtension } from './highlighted-code-markdown-extension'
|
||||||
import type { CompletionSource } from '@codemirror/autocomplete'
|
import type { CompletionContext, CompletionResult, CompletionSource } from '@codemirror/autocomplete'
|
||||||
import type { CompletionContext, CompletionResult } from '@codemirror/autocomplete'
|
|
||||||
import { languages } from '@codemirror/language-data'
|
import { languages } from '@codemirror/language-data'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import * as HighlightedCodeModule from '../../../components/common/highlighted-code/highlighted-code'
|
|
||||||
import type { HighlightedCodeProps } from '../../../components/common/highlighted-code/highlighted-code'
|
import type { HighlightedCodeProps } from '../../../components/common/highlighted-code/highlighted-code'
|
||||||
|
import * as HighlightedCodeModule from '../../../components/common/highlighted-code/highlighted-code'
|
||||||
import { TestMarkdownRenderer } from '../../../components/markdown-renderer/test-utils/test-markdown-renderer'
|
import { TestMarkdownRenderer } from '../../../components/markdown-renderer/test-utils/test-markdown-renderer'
|
||||||
import { mockI18n } from '../../../test-utils/mock-i18n'
|
import { mockI18n } from '../../../test-utils/mock-i18n'
|
||||||
import { HighlightedCodeMarkdownExtension } from './highlighted-code-markdown-extension'
|
import { HighlightedCodeMarkdownExtension } from './highlighted-code-markdown-extension'
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import * as ClickShieldModule from '../../../components/markdown-renderer/replace-components/click-shield/click-shield'
|
|
||||||
import type { ClickShieldProps } from '../../../components/markdown-renderer/replace-components/click-shield/click-shield'
|
import type { ClickShieldProps } from '../../../components/markdown-renderer/replace-components/click-shield/click-shield'
|
||||||
|
import * as ClickShieldModule from '../../../components/markdown-renderer/replace-components/click-shield/click-shield'
|
||||||
import { AsciinemaFrame } from './asciinema-frame'
|
import { AsciinemaFrame } from './asciinema-frame'
|
||||||
import { render } from '@testing-library/react'
|
import { render } from '@testing-library/react'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
|
@ -49,7 +49,7 @@ export const VegaLiteChart: React.FC<CodeProps> = ({ code }) => {
|
||||||
SVG_ACTION: t('renderer.vega-lite.svg') ?? undefined
|
SVG_ACTION: t('renderer.vega-lite.svg') ?? undefined
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [code, vegaEmbed])
|
}, [code, vegaEmbed, t])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (renderingError) {
|
if (renderingError) {
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import * as ClickShieldModule from '../../../components/markdown-renderer/replace-components/click-shield/click-shield'
|
|
||||||
import type { ClickShieldProps } from '../../../components/markdown-renderer/replace-components/click-shield/click-shield'
|
import type { ClickShieldProps } from '../../../components/markdown-renderer/replace-components/click-shield/click-shield'
|
||||||
|
import * as ClickShieldModule from '../../../components/markdown-renderer/replace-components/click-shield/click-shield'
|
||||||
import { VimeoFrame } from './vimeo-frame'
|
import { VimeoFrame } from './vimeo-frame'
|
||||||
import { render } from '@testing-library/react'
|
import { render } from '@testing-library/react'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import * as ClickShieldModule from '../../../components/markdown-renderer/replace-components/click-shield/click-shield'
|
|
||||||
import type { ClickShieldProps } from '../../../components/markdown-renderer/replace-components/click-shield/click-shield'
|
import type { ClickShieldProps } from '../../../components/markdown-renderer/replace-components/click-shield/click-shield'
|
||||||
|
import * as ClickShieldModule from '../../../components/markdown-renderer/replace-components/click-shield/click-shield'
|
||||||
import { YouTubeFrame } from './youtube-frame'
|
import { YouTubeFrame } from './youtube-frame'
|
||||||
import { render } from '@testing-library/react'
|
import { render } from '@testing-library/react'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { useApplicationState } from './use-application-state'
|
import { useApplicationState } from './use-application-state'
|
||||||
|
import { useTranslatedText } from './use-translated-text'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the title of the note or a placeholder text, if no title is set.
|
* Retrieves the title of the note or a placeholder text, if no title is set.
|
||||||
|
@ -13,8 +13,7 @@ import { useTranslation } from 'react-i18next'
|
||||||
* @return The title of the note
|
* @return The title of the note
|
||||||
*/
|
*/
|
||||||
export const useNoteTitle = (): string => {
|
export const useNoteTitle = (): string => {
|
||||||
const { t } = useTranslation()
|
const untitledNote = useTranslatedText('editor.untitledNote')
|
||||||
const untitledNote = useMemo(() => t('editor.untitledNote'), [t])
|
|
||||||
const noteTitle = useApplicationState((state) => state.noteDetails.title)
|
const noteTitle = useApplicationState((state) => state.noteDetails.title)
|
||||||
|
|
||||||
return useMemo(() => (noteTitle === '' ? untitledNote : noteTitle), [noteTitle, untitledNote])
|
return useMemo(() => (noteTitle === '' ? untitledNote : noteTitle), [noteTitle, untitledNote])
|
||||||
|
|
20
frontend/src/hooks/common/use-translated-text.ts
Normal file
20
frontend/src/hooks/common/use-translated-text.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import type { TOptions } from 'i18next'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translates text and caches it.
|
||||||
|
*
|
||||||
|
* @param key The translation key
|
||||||
|
* @param tOptions Optional translation options
|
||||||
|
* @return the translated text
|
||||||
|
*/
|
||||||
|
export const useTranslatedText = (key: string, tOptions?: TOptions): string => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
return useMemo(() => (tOptions ? t(key, tOptions) : t(key)), [key, tOptions, t])
|
||||||
|
}
|
42
frontend/src/hooks/common/useTranslatedText.spec.ts
Normal file
42
frontend/src/hooks/common/useTranslatedText.spec.ts
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { useTranslatedText } from './use-translated-text'
|
||||||
|
import { renderHook } from '@testing-library/react'
|
||||||
|
import type { Namespace } from 'i18next'
|
||||||
|
import * as ReactI18NextModule from 'react-i18next'
|
||||||
|
import type { UseTranslationResponse } from 'react-i18next'
|
||||||
|
import { Mock } from 'ts-mockery'
|
||||||
|
|
||||||
|
jest.mock('react-i18next')
|
||||||
|
|
||||||
|
describe('useTranslatedText', () => {
|
||||||
|
const mockTranslation = 'mockTranslation'
|
||||||
|
const mockKey = 'mockKey'
|
||||||
|
let translateFunction: jest.Mock
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
translateFunction = jest.fn(() => mockTranslation)
|
||||||
|
const useTranslateMock = Mock.of<UseTranslationResponse<Namespace, unknown>>({
|
||||||
|
t: translateFunction
|
||||||
|
})
|
||||||
|
jest.spyOn(ReactI18NextModule, 'useTranslation').mockReturnValue(useTranslateMock)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('translates text', () => {
|
||||||
|
const { result } = renderHook(() => useTranslatedText(mockKey))
|
||||||
|
|
||||||
|
expect(result.current).toBe(mockTranslation)
|
||||||
|
expect(translateFunction).toBeCalledWith(mockKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('translates text with options', () => {
|
||||||
|
const mockOptions = {}
|
||||||
|
const { result } = renderHook(() => useTranslatedText(mockKey, mockOptions))
|
||||||
|
|
||||||
|
expect(result.current).toBe(mockTranslation)
|
||||||
|
expect(translateFunction).toBeCalledWith(mockKey, mockOptions)
|
||||||
|
})
|
||||||
|
})
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { store } from '..'
|
import { store } from '..'
|
||||||
import type { SetRealtimeSyncStatusAction, SetRealtimeUsersAction, SetRealtimeConnectionStatusAction } from './types'
|
import type { SetRealtimeConnectionStatusAction, SetRealtimeSyncStatusAction, SetRealtimeUsersAction } from './types'
|
||||||
import { RealtimeStatusActionType } from './types'
|
import { RealtimeStatusActionType } from './types'
|
||||||
import type { RealtimeUser } from '@hedgedoc/commons'
|
import type { RealtimeUser } from '@hedgedoc/commons'
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import type { RealtimeStatusActions, RealtimeStatus } from './types'
|
import type { RealtimeStatus, RealtimeStatusActions } from './types'
|
||||||
import { RealtimeStatusActionType } from './types'
|
import { RealtimeStatusActionType } from './types'
|
||||||
import type { Reducer } from 'redux'
|
import type { Reducer } from 'redux'
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { useBaseUrl } from '../hooks/common/use-base-url'
|
import { useBaseUrl } from '../hooks/common/use-base-url'
|
||||||
import React, { Fragment, useMemo } from 'react'
|
|
||||||
import type { PropsWithChildren } from 'react'
|
import type { PropsWithChildren } from 'react'
|
||||||
|
import React, { Fragment, useMemo } from 'react'
|
||||||
|
|
||||||
export interface ExpectedOriginBoundaryProps {
|
export interface ExpectedOriginBoundaryProps {
|
||||||
currentOrigin?: string
|
currentOrigin?: string
|
||||||
|
|
Loading…
Reference in a new issue