Add prettier for codestyle and re-format everything (#1294)

This commit is contained in:
Erik Michelson 2021-06-06 23:14:00 +02:00 committed by GitHub
parent 8b78154075
commit 0aae1f70d2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
319 changed files with 4809 additions and 3936 deletions

View file

@ -39,3 +39,31 @@ jobs:
run: yarn install --frozen-lockfile --prefer-offline
- name: Lint code
run: yarn lint
format:
runs-on: ubuntu-latest
name: Checks codestyle of all .ts and .tsx files
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Cache node_modules
uses: actions/cache@v2
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-16-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Set up NodeJS
uses: actions/setup-node@v2
with:
node-version: 16
- name: Install dependencies
run: yarn install --frozen-lockfile --prefer-offline
- name: Lint code
run: yarn format

1
.gitignore vendored
View file

@ -22,6 +22,7 @@
.idea
!.idea/dictionaries/hedgedoc.xml
!.idea/copyright
!.idea/prettier.xml
# misc
.DS_Store
.env.local

6
.idea/prettier.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PrettierConfiguration">
<option name="myRunOnReformat" value="true" />
</component>
</project>

1
.prettierignore Normal file
View file

@ -0,0 +1 @@
node_modules/

4
.prettierignore.license Normal file
View file

@ -0,0 +1,4 @@
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: CC0-1.0

View file

@ -118,6 +118,9 @@
"analyze": "cross-env ANALYZE=true yarn build:mock",
"test": "craco test",
"lint": "eslint --max-warnings=0 --ext .ts,.tsx src",
"lint:fix": "eslint --fix --ext .ts,.tsx src",
"format": "prettier -c \"src/**/*.{ts,tsx,js}\"",
"format:fix": "prettier -w \"src/**/*.{ts,tsx,js}\"",
"eject": "react-scripts eject",
"cy:open": "cypress open",
"cy:run:chrome": "cypress run --browser chrome",
@ -145,9 +148,21 @@
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"plugin:import/recommended",
"plugin:import/typescript"
"plugin:import/typescript",
"prettier"
]
},
"prettier": {
"parser": "typescript",
"singleQuote": true,
"jsxSingleQuote": true,
"semi": false,
"tabWidth": 2,
"trailingComma": "none",
"bracketSpacing": true,
"jsxBracketSameLine": true,
"arrowParens": "always"
},
"browserslist": {
"production": [
">0.2%",
@ -169,9 +184,11 @@
"cypress": "7.4.0",
"cypress-commands": "1.1.0",
"cypress-file-upload": "5.0.7",
"eslint-config-prettier": "8.3.0",
"eslint-plugin-chai-friendly": "0.7.1",
"eslint-plugin-cypress": "2.11.3",
"http-server": "0.12.3",
"prettier": "2.3.0",
"redux-devtools": "3.7.0",
"redux-devtools-extension": "2.13.9",
"ts-loader": "9.2.2",

View file

@ -12,5 +12,5 @@ export const getConfig = async (): Promise<Config> => {
...defaultFetchConfig
})
expectResponseCode(response)
return await response.json() as Promise<Config>
return (await response.json()) as Promise<Config>
}

View file

@ -5,27 +5,27 @@
*/
export interface Config {
allowAnonymous: boolean,
allowRegister: boolean,
authProviders: AuthProvidersState,
branding: BrandingConfig,
customAuthNames: CustomAuthNames,
useImageProxy: boolean,
specialUrls: SpecialUrls,
version: BackendVersion,
plantumlServer: string | null,
maxDocumentLength: number,
allowAnonymous: boolean
allowRegister: boolean
authProviders: AuthProvidersState
branding: BrandingConfig
customAuthNames: CustomAuthNames
useImageProxy: boolean
specialUrls: SpecialUrls
version: BackendVersion
plantumlServer: string | null
maxDocumentLength: number
iframeCommunication: iframeCommunicationConfig
}
export interface iframeCommunicationConfig {
editorOrigin: string,
editorOrigin: string
rendererOrigin: string
}
export interface BrandingConfig {
name: string,
logo: string,
name: string
logo: string
}
export interface BackendVersion {
@ -37,27 +37,27 @@ export interface BackendVersion {
}
export interface AuthProvidersState {
facebook: boolean,
github: boolean,
twitter: boolean,
gitlab: boolean,
dropbox: boolean,
ldap: boolean,
google: boolean,
saml: boolean,
oauth2: boolean,
internal: boolean,
openid: boolean,
facebook: boolean
github: boolean
twitter: boolean
gitlab: boolean
dropbox: boolean
ldap: boolean
google: boolean
saml: boolean
oauth2: boolean
internal: boolean
openid: boolean
}
export interface CustomAuthNames {
ldap: string;
oauth2: string;
saml: string;
ldap: string
oauth2: string
saml: string
}
export interface SpecialUrls {
privacy?: string,
termsOfUse?: string,
imprint?: string,
privacy?: string
termsOfUse?: string
imprint?: string
}

View file

@ -10,7 +10,7 @@ import { HistoryEntryDto, HistoryEntryPutDto, HistoryEntryUpdateDto } from './ty
export const getHistory = async (): Promise<HistoryEntryDto[]> => {
const response = await fetch(getApiUrl() + 'me/history')
expectResponseCode(response)
return await response.json() as Promise<HistoryEntryDto[]>
return (await response.json()) as Promise<HistoryEntryDto[]>
}
export const postHistory = async (entries: HistoryEntryPutDto[]): Promise<void> => {

View file

@ -16,7 +16,7 @@ export const getProxiedUrl = async (imageUrl: string): Promise<ImageProxyRespons
})
})
expectResponseCode(response)
return await response.json() as Promise<ImageProxyResponse>
return (await response.json()) as Promise<ImageProxyResponse>
}
export interface UploadedMedia {
@ -34,5 +34,5 @@ export const uploadFile = async (noteId: string, contentType: string, media: Blo
body: media
})
expectResponseCode(response, 201)
return await response.json() as Promise<UploadedMedia>
return (await response.json()) as Promise<UploadedMedia>
}

View file

@ -15,7 +15,7 @@ export const getNote = async (noteId: string): Promise<NoteDto> => {
...defaultFetchConfig
})
expectResponseCode(response)
return await response.json() as Promise<NoteDto>
return (await response.json()) as Promise<NoteDto>
}
export const deleteNote = async (noteId: string): Promise<void> => {

View file

@ -19,7 +19,7 @@ export const getRevision = async (noteId: string, timestamp: number): Promise<Re
...defaultFetchConfig
})
expectResponseCode(response)
const revisionData = await response.json() as Revision
const revisionData = (await response.json()) as Revision
revisionCache.put(cacheKey, revisionData)
return revisionData
}
@ -30,5 +30,5 @@ export const getAllRevisions = async (noteId: string): Promise<RevisionListEntry
...defaultFetchConfig
})
expectResponseCode(response)
return await response.json() as Promise<RevisionListEntry[]>
return (await response.json()) as Promise<RevisionListEntry[]>
}

View file

@ -12,7 +12,7 @@ export const getAccessTokenList = async (): Promise<AccessToken[]> => {
...defaultFetchConfig
})
expectResponseCode(response)
return await response.json() as AccessToken[]
return (await response.json()) as AccessToken[]
}
export const postNewAccessToken = async (label: string): Promise<AccessToken & AccessTokenSecret> => {
@ -22,7 +22,7 @@ export const postNewAccessToken = async (label: string): Promise<AccessToken & A
body: label
})
expectResponseCode(response)
return await response.json() as (AccessToken & AccessTokenSecret)
return (await response.json()) as AccessToken & AccessTokenSecret
}
export const deleteAccessToken = async (timestamp: number): Promise<void> => {

View file

@ -17,8 +17,10 @@ export const ApplicationLoader: React.FC = ({ children }) => {
const backendBaseUrl = useBackendBaseUrl()
const customizeAssetsUrl = useCustomizeAssetsUrl()
const setUpTasks = useCallback(() => createSetUpTaskList(frontendAssetsUrl, customizeAssetsUrl, backendBaseUrl),
[backendBaseUrl, customizeAssetsUrl, frontendAssetsUrl])
const setUpTasks = useCallback(
() => createSetUpTaskList(frontendAssetsUrl, customizeAssetsUrl, backendBaseUrl),
[backendBaseUrl, customizeAssetsUrl, frontendAssetsUrl]
)
const [failedTitle, setFailedTitle] = useState<string>('')
const [doneTasks, setDoneTasks] = useState<number>(0)
@ -26,15 +28,14 @@ export const ApplicationLoader: React.FC = ({ children }) => {
const runTask = useCallback(async (task: Promise<void>): Promise<void> => {
await task
setDoneTasks(prevDoneTasks => {
setDoneTasks((prevDoneTasks) => {
return prevDoneTasks + 1
})
}, [])
useEffect(() => {
for (const task of initTasks) {
runTask(task.task)
.catch((reason: Error) => {
runTask(task.task).catch((reason: Error) => {
console.error(reason)
setFailedTitle(task.name)
})
@ -46,8 +47,6 @@ export const ApplicationLoader: React.FC = ({ children }) => {
if (tasksAreRunning) {
return <LoadingScreen failedTitle={failedTitle} />
} else {
return <Suspense fallback={ (<LoadingScreen/>) }>
{ children }
</Suspense>
return <Suspense fallback={<LoadingScreen />}>{children}</Suspense>
}
}

View file

@ -13,7 +13,7 @@ import { fetchFrontendConfig } from './fetch-frontend-config'
const customDelay: () => Promise<void> = async () => {
if (window.localStorage.getItem('customDelay')) {
return new Promise(resolve => setTimeout(resolve, 5000))
return new Promise((resolve) => setTimeout(resolve, 5000))
} else {
return Promise.resolve()
}
@ -24,28 +24,39 @@ export interface InitTask {
task: Promise<void>
}
export const createSetUpTaskList = (frontendAssetsUrl: string, customizeAssetsUrl: string, backendBaseUrl: string): InitTask[] => {
export const createSetUpTaskList = (
frontendAssetsUrl: string,
customizeAssetsUrl: string,
backendBaseUrl: string
): InitTask[] => {
setApiUrl({
apiUrl: `${backendBaseUrl}api/private/`
})
return [{
return [
{
name: 'Load Translations',
task: setUpI18n(frontendAssetsUrl)
}, {
},
{
name: 'Load config',
task: fetchFrontendConfig()
}, {
},
{
name: 'Fetch user information',
task: fetchAndSetUser()
}, {
},
{
name: 'Banner',
task: fetchAndSetBanner(customizeAssetsUrl)
}, {
},
{
name: 'Load history state',
task: refreshHistoryState()
}, {
},
{
name: 'Add Delay',
task: customDelay()
}]
}
]
}

View file

@ -15,15 +15,16 @@ export interface LoadingScreenProps {
export const LoadingScreen: React.FC<LoadingScreenProps> = ({ failedTitle }) => {
return (
<div className="loader middle text-light overflow-hidden">
<div className="mb-3 text-light">
<div className='loader middle text-light overflow-hidden'>
<div className='mb-3 text-light'>
<span className={`d-block ${failedTitle ? 'animation-shake' : 'animation-jump'}`}>
<HedgeDocLogo size={HedgeDocLogoSize.BIG} />
</span>
</div>
<ShowIf condition={!!failedTitle}>
<Alert variant={'danger'}>
The task '{ failedTitle }' failed.<br/>
The task '{failedTitle}' failed.
<br />
For further information look into the browser console.
</Alert>
</ShowIf>

View file

@ -25,16 +25,16 @@ export const Branding: React.FC<BrandingProps> = ({ inline = false, delimiter =
<ShowIf condition={delimiter}>
<strong className={`mx-1 ${inline ? 'inline-size' : 'regular-size'}`}>@</strong>
</ShowIf>
{
branding.logo
? <img
{branding.logo ? (
<img
src={branding.logo}
alt={branding.name}
title={branding.name}
className={inline ? 'inline-size' : 'regular-size'}
/>
: branding.name
}
) : (
branding.name
)}
</ShowIf>
)
}

View file

@ -16,88 +16,68 @@ describe('Test caching functionality', () => {
it('initialize with right lifetime, no entry limit', () => {
const lifetime = 1000
const lifetimedCache = new Cache<string, string>(lifetime)
expect(lifetimedCache.entryLifetime)
.toEqual(lifetime)
expect(lifetimedCache.maxEntries)
.toEqual(0)
expect(lifetimedCache.entryLifetime).toEqual(lifetime)
expect(lifetimedCache.maxEntries).toEqual(0)
})
it('initialize with right lifetime, given entry limit', () => {
const lifetime = 1000
const maxEntries = 10
const limitedCache = new Cache<string, string>(lifetime, maxEntries)
expect(limitedCache.entryLifetime)
.toEqual(lifetime)
expect(limitedCache.maxEntries)
.toEqual(maxEntries)
expect(limitedCache.entryLifetime).toEqual(lifetime)
expect(limitedCache.maxEntries).toEqual(maxEntries)
})
it('entry exists after inserting', () => {
testCache.put('test', 123)
expect(testCache.has('test'))
.toBe(true)
expect(testCache.has('test')).toBe(true)
})
it('entry does not exist prior inserting', () => {
expect(testCache.has('test'))
.toBe(false)
expect(testCache.has('test')).toBe(false)
})
it('entry does expire', () => {
const shortLivingCache = new Cache<string, number>(2)
shortLivingCache.put('test', 123)
expect(shortLivingCache.has('test'))
.toBe(true)
expect(shortLivingCache.has('test')).toBe(true)
setTimeout(() => {
expect(shortLivingCache.has('test'))
.toBe(false)
expect(shortLivingCache.has('test')).toBe(false)
}, 2000)
})
it('entry value does not change', () => {
const testValue = Date.now()
testCache.put('test', testValue)
expect(testCache.get('test'))
.toEqual(testValue)
expect(testCache.get('test')).toEqual(testValue)
})
it('error is thrown on non-existent entry', () => {
const accessNonExistentEntry = () => {
testCache.get('test')
}
expect(accessNonExistentEntry)
.toThrow(Error)
expect(accessNonExistentEntry).toThrow(Error)
})
it('newer item replaces older item', () => {
testCache.put('test', 123)
testCache.put('test', 456)
expect(testCache.get('test'))
.toEqual(456)
expect(testCache.get('test')).toEqual(456)
})
it('entry limit is respected', () => {
const limitedCache = new Cache<string, number>(1000, 2)
limitedCache.put('first', 1)
expect(limitedCache.has('first'))
.toBe(true)
expect(limitedCache.has('second'))
.toBe(false)
expect(limitedCache.has('third'))
.toBe(false)
expect(limitedCache.has('first')).toBe(true)
expect(limitedCache.has('second')).toBe(false)
expect(limitedCache.has('third')).toBe(false)
limitedCache.put('second', 2)
expect(limitedCache.has('first'))
.toBe(true)
expect(limitedCache.has('second'))
.toBe(true)
expect(limitedCache.has('third'))
.toBe(false)
expect(limitedCache.has('first')).toBe(true)
expect(limitedCache.has('second')).toBe(true)
expect(limitedCache.has('third')).toBe(false)
limitedCache.put('third', 3)
expect(limitedCache.has('first'))
.toBe(false)
expect(limitedCache.has('second'))
.toBe(true)
expect(limitedCache.has('third'))
.toBe(true)
expect(limitedCache.has('first')).toBe(false)
expect(limitedCache.has('second')).toBe(true)
expect(limitedCache.has('third')).toBe(true)
})
})

View file

@ -27,7 +27,7 @@ export class Cache<K, V> {
return false
}
const entry = this.store.get(key)
return (!!entry && entry.entryCreated >= (Date.now() - this.entryLifetime * 1000))
return !!entry && entry.entryCreated >= Date.now() - this.entryLifetime * 1000
}
get(key: K): V {
@ -40,8 +40,7 @@ export class Cache<K, V> {
put(key: K, value: V): void {
if (this.maxEntries > 0 && this.store.size === this.maxEntries) {
this.store.delete(this.store.keys()
.next().value)
this.store.delete(this.store.keys().next().value)
}
this.store.set(key, {
entryCreated: Date.now(),

View file

@ -22,13 +22,14 @@ export const CopyOverlay: React.FC<CopyOverlayProps> = ({ content, clickComponen
const [tooltipId] = useState<string>(() => uuid())
const copyToClipboard = useCallback((content: string) => {
navigator.clipboard.writeText(content)
navigator.clipboard
.writeText(content)
.then(() => {
setError(false)
})
.catch(() => {
setError(true)
console.error('couldn\'t copy')
console.error("couldn't copy")
})
.finally(() => {
setShowCopiedTooltip(true)
@ -51,7 +52,7 @@ export const CopyOverlay: React.FC<CopyOverlayProps> = ({ content, clickComponen
}, [clickComponent, copyToClipboard, content])
return (
<Overlay target={ clickComponent } show={ showCopiedTooltip } placement="top">
<Overlay target={clickComponent} show={showCopiedTooltip} placement='top'>
{(props) => (
<Tooltip id={`copied_${tooltipId}`} {...props}>
<ShowIf condition={error}>

View file

@ -29,7 +29,11 @@ export const CopyToClipboardButton: React.FC<CopyToClipboardButtonProps> = ({
return (
<Fragment>
<Button ref={ button } size={ size } variant={ variant } title={ t('renderer.highlightCode.copyCode') }
<Button
ref={button}
size={size}
variant={variant}
title={t('renderer.highlightCode.copyCode')}
data-cy={props['data-cy']}>
<ForkAwesomeIcon icon='files-o' />
</Button>

View file

@ -22,11 +22,12 @@ export const CopyableField: React.FC<CopyableFieldProps> = ({ content, nativeSha
const copyButton = useRef<HTMLButtonElement>(null)
const doShareAction = useCallback(() => {
navigator.share({
navigator
.share({
text: content,
url: url
})
.catch(err => {
.catch((err) => {
console.error('Native sharing failed: ', err)
})
}, [content, url])
@ -35,16 +36,16 @@ export const CopyableField: React.FC<CopyableFieldProps> = ({ content, nativeSha
return (
<Fragment>
<InputGroup className="my-3">
<InputGroup className='my-3'>
<FormControl readOnly={true} className={'text-center'} value={content} />
<InputGroup.Append>
<Button variant="outline-secondary" ref={ copyButton } title={ 'Copy' }>
<Button variant='outline-secondary' ref={copyButton} title={'Copy'}>
<ForkAwesomeIcon icon='files-o' />
</Button>
</InputGroup.Append>
<ShowIf condition={!!nativeShareButton && sharingSupported}>
<InputGroup.Append>
<Button variant="outline-secondary" title={ 'Share' } onClick={ doShareAction }>
<Button variant='outline-secondary' title={'Share'} onClick={doShareAction}>
<ForkAwesomeIcon icon='share-alt' />
</Button>
</InputGroup.Append>

View file

@ -15,12 +15,16 @@ export interface ForkAwesomeIconProps {
stacked?: boolean
}
export const ForkAwesomeIcon: React.FC<ForkAwesomeIconProps> = ({ icon, fixedWidth = false, size, className, stacked = false }) => {
export const ForkAwesomeIcon: React.FC<ForkAwesomeIconProps> = ({
icon,
fixedWidth = false,
size,
className,
stacked = false
}) => {
const fixedWithClass = fixedWidth ? 'fa-fw' : ''
const sizeClass = size ? `-${ size }` : (stacked ? '-1x' : '')
const sizeClass = size ? `-${size}` : stacked ? '-1x' : ''
const stackClass = stacked ? '-stack' : ''
const extraClasses = `${className ?? ''} ${sizeClass || stackClass ? `fa${stackClass}${sizeClass}` : ''}`
return (
<i className={ `fa ${ fixedWithClass } fa-${ icon } ${ extraClasses }` }/>
)
return <i className={`fa ${fixedWithClass} fa-${icon} ${extraClasses}`} />
}

View file

@ -16,14 +16,12 @@ export interface ForkAwesomeStackProps {
export const ForkAwesomeStack: React.FC<ForkAwesomeStackProps> = ({ size, children }) => {
return (
<span className={`fa-stack ${size ? 'fa-' : ''}${size ?? ''}`}>
{
React.Children.map(children, (child) => {
{React.Children.map(children, (child) => {
if (!React.isValidElement<ForkAwesomeIconProps>(child)) {
return null
}
return <ForkAwesomeIcon {...child.props} stacked={true} />
})
}
})}
</span>
)
}

View file

@ -16,7 +16,7 @@ export enum HedgeDocLogoSize {
}
export interface HedgeDocLogoProps {
size?: HedgeDocLogoSize | number,
size?: HedgeDocLogoSize | number
logoType: HedgeDocLogoType
}

View file

@ -18,17 +18,23 @@ export interface IconButtonProps extends ButtonProps {
iconFixedWidth?: boolean
}
export const IconButton: React.FC<IconButtonProps> = ({ icon, children, iconFixedWidth = false, border = false, className, ...props }) => {
export const IconButton: React.FC<IconButtonProps> = ({
icon,
children,
iconFixedWidth = false,
border = false,
className,
...props
}) => {
return (
<Button { ...props }
<Button
{...props}
className={`btn-icon p-0 d-inline-flex align-items-stretch ${border ? 'with-border' : ''} ${className ?? ''}`}>
<span className="icon-part d-flex align-items-center">
<span className='icon-part d-flex align-items-center'>
<ForkAwesomeIcon icon={icon} fixedWidth={iconFixedWidth} className={'icon'} />
</span>
<ShowIf condition={!!children}>
<span className="text-part d-flex align-items-center">
{ children }
</span>
<span className='text-part d-flex align-items-center'>{children}</span>
</ShowIf>
</Button>
)

View file

@ -10,18 +10,19 @@ import { IconName } from '../fork-awesome/types'
import { ShowIf } from '../show-if/show-if'
import { LinkWithTextProps } from './types'
export const ExternalLink: React.FC<LinkWithTextProps> = ({ href, text, icon, id, className = 'text-light', title }) => {
export const ExternalLink: React.FC<LinkWithTextProps> = ({
href,
text,
icon,
id,
className = 'text-light',
title
}) => {
return (
<a href={ href }
target="_blank"
rel="noopener noreferrer"
id={ id }
className={ className }
title={ title }
dir='auto'
>
<a href={href} target='_blank' rel='noopener noreferrer' id={id} className={className} title={title} dir='auto'>
<ShowIf condition={!!icon}>
<ForkAwesomeIcon icon={ icon as IconName } fixedWidth={ true }/>&nbsp;
<ForkAwesomeIcon icon={icon as IconName} fixedWidth={true} />
&nbsp;
</ShowIf>
{text}
</a>

View file

@ -11,15 +11,19 @@ import { IconName } from '../fork-awesome/types'
import { ShowIf } from '../show-if/show-if'
import { LinkWithTextProps } from './types'
export const InternalLink: React.FC<LinkWithTextProps> = ({ href, text, icon, id, className = 'text-light', title }) => {
export const InternalLink: React.FC<LinkWithTextProps> = ({
href,
text,
icon,
id,
className = 'text-light',
title
}) => {
return (
<Link
to={ href }
className={ className }
id={ id }
title={ title }>
<Link to={href} className={className} id={id} title={title}>
<ShowIf condition={!!icon}>
<ForkAwesomeIcon icon={ icon as IconName } fixedWidth={ true }/>&nbsp;
<ForkAwesomeIcon icon={icon as IconName} fixedWidth={true} />
&nbsp;
</ShowIf>
{text}
</Link>

View file

@ -11,7 +11,5 @@ import { TranslatedLinkProps } from './types'
export const TranslatedExternalLink: React.FC<TranslatedLinkProps> = ({ i18nKey, i18nOption, ...props }) => {
const { t } = useTranslation()
return (
<ExternalLink text={ t(i18nKey, i18nOption) } { ...props }/>
)
return <ExternalLink text={t(i18nKey, i18nOption)} {...props} />
}

View file

@ -11,7 +11,5 @@ import { TranslatedLinkProps } from './types'
export const TranslatedInternalLink: React.FC<TranslatedLinkProps> = ({ i18nKey, i18nOption, ...props }) => {
const { t } = useTranslation()
return (
<InternalLink text={ t(i18nKey, i18nOption) } { ...props }/>
)
return <InternalLink text={t(i18nKey, i18nOption)} {...props} />
}

View file

@ -9,7 +9,7 @@ import { Button } from 'react-bootstrap'
import { ForkAwesomeIcon } from '../fork-awesome/fork-awesome-icon'
export interface LockButtonProps {
locked: boolean,
locked: boolean
onLockedChanged: (newState: boolean) => void
title: string
}
@ -17,10 +17,7 @@ export interface LockButtonProps {
export const LockButton: React.FC<LockButtonProps> = ({ locked, onLockedChanged, title }) => {
return (
<Button variant='dark' size='sm' onClick={() => onLockedChanged(!locked)} title={title}>
{ locked
? <ForkAwesomeIcon icon='lock'/>
: <ForkAwesomeIcon icon='unlock'/>
}
{locked ? <ForkAwesomeIcon icon='lock' /> : <ForkAwesomeIcon icon='unlock' />}
</Button>
)
}

View file

@ -23,21 +23,35 @@ export interface CommonModalProps {
'data-cy'?: string
}
export const CommonModal: React.FC<CommonModalProps> = ({ show, onHide, titleI18nKey, title, closeButton, icon, additionalClasses, size, children, ...props }) => {
export const CommonModal: React.FC<CommonModalProps> = ({
show,
onHide,
titleI18nKey,
title,
closeButton,
icon,
additionalClasses,
size,
children,
...props
}) => {
useTranslation()
return (
<Modal data-cy={ props['data-cy'] } show={ show } onHide={ onHide } animation={ true }
dialogClassName={ `text-dark ${ additionalClasses ?? '' }` } size={ size }>
<Modal
data-cy={props['data-cy']}
show={show}
onHide={onHide}
animation={true}
dialogClassName={`text-dark ${additionalClasses ?? ''}`}
size={size}>
<Modal.Header closeButton={!!closeButton}>
<Modal.Title>
<ShowIf condition={!!icon}>
<ForkAwesomeIcon icon={ icon as IconName }/>&nbsp;
<ForkAwesomeIcon icon={icon as IconName} />
&nbsp;
</ShowIf>
{ titleI18nKey
? <Trans i18nKey={ titleI18nKey }/>
: <span>{ title }</span>
}
{titleI18nKey ? <Trans i18nKey={titleI18nKey} /> : <span>{title}</span>}
</Modal.Title>
</Modal.Header>
{children}

View file

@ -14,16 +14,22 @@ export interface DeletionModalProps extends CommonModalProps {
deletionButtonI18nKey: string
}
export const DeletionModal: React.FC<DeletionModalProps> = ({ show, onHide, titleI18nKey, onConfirm, deletionButtonI18nKey, icon, children }) => {
export const DeletionModal: React.FC<DeletionModalProps> = ({
show,
onHide,
titleI18nKey,
onConfirm,
deletionButtonI18nKey,
icon,
children
}) => {
useTranslation()
return (
<CommonModal show={show} onHide={onHide} titleI18nKey={titleI18nKey} icon={icon} closeButton={true}>
<Modal.Body className="text-dark">
{ children }
</Modal.Body>
<Modal.Body className='text-dark'>{children}</Modal.Body>
<Modal.Footer>
<Button variant="danger" onClick={ onConfirm }>
<Button variant='danger' onClick={onConfirm}>
<Trans i18nKey={deletionButtonI18nKey} />
</Button>
</Modal.Footer>

View file

@ -11,9 +11,7 @@ import { CommonModal, CommonModalProps } from './common-modal'
export const ErrorModal: React.FC<CommonModalProps> = ({ show, onHide, titleI18nKey, icon, children }) => {
return (
<CommonModal show={show} onHide={onHide} titleI18nKey={titleI18nKey} icon={icon} closeButton={true}>
<Modal.Body className="text-dark text-center">
{ children }
</Modal.Body>
<Modal.Body className='text-dark text-center'>{children}</Modal.Body>
</CommonModal>
)
}

View file

@ -35,18 +35,14 @@ export const MotdBanner: React.FC = () => {
}
return (
<Alert data-cy={ 'motd-banner' } variant="primary" dir="auto"
className="mb-0 text-center d-flex flex-row justify-content-center">
<span className="flex-grow-1 align-self-center text-black">
{ bannerState.text }
</span>
<Button
data-cy={ 'motd-dismiss' }
variant="outline-primary"
size="sm"
className="mx-2"
onClick={ dismissBanner }>
<ForkAwesomeIcon icon="times"/>
<Alert
data-cy={'motd-banner'}
variant='primary'
dir='auto'
className='mb-0 text-center d-flex flex-row justify-content-center'>
<span className='flex-grow-1 align-self-center text-black'>{bannerState.text}</span>
<Button data-cy={'motd-dismiss'} variant='outline-primary' size='sm' className='mx-2' onClick={dismissBanner}>
<ForkAwesomeIcon icon='times' />
</Button>
</Alert>
)

View file

@ -5,6 +5,5 @@
*/
export const createNumberRangeArray = (length: number): number[] => {
return Array.from(Array(length)
.keys())
return Array.from(Array(length).keys())
}

View file

@ -13,8 +13,8 @@ export interface PageItemProps {
export const PagerItem: React.FC<PageItemProps> = ({ index, onClick }) => {
return (
<li className="page-item">
<span className="page-link" role="button" onClick={ () => onClick(index) }>
<li className='page-item'>
<span className='page-link' role='button' onClick={() => onClick(index)}>
{index + 1}
</span>
</li>

View file

@ -15,7 +15,11 @@ export interface PaginationProps {
lastPageIndex: number
}
export const PagerPagination: React.FC<PaginationProps> = ({ numberOfPageButtonsToShowAfterAndBeforeCurrent, onPageChange, lastPageIndex }) => {
export const PagerPagination: React.FC<PaginationProps> = ({
numberOfPageButtonsToShowAfterAndBeforeCurrent,
onPageChange,
lastPageIndex
}) => {
if (numberOfPageButtonsToShowAfterAndBeforeCurrent % 2 !== 0) {
throw new Error('number of pages to show must be even!')
}
@ -29,39 +33,22 @@ export const PagerPagination: React.FC<PaginationProps> = ({ numberOfPageButtons
onPageChange(pageIndex)
}, [onPageChange, pageIndex])
const correctedLowerPageIndex =
Math.min(
Math.max(
Math.min(
wantedLowerPageIndex,
wantedLowerPageIndex + lastPageIndex - wantedUpperPageIndex
),
0
),
const correctedLowerPageIndex = Math.min(
Math.max(Math.min(wantedLowerPageIndex, wantedLowerPageIndex + lastPageIndex - wantedUpperPageIndex), 0),
lastPageIndex
)
const correctedUpperPageIndex =
Math.max(
Math.min(
Math.max(
wantedUpperPageIndex,
wantedUpperPageIndex - wantedLowerPageIndex
),
lastPageIndex
),
const correctedUpperPageIndex = Math.max(
Math.min(Math.max(wantedUpperPageIndex, wantedUpperPageIndex - wantedLowerPageIndex), lastPageIndex),
0
)
const paginationItemsBefore = Array.from(new Array(correctedPageIndex - correctedLowerPageIndex))
.map((k, index) => {
const paginationItemsBefore = Array.from(new Array(correctedPageIndex - correctedLowerPageIndex)).map((k, index) => {
const itemIndex = correctedLowerPageIndex + index
return <PagerItem key={ itemIndex } index={ itemIndex }
onClick={ setPageIndex }/>
return <PagerItem key={itemIndex} index={itemIndex} onClick={setPageIndex} />
})
const paginationItemsAfter = Array.from(new Array(correctedUpperPageIndex - correctedPageIndex))
.map((k, index) => {
const paginationItemsAfter = Array.from(new Array(correctedUpperPageIndex - correctedPageIndex)).map((k, index) => {
const itemIndex = correctedPageIndex + index + 1
return <PagerItem key={itemIndex} index={itemIndex} onClick={setPageIndex} />
})

View file

@ -12,7 +12,12 @@ export interface PagerPageProps {
onLastPageIndexChange: (lastPageIndex: number) => void
}
export const Pager: React.FC<PagerPageProps> = ({ children, numberOfElementsPerPage, pageIndex, onLastPageIndexChange }) => {
export const Pager: React.FC<PagerPageProps> = ({
children,
numberOfElementsPerPage,
pageIndex,
onLastPageIndexChange
}) => {
const maxPageIndex = Math.ceil(React.Children.count(children) / numberOfElementsPerPage) - 1
const correctedPageIndex = Math.min(maxPageIndex, Math.max(0, pageIndex))
@ -20,13 +25,12 @@ export const Pager: React.FC<PagerPageProps> = ({ children, numberOfElementsPerP
onLastPageIndexChange(maxPageIndex)
}, [children, maxPageIndex, numberOfElementsPerPage, onLastPageIndexChange])
return <Fragment>
{
React.Children.toArray(children)
.filter((value, index) => {
const pageOfElement = Math.floor((index) / numberOfElementsPerPage)
return (pageOfElement === correctedPageIndex)
})
}
return (
<Fragment>
{React.Children.toArray(children).filter((value, index) => {
const pageOfElement = Math.floor(index / numberOfElementsPerPage)
return pageOfElement === correctedPageIndex
})}
</Fragment>
)
}

View file

@ -11,7 +11,9 @@ export const NotFoundErrorScreen: React.FC = () => {
return (
<LandingLayout>
<div className='text-light d-flex align-items-center justify-content-center my-5'>
<h1>404 Not Found <small>oops.</small></h1>
<h1>
404 Not Found <small>oops.</small>
</h1>
</div>
</LandingLayout>
)

View file

@ -26,10 +26,10 @@ export const Redirector: React.FC = () => {
}, [id])
if (error) {
return (<NotFoundErrorScreen/>)
return <NotFoundErrorScreen />
} else if (!error && error != null) {
return (<Redirect to={ `/n/${ id }` }/>)
return <Redirect to={`/n/${id}`} />
} else {
return (<span>Loading</span>)
return <span>Loading</span>
}
}

View file

@ -11,9 +11,9 @@ import './user-avatar.scss'
export interface UserAvatarProps {
size?: 'sm' | 'lg'
name: string;
photo: string;
additionalClasses?: string;
name: string
photo: string
additionalClasses?: string
showName?: boolean
}
@ -29,7 +29,7 @@ const UserAvatar: React.FC<UserAvatarProps> = ({ name, photo, size, additionalCl
title={name}
/>
<ShowIf condition={showName}>
<span className="mx-1 user-line-name">{ name }</span>
<span className='mx-1 user-line-name'>{name}</span>
</ShowIf>
</span>
)

View file

@ -16,7 +16,9 @@ export const ErrorWhileLoadingNoteAlert: React.FC<SimpleAlertProps> = ({ show })
return (
<ShowIf condition={show}>
<Alert variant={'danger'} className={'my-2'}>
<b><Trans i18nKey={ 'views.readOnly.error.title' }/></b>
<b>
<Trans i18nKey={'views.readOnly.error.title'} />
</b>
<br />
<Trans i18nKey={'views.readOnly.error.description'} />
</Alert>

View file

@ -47,20 +47,26 @@ export const DocumentInfobar: React.FC<DocumentInfobarProps> = ({
mode={DocumentInfoLineWithTimeMode.CREATED}
time={createdTime}
userName={createdAuthor}
profileImageSrc={ `${ assetsBaseUrl }/img/avatar.png` }/>
profileImageSrc={`${assetsBaseUrl}/img/avatar.png`}
/>
<DocumentInfoTimeLine
mode={DocumentInfoLineWithTimeMode.EDITED}
time={changedTime}
userName={changedAuthor}
profileImageSrc={ `${ assetsBaseUrl }/img/avatar.png` }/>
profileImageSrc={`${assetsBaseUrl}/img/avatar.png`}
/>
<hr />
</div>
<span className={'ml-auto'}>
{viewCount} <Trans i18nKey={'views.readOnly.viewCount'} />
<ShowIf condition={editable}>
<InternalLink text={ '' } href={ `/n/${ noteId }` } icon={ 'pencil' }
<InternalLink
text={''}
href={`/n/${noteId}`}
icon={'pencil'}
className={'text-primary text-decoration-none mx-1'}
title={ t('views.readOnly.editNote') }/>
title={t('views.readOnly.editNote')}
/>
</ShowIf>
</span>
</div>

View file

@ -25,7 +25,6 @@ import { LoadingNoteAlert } from './LoadingNoteAlert'
import { RendererType } from '../render-page/rendering-message'
export const DocumentReadOnlyPage: React.FC = () => {
useTranslation()
const { id } = useParams<EditorPagePathParams>()
@ -56,11 +55,13 @@ export const DocumentReadOnlyPage: React.FC = () => {
noteId={id}
viewCount={noteDetails.viewCount}
/>
<RenderIframe frameClasses={ 'flex-fill h-100 w-100' }
<RenderIframe
frameClasses={'flex-fill h-100 w-100'}
markdownContent={markdownContent}
onFirstHeadingChange={onFirstHeadingChange}
onFrontmatterChange={onFrontmatterChange}
rendererType={RendererType.DOCUMENT}/>
rendererType={RendererType.DOCUMENT}
/>
</ShowIf>
</div>
)

View file

@ -37,7 +37,7 @@ export const AppBar: React.FC<AppBarProps> = ({ mode }) => {
return (
<Navbar bg={'light'}>
<Nav className="mr-auto d-flex align-items-center">
<Nav className='mr-auto d-flex align-items-center'>
<NavbarBranding />
<ShowIf condition={mode === AppBarMode.EDITOR}>
<EditorViewMode />
@ -54,7 +54,7 @@ export const AppBar: React.FC<AppBarProps> = ({ mode }) => {
<HelpButton />
</ShowIf>
</Nav>
<Nav className="d-flex align-items-center text-secondary">
<Nav className='d-flex align-items-center text-secondary'>
<NewNoteButton />
<ShowIf condition={!userExists}>
<SignInButton size={'sm'} />

View file

@ -21,27 +21,20 @@ const DarkModeButton: React.FC = () => {
const darkModeEnabled = useIsDarkModeActivated() ? DarkModeState.DARK : DarkModeState.LIGHT
return (
<ToggleButtonGroup
type="radio"
name="dark-mode"
value={ darkModeEnabled }
className="ml-2"
>
<ToggleButtonGroup type='radio' name='dark-mode' value={darkModeEnabled} className='ml-2'>
<ToggleButton
value={DarkModeState.DARK}
variant="outline-secondary"
variant='outline-secondary'
title={t('editor.darkMode.switchToDark')}
onChange={ () => setDarkMode(true) }
>
<ForkAwesomeIcon icon="moon"/>
onChange={() => setDarkMode(true)}>
<ForkAwesomeIcon icon='moon' />
</ToggleButton>
<ToggleButton
value={DarkModeState.LIGHT}
variant="outline-secondary"
variant='outline-secondary'
title={t('editor.darkMode.switchToLight')}
onChange={ () => setDarkMode(false) }
>
<ForkAwesomeIcon icon="sun-o"/>
onChange={() => setDarkMode(false)}>
<ForkAwesomeIcon icon='sun-o' />
</ToggleButton>
</ToggleButtonGroup>
)

View file

@ -23,20 +23,20 @@ export const EditorViewMode: React.FC = () => {
const editorMode = useSelector((state: ApplicationState) => state.editorConfig.editorMode)
return (
<ToggleButtonGroup
type="radio"
name="options"
type='radio'
name='options'
value={editorMode}
onChange={(value: EditorMode) => {
setEditorMode(value)
}}>
<ToggleButton value={ EditorMode.PREVIEW } variant="outline-secondary" title={ t('editor.viewMode.view') }>
<ForkAwesomeIcon icon="eye"/>
<ToggleButton value={EditorMode.PREVIEW} variant='outline-secondary' title={t('editor.viewMode.view')}>
<ForkAwesomeIcon icon='eye' />
</ToggleButton>
<ToggleButton value={ EditorMode.BOTH } variant="outline-secondary" title={ t('editor.viewMode.both') }>
<ForkAwesomeIcon icon="columns"/>
<ToggleButton value={EditorMode.BOTH} variant='outline-secondary' title={t('editor.viewMode.both')}>
<ForkAwesomeIcon icon='columns' />
</ToggleButton>
<ToggleButton value={ EditorMode.EDITOR } variant="outline-secondary" title={ t('editor.viewMode.edit') }>
<ForkAwesomeIcon icon="pencil"/>
<ToggleButton value={EditorMode.EDITOR} variant='outline-secondary' title={t('editor.viewMode.edit')}>
<ForkAwesomeIcon icon='pencil' />
</ToggleButton>
</ToggleButtonGroup>
)

View file

@ -8,28 +8,35 @@ import React, { Suspense, useCallback } from 'react'
import { WaitSpinner } from '../../../common/wait-spinner/wait-spinner'
export interface CheatsheetLineProps {
code: string,
code: string
onTaskCheckedChange: (newValue: boolean) => void
}
const HighlightedCode = React.lazy(() => import('../../../markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code'))
const HighlightedCode = React.lazy(
() => import('../../../markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code')
)
const BasicMarkdownRenderer = React.lazy(() => import('../../../markdown-renderer/basic-markdown-renderer'))
export const CheatsheetLine: React.FC<CheatsheetLineProps> = ({ code, onTaskCheckedChange }) => {
const checkboxClick = useCallback((lineInMarkdown: number, newValue: boolean) => {
const checkboxClick = useCallback(
(lineInMarkdown: number, newValue: boolean) => {
onTaskCheckedChange(newValue)
}, [onTaskCheckedChange])
},
[onTaskCheckedChange]
)
return (
<Suspense fallback={ <tr>
<td colSpan={ 2 }><WaitSpinner/></td>
</tr> }>
<Suspense
fallback={
<tr>
<td colSpan={2}>
<WaitSpinner />
</td>
</tr>
}>
<tr>
<td>
<BasicMarkdownRenderer
content={ code }
baseUrl={ 'https://example.org' }
onTaskCheckedChange={ checkboxClick }/>
<BasicMarkdownRenderer content={code} baseUrl={'https://example.org'} onTaskCheckedChange={checkboxClick} />
</td>
<td className={'markdown-body'}>
<HighlightedCode code={code} wrapLines={true} startLineNumber={1} language={'markdown'} />

View file

@ -13,7 +13,8 @@ import { CheatsheetLine } from './cheatsheet-line'
export const Cheatsheet: React.FC = () => {
const { t } = useTranslation()
const [checked, setChecked] = useState<boolean>(false)
const codes = useMemo(() => [
const codes = useMemo(
() => [
`**${t('editor.editorToolbar.bold')}**`,
`*${t('editor.editorToolbar.italic')}*`,
`++${t('editor.editorToolbar.underline')}++`,
@ -32,21 +33,26 @@ export const Cheatsheet: React.FC = () => {
`![${t('editor.editorToolbar.image')}](/icons/apple-touch-icon.png)`,
':smile:',
`:::info\n${t('editor.help.cheatsheet.exampleAlert')}\n:::`
], [checked, t])
],
[checked, t]
)
return (
<Table className="table-condensed table-cheatsheet">
<Table className='table-condensed table-cheatsheet'>
<thead>
<tr>
<th><Trans i18nKey='editor.help.cheatsheet.example'/></th>
<th><Trans i18nKey='editor.help.cheatsheet.syntax'/></th>
<th>
<Trans i18nKey='editor.help.cheatsheet.example' />
</th>
<th>
<Trans i18nKey='editor.help.cheatsheet.syntax' />
</th>
</tr>
</thead>
<tbody>
{
codes.map((code) =>
<CheatsheetLine code={ code } key={ code } onTaskCheckedChange={ setChecked }/>)
}
{codes.map((code) => (
<CheatsheetLine code={code} key={code} onTaskCheckedChange={setChecked} />
))}
</tbody>
</Table>
)

View file

@ -17,9 +17,13 @@ export const HelpButton: React.FC = () => {
return (
<Fragment>
<Button title={ t('editor.documentBar.help') } className='ml-2 text-secondary' size='sm' variant='outline-light'
<Button
title={t('editor.documentBar.help')}
className='ml-2 text-secondary'
size='sm'
variant='outline-light'
onClick={() => setShow(true)}>
<ForkAwesomeIcon icon="question-circle"/>
<ForkAwesomeIcon icon='question-circle' />
</Button>
<HelpModal show={show} onHide={onHide} />
</Fragment>

View file

@ -19,7 +19,7 @@ export enum HelpTabStatus {
}
export interface HelpModalProps {
show: boolean,
show: boolean
onHide: () => void
}
@ -30,11 +30,11 @@ export const HelpModal: React.FC<HelpModalProps> = ({ show, onHide }) => {
const tabContent = useMemo(() => {
switch (tab) {
case HelpTabStatus.Cheatsheet:
return (<Cheatsheet/>)
return <Cheatsheet />
case HelpTabStatus.Shortcuts:
return (<Shortcut/>)
return <Shortcut />
case HelpTabStatus.Links:
return (<Links/>)
return <Links />
}
}, [tab])
@ -65,5 +65,6 @@ export const HelpModal: React.FC<HelpModalProps> = ({ show, onHide }) => {
</nav>
{tabContent}
</Modal.Body>
</CommonModal>)
</CommonModal>
)
}

View file

@ -17,9 +17,11 @@ export const Links: React.FC = () => {
return (
<Row className={'justify-content-center pt-4'}>
<Col lg={4}>
<h3><Trans i18nKey='editor.help.contacts.title'/></h3>
<h3>
<Trans i18nKey='editor.help.contacts.title' />
</h3>
<div>
<ul className="list-unstyled">
<ul className='list-unstyled'>
<li>
<TranslatedExternalLink
i18nKey='editor.help.contacts.community'
@ -57,9 +59,11 @@ export const Links: React.FC = () => {
</div>
</Col>
<Col lg={4}>
<h3><Trans i18nKey='editor.help.documents.title'/></h3>
<h3>
<Trans i18nKey='editor.help.documents.title' />
</h3>
<div>
<ul className="list-unstyled">
<ul className='list-unstyled'>
<li>
<TranslatedInternalLink
i18nKey='editor.help.documents.features'

View file

@ -30,30 +30,29 @@ export const Shortcut: React.FC = () => {
}
return (
<Row className={'justify-content-center pt-4'}>
{ Object.keys(shortcutMap)
.map(category => {
{Object.keys(shortcutMap).map((category) => {
return (
<Card key={category} className={'m-2 w-50'}>
<Card.Header>{category}</Card.Header>
<ListGroup variant="flush">
{ Object.entries(shortcutMap[category])
.map(([functionName, shortcuts]) => {
<ListGroup variant='flush'>
{Object.entries(shortcutMap[category]).map(([functionName, shortcuts]) => {
return (
<ListGroup.Item key={functionName} className={'d-flex justify-content-between'}>
<span><Trans i18nKey={ functionName }/></span>
<span>
{
shortcuts.map((shortcut, shortcutIndex) =>
<Fragment key={ shortcutIndex }>{ shortcut }</Fragment>)
}
<Trans i18nKey={functionName} />
</span>
<span>
{shortcuts.map((shortcut, shortcutIndex) => (
<Fragment key={shortcutIndex}>{shortcut}</Fragment>
))}
</span>
</ListGroup.Item>
)
})}
</ListGroup>
</Card>)
})
}
</Card>
)
})}
</Row>
)
}

View file

@ -20,10 +20,11 @@ export const NavbarBranding: React.FC = () => {
return (
<Navbar.Brand>
<Link to="/intro" className="text-secondary text-decoration-none d-flex align-items-center">
<Link to='/intro' className='text-secondary text-decoration-none d-flex align-items-center'>
<HedgeDocLogoWithText
logoType={darkModeActivated ? HedgeDocLogoType.WB_HORIZONTAL : HedgeDocLogoType.BW_HORIZONTAL}
size={ HedgeDocLogoSize.SMALL }/>
size={HedgeDocLogoSize.SMALL}
/>
<Branding inline={true} />
</Link>
</Navbar.Brand>

View file

@ -13,8 +13,8 @@ export const NewNoteButton: React.FC = () => {
useTranslation()
return (
<Button className="mx-2" size="sm" variant="primary">
<ForkAwesomeIcon icon="plus"/> <Trans i18nKey="editor.appBar.new"/>
<Button className='mx-2' size='sm' variant='primary'>
<ForkAwesomeIcon icon='plus' /> <Trans i18nKey='editor.appBar.new' />
</Button>
)
}

View file

@ -18,9 +18,12 @@ export const ReadOnlyModeButton: React.FC = () => {
return (
<Link to={`/s/${id}`} target='_blank'>
<Button title={ t('editor.documentBar.readOnlyMode') } className="ml-2 text-secondary" size="sm"
variant="outline-light">
<ForkAwesomeIcon icon="file-text-o"/>
<Button
title={t('editor.documentBar.readOnlyMode')}
className='ml-2 text-secondary'
size='sm'
variant='outline-light'>
<ForkAwesomeIcon icon='file-text-o' />
</Button>
</Link>
)

View file

@ -18,9 +18,12 @@ export const SlideModeButton: React.FC = () => {
return (
<Link to={`/p/${id}`} target='_blank'>
<Button title={ t('editor.documentBar.slideMode') } className="ml-2 text-secondary" size="sm"
variant="outline-light">
<ForkAwesomeIcon icon="television"/>
<Button
title={t('editor.documentBar.slideMode')}
className='ml-2 text-secondary'
size='sm'
variant='outline-light'>
<ForkAwesomeIcon icon='television' />
</Button>
</Link>
)

View file

@ -20,26 +20,30 @@ enum SyncScrollState {
}
export const SyncScrollButtons: React.FC = () => {
const syncScrollEnabled = useSelector((state: ApplicationState) => state.editorConfig.syncScroll) ? SyncScrollState.SYNCED : SyncScrollState.UNSYNCED
const syncScrollEnabled = useSelector((state: ApplicationState) => state.editorConfig.syncScroll)
? SyncScrollState.SYNCED
: SyncScrollState.UNSYNCED
const { t } = useTranslation()
return (
<ToggleButtonGroup type="radio" defaultValue={ [] } name="sync-scroll" className={ 'ml-2 sync-scroll-buttons' }
<ToggleButtonGroup
type='radio'
defaultValue={[]}
name='sync-scroll'
className={'ml-2 sync-scroll-buttons'}
value={syncScrollEnabled}>
<ToggleButton
variant={'outline-secondary'}
title={t('editor.appBar.syncScroll.enable')}
onChange={() => setEditorSyncScroll(true)}
value={ SyncScrollState.SYNCED }
>
value={SyncScrollState.SYNCED}>
<EnabledScrollIcon />
</ToggleButton>
<ToggleButton
variant={'outline-secondary'}
title={t('editor.appBar.syncScroll.disable')}
onChange={() => setEditorSyncScroll(false)}
value={ SyncScrollState.UNSYNCED }
>
value={SyncScrollState.UNSYNCED}>
<DisabledScrollIcon />
</ToggleButton>
</ToggleButtonGroup>

View file

@ -17,9 +17,7 @@ export const DocumentInfoLine: React.FC<DocumentInfoLineProps> = ({ icon, size,
return (
<span className={'d-flex align-items-center'}>
<ForkAwesomeIcon icon={icon} size={size} fixedWidth={true} className={'mx-2'} />
<i className={ 'd-flex align-items-center' }>
{ children }
</i>
<i className={'d-flex align-items-center'}>{children}</i>
</span>
)
}

View file

@ -15,7 +15,7 @@ import { UnitalicBoldText } from './unitalic-bold-text'
import { useCustomizeAssetsUrl } from '../../../../hooks/common/use-customize-assets-url'
export interface DocumentInfoModalProps {
show: boolean,
show: boolean
onHide: () => void
}
@ -24,30 +24,26 @@ export const DocumentInfoModal: React.FC<DocumentInfoModalProps> = ({ show, onHi
// TODO Replace hardcoded mock data with real/mock API requests
return (
<CommonModal
show={ show }
onHide={ onHide }
closeButton={ true }
titleI18nKey={ 'editor.modal.documentInfo.title' }>
<CommonModal show={show} onHide={onHide} closeButton={true} titleI18nKey={'editor.modal.documentInfo.title'}>
<Modal.Body>
<ListGroup>
<ListGroup.Item>
<DocumentInfoTimeLine
size={'2x'}
mode={DocumentInfoLineWithTimeMode.CREATED}
time={ DateTime.local()
.minus({ days: 11 }) }
time={DateTime.local().minus({ days: 11 })}
userName={'Tilman'}
profileImageSrc={ `${ assetsBaseUrl }img/avatar.png` }/>
profileImageSrc={`${assetsBaseUrl}img/avatar.png`}
/>
</ListGroup.Item>
<ListGroup.Item>
<DocumentInfoTimeLine
size={'2x'}
mode={DocumentInfoLineWithTimeMode.EDITED}
time={ DateTime.local()
.minus({ minutes: 3 }) }
time={DateTime.local().minus({ minutes: 3 })}
userName={'Philip'}
profileImageSrc={ `${ assetsBaseUrl }img/avatar.png` }/>
profileImageSrc={`${assetsBaseUrl}img/avatar.png`}
/>
</ListGroup.Item>
<ListGroup.Item>
<DocumentInfoLine icon={'users'} size={'2x'}>

View file

@ -14,7 +14,7 @@ import { TimeFromNow } from './time-from-now'
export interface DocumentInfoLineWithTimeProps {
size?: '2x' | '3x' | '4x' | '5x' | undefined
time: DateTime,
time: DateTime
mode: DocumentInfoLineWithTimeMode
userName: string
profileImageSrc: string
@ -25,17 +25,30 @@ export enum DocumentInfoLineWithTimeMode {
EDITED
}
export const DocumentInfoTimeLine: React.FC<DocumentInfoLineWithTimeProps> = ({ time, mode, userName, profileImageSrc, size }) => {
export const DocumentInfoTimeLine: React.FC<DocumentInfoLineWithTimeProps> = ({
time,
mode,
userName,
profileImageSrc,
size
}) => {
useTranslation()
const i18nKey = mode === DocumentInfoLineWithTimeMode.CREATED ? 'editor.modal.documentInfo.created' : 'editor.modal.documentInfo.edited'
const i18nKey =
mode === DocumentInfoLineWithTimeMode.CREATED
? 'editor.modal.documentInfo.created'
: 'editor.modal.documentInfo.edited'
const icon: IconName = mode === DocumentInfoLineWithTimeMode.CREATED ? 'plus' : 'pencil'
return (
<DocumentInfoLine icon={icon} size={size}>
<Trans i18nKey={i18nKey}>
<UserAvatar photo={ profileImageSrc } additionalClasses={ 'font-style-normal bold font-weight-bold' }
name={ userName } size={ size ? 'lg' : undefined }/>
<UserAvatar
photo={profileImageSrc}
additionalClasses={'font-style-normal bold font-weight-bold'}
name={userName}
size={size ? 'lg' : undefined}
/>
<TimeFromNow time={time} />
</Trans>
</DocumentInfoLine>

View file

@ -14,7 +14,8 @@ export interface TimeFromNowProps {
export const TimeFromNow: React.FC<TimeFromNowProps> = ({ time }) => {
return (
<time className={ 'mx-1' } title={ time.toFormat('DDDD T') }
dateTime={ time.toString() }>{ time.toRelative() }</time>
<time className={'mx-1'} title={time.toFormat('DDDD T')} dateTime={time.toString()}>
{time.toRelative()}
</time>
)
}

View file

@ -7,7 +7,7 @@
import React from 'react'
export interface UnitalicBoldTextProps {
text: string;
text: string
}
export const UnitalicBoldText: React.FC<UnitalicBoldTextProps> = ({ text }) => {

View file

@ -18,7 +18,7 @@ export interface PermissionGroupEntryProps {
export enum GroupMode {
NONE,
VIEW,
EDIT,
EDIT
}
export const PermissionGroupEntry: React.FC<PermissionGroupEntryProps> = ({ title, editMode, onChangeEditMode }) => {
@ -27,34 +27,26 @@ export const PermissionGroupEntry: React.FC<PermissionGroupEntryProps> = ({ titl
return (
<li className={'list-group-item d-flex flex-row justify-content-between align-items-center'}>
<Trans i18nKey={title} />
<ToggleButtonGroup
type='radio'
name='edit-mode'
value={ editMode }
onChange={ onChangeEditMode }
>
<ToggleButtonGroup type='radio' name='edit-mode' value={editMode} onChange={onChangeEditMode}>
<ToggleButton
title={t('editor.modal.permissions.denyGroup', { name: t(title) })}
variant={'light'}
className={'text-secondary'}
value={ GroupMode.NONE }
>
value={GroupMode.NONE}>
<ForkAwesomeIcon icon='ban' />
</ToggleButton>
<ToggleButton
title={t('editor.modal.permissions.viewOnlyGroup', { name: t(title) })}
variant={'light'}
className={'text-secondary'}
value={ GroupMode.VIEW }
>
value={GroupMode.VIEW}>
<ForkAwesomeIcon icon='eye' />
</ToggleButton>
<ToggleButton
title={t('editor.modal.permissions.editGroup', { name: t(title) })}
variant={'light'}
className={'text-secondary'}
value={ GroupMode.EDIT }
>
value={GroupMode.EDIT}>
<ForkAwesomeIcon icon='pencil' />
</ToggleButton>
</ToggleButtonGroup>

View file

@ -27,7 +27,17 @@ export enum EditMode {
EDIT
}
export const PermissionList: React.FC<PermissionListProps> = ({ list, identifier, changeEditMode, removeEntry, createEntry, editI18nKey, viewI18nKey, removeI18nKey, addI18nKey }) => {
export const PermissionList: React.FC<PermissionListProps> = ({
list,
identifier,
changeEditMode,
removeEntry,
createEntry,
editI18nKey,
viewI18nKey,
removeI18nKey,
addI18nKey
}) => {
const { t } = useTranslation()
const [newEntry, setNewEntry] = useState('')
@ -38,7 +48,7 @@ export const PermissionList: React.FC<PermissionListProps> = ({ list, identifier
return (
<ul className={'list-group'}>
{ list.map(entry => (
{list.map((entry) => (
<li key={entry.id} className={'list-group-item d-flex flex-row justify-content-between align-items-center'}>
{identifier(entry)}
<div>
@ -46,30 +56,26 @@ export const PermissionList: React.FC<PermissionListProps> = ({ list, identifier
variant='light'
className={'text-danger mr-2'}
title={t(removeI18nKey, { name: entry.name })}
onClick={ () => removeEntry(entry.id) }
>
onClick={() => removeEntry(entry.id)}>
<ForkAwesomeIcon icon={'times'} />
</Button>
<ToggleButtonGroup
type='radio'
name='edit-mode'
value={entry.canEdit ? EditMode.EDIT : EditMode.VIEW}
onChange={ (value: EditMode) => changeEditMode(entry.id, value === EditMode.EDIT) }
>
onChange={(value: EditMode) => changeEditMode(entry.id, value === EditMode.EDIT)}>
<ToggleButton
title={t(viewI18nKey, { name: entry.name })}
variant={'light'}
className={'text-secondary'}
value={ EditMode.VIEW }
>
value={EditMode.VIEW}>
<ForkAwesomeIcon icon='eye' />
</ToggleButton>
<ToggleButton
title={t(editI18nKey, { name: entry.name })}
variant={'light'}
className={'text-secondary'}
value={ EditMode.EDIT }
>
value={EditMode.EDIT}>
<ForkAwesomeIcon icon='pencil' />
</ToggleButton>
</ToggleButtonGroup>
@ -77,7 +83,8 @@ export const PermissionList: React.FC<PermissionListProps> = ({ list, identifier
</li>
))}
<li className={'list-group-item'}>
<form onSubmit={ event => {
<form
onSubmit={(event) => {
event.preventDefault()
addEntry()
}}>
@ -86,14 +93,9 @@ export const PermissionList: React.FC<PermissionListProps> = ({ list, identifier
value={newEntry}
placeholder={t(addI18nKey)}
aria-label={t(addI18nKey)}
onChange={ event => setNewEntry(event.currentTarget.value) }
onChange={(event) => setNewEntry(event.currentTarget.value)}
/>
<Button
variant='light'
className={ 'text-secondary ml-2' }
title={ t(addI18nKey) }
onClick={ addEntry }
>
<Button variant='light' className={'text-secondary ml-2'} title={t(addI18nKey)} onClick={addEntry}>
<ForkAwesomeIcon icon={'plus'} />
</Button>
</InputGroup>

View file

@ -15,7 +15,7 @@ import { GroupMode, PermissionGroupEntry } from './permission-group-entry'
import { PermissionList } from './permission-list'
export interface PermissionsModalProps {
show: boolean,
show: boolean
onHide: () => void
}
@ -31,7 +31,7 @@ interface NotePermissions {
sharedTo: {
username: string
canEdit: boolean
}[],
}[]
sharedToGroup: {
id: string
canEdit: boolean
@ -43,20 +43,26 @@ export const EVERYONE_LOGGED_IN_GROUP_ID = '2'
const permissionsApiResponse: NotePermissions = {
owner: 'dermolly',
sharedTo: [{
sharedTo: [
{
username: 'emcrx',
canEdit: true
}, {
},
{
username: 'mrdrogdrog',
canEdit: false
}],
sharedToGroup: [{
}
],
sharedToGroup: [
{
id: EVERYONE_GROUP_ID,
canEdit: true
}, {
},
{
id: EVERYONE_LOGGED_IN_GROUP_ID,
canEdit: false
}]
}
]
}
export const PermissionModal: React.FC<PermissionsModalProps> = ({ show, onHide }) => {
@ -70,7 +76,7 @@ export const PermissionModal: React.FC<PermissionsModalProps> = ({ show, onHide
useEffect(() => {
// set owner
getUserById(permissionsApiResponse.owner)
.then(response => {
.then((response) => {
setOwner({
name: response.name,
photo: response.photo
@ -78,20 +84,24 @@ export const PermissionModal: React.FC<PermissionsModalProps> = ({ show, onHide
})
.catch(() => setError(true))
// set user List
permissionsApiResponse.sharedTo.forEach(shareUser => {
permissionsApiResponse.sharedTo.forEach((shareUser) => {
getUserById(shareUser.username)
.then(response => {
setUserList(list => list.concat([{
.then((response) => {
setUserList((list) =>
list.concat([
{
id: response.id,
name: response.name,
photo: response.photo,
canEdit: shareUser.canEdit
}]))
}
])
)
})
.catch(() => setError(true))
})
// set group List
permissionsApiResponse.sharedToGroup.forEach(sharedGroup => {
permissionsApiResponse.sharedToGroup.forEach((sharedGroup) => {
if (sharedGroup.id === EVERYONE_GROUP_ID) {
setAllUserPermissions(sharedGroup.canEdit ? GroupMode.EDIT : GroupMode.VIEW)
} else if (sharedGroup.id === EVERYONE_LOGGED_IN_GROUP_ID) {
@ -101,37 +111,37 @@ export const PermissionModal: React.FC<PermissionsModalProps> = ({ show, onHide
}, [])
const changeUserMode = (userId: Principal['id'], canEdit: Principal['canEdit']) => {
setUserList(list =>
list
.map(user => {
setUserList((list) =>
list.map((user) => {
if (user.id === userId) {
user.canEdit = canEdit
}
return user
}))
})
)
}
const removeUser = (userId: Principal['id']) => {
setUserList(list => list.filter(user => user.id !== userId))
setUserList((list) => list.filter((user) => user.id !== userId))
}
const addUser = (name: Principal['name']) => {
setUserList(list => list.concat({
setUserList((list) =>
list.concat({
id: name,
photo: '/img/avatar.png',
name: name,
canEdit: false
}))
})
)
}
return (
<CommonModal
show={ show }
onHide={ onHide }
closeButton={ true }
titleI18nKey={ 'editor.modal.permissions.title' }>
<CommonModal show={show} onHide={onHide} closeButton={true} titleI18nKey={'editor.modal.permissions.title'}>
<Modal.Body>
<h5 className={ 'mb-3' }><Trans i18nKey={ 'editor.modal.permissions.owner' }/></h5>
<h5 className={'mb-3'}>
<Trans i18nKey={'editor.modal.permissions.owner'} />
</h5>
<ShowIf condition={error}>
<Alert variant='danger'>
<Trans i18nKey='editor.modal.permissions.error' />
@ -142,10 +152,12 @@ export const PermissionModal: React.FC<PermissionsModalProps> = ({ show, onHide
<UserAvatar name={owner?.name ?? ''} photo={owner?.photo ?? ''} />
</li>
</ul>
<h5 className={ 'my-3' }><Trans i18nKey={ 'editor.modal.permissions.sharedWithUsers' }/></h5>
<h5 className={'my-3'}>
<Trans i18nKey={'editor.modal.permissions.sharedWithUsers'} />
</h5>
<PermissionList
list={userList}
identifier={ entry => (<UserAvatar name={ entry.name } photo={ entry.photo }/>) }
identifier={(entry) => <UserAvatar name={entry.name} photo={entry.photo} />}
changeEditMode={changeUserMode}
removeEntry={removeUser}
createEntry={addUser}
@ -154,7 +166,9 @@ export const PermissionModal: React.FC<PermissionsModalProps> = ({ show, onHide
removeI18nKey={'editor.modal.permissions.removeUser'}
addI18nKey={'editor.modal.permissions.addUser'}
/>
<h5 className={ 'my-3' }><Trans i18nKey={ 'editor.modal.permissions.sharedWithGroups' }/></h5>
<h5 className={'my-3'}>
<Trans i18nKey={'editor.modal.permissions.sharedWithGroups'} />
</h5>
<ul className={'list-group'}>
<PermissionGroupEntry
title={'editor.modal.permissions.allUser'}

View file

@ -20,17 +20,20 @@ export interface RevisionModalListEntryProps {
revisionAuthorListMap: Map<number, UserResponse[]>
}
export const RevisionModalListEntry: React.FC<RevisionModalListEntryProps> = ({ active, onClick, revision, revisionAuthorListMap }) => (
export const RevisionModalListEntry: React.FC<RevisionModalListEntryProps> = ({
active,
onClick,
revision,
revisionAuthorListMap
}) => (
<ListGroup.Item
as='li'
active={active}
onClick={onClick}
className='user-select-none revision-item d-flex flex-column'
>
className='user-select-none revision-item d-flex flex-column'>
<span>
<ForkAwesomeIcon icon={'clock-o'} className='mx-2' />
{ DateTime.fromMillis(revision.timestamp * 1000)
.toFormat('DDDD T') }
{DateTime.fromMillis(revision.timestamp * 1000).toFormat('DDDD T')}
</span>
<span>
<ForkAwesomeIcon icon={'file-text-o'} className='mx-2' />
@ -38,15 +41,11 @@ export const RevisionModalListEntry: React.FC<RevisionModalListEntryProps> = ({
</span>
<span className={'d-flex flex-row my-1 align-items-center'}>
<ForkAwesomeIcon icon={'user-o'} className={'mx-2'} />
{
revisionAuthorListMap.get(revision.timestamp)
?.map((user, index) => {
{revisionAuthorListMap.get(revision.timestamp)?.map((user, index) => {
return (
<UserAvatar name={ user.name } photo={ user.photo } showName={ false }
additionalClasses={ 'mx-1' } key={ index }/>
<UserAvatar name={user.name} photo={user.photo} showName={false} additionalClasses={'mx-1'} key={index} />
)
})
}
})}
</span>
</ListGroup.Item>
)

View file

@ -21,7 +21,7 @@ import './revision-modal.scss'
import { downloadRevision, getUserDataForRevision } from './utils'
export interface PermissionsModalProps {
show: boolean,
show: boolean
onHide: () => void
}
@ -37,8 +37,8 @@ export const RevisionModal: React.FC<PermissionsModalProps> = ({ show, onHide })
useEffect(() => {
getAllRevisions(id)
.then(fetchedRevisions => {
fetchedRevisions.forEach(revision => {
.then((fetchedRevisions) => {
fetchedRevisions.forEach((revision) => {
const authorData = getUserDataForRevision(revision.authors)
revisionAuthorListMap.current.set(revision.timestamp, authorData)
})
@ -55,7 +55,7 @@ export const RevisionModal: React.FC<PermissionsModalProps> = ({ show, onHide })
return
}
getRevision(id, selectedRevisionTimestamp)
.then(fetchedRevision => {
.then((fetchedRevision) => {
setSelectedRevision(fetchedRevision)
})
.catch(() => setError(true))
@ -64,14 +64,19 @@ export const RevisionModal: React.FC<PermissionsModalProps> = ({ show, onHide })
const markdownContent = useNoteMarkdownContent()
return (
<CommonModal show={ show } onHide={ onHide } titleI18nKey={ 'editor.modal.revision.title' } icon={ 'history' }
closeButton={ true } size={ 'xl' } additionalClasses='revision-modal'>
<CommonModal
show={show}
onHide={onHide}
titleI18nKey={'editor.modal.revision.title'}
icon={'history'}
closeButton={true}
size={'xl'}
additionalClasses='revision-modal'>
<Modal.Body>
<Row>
<Col lg={4} className={'scroll-col'}>
<ListGroup as='ul'>
{
revisions.map((revision, revisionIndex) => (
{revisions.map((revision, revisionIndex) => (
<RevisionModalListEntry
key={revisionIndex}
active={selectedRevisionTimestamp === revision.timestamp}
@ -79,8 +84,7 @@ export const RevisionModal: React.FC<PermissionsModalProps> = ({ show, onHide })
revisionAuthorListMap={revisionAuthorListMap.current}
onClick={() => setSelectedRevisionTimestamp(revision.timestamp)}
/>
))
}
))}
</ListGroup>
</Col>
<Col lg={8} className={'scroll-col'}>
@ -102,9 +106,7 @@ export const RevisionModal: React.FC<PermissionsModalProps> = ({ show, onHide })
</Row>
</Modal.Body>
<Modal.Footer>
<Button
variant='secondary'
onClick={ onHide }>
<Button variant='secondary' onClick={onHide}>
<Trans i18nKey={'common.close'} />
</Button>
<Button

View file

@ -23,7 +23,7 @@ export const getUserDataForRevision = (authors: string[]): UserResponse[] => {
return
}
getUserById(author)
.then(userData => {
.then((userData) => {
users.push(userData)
})
.catch((error) => console.error(error))

View file

@ -19,7 +19,7 @@ import { EditorPagePathParams } from '../../editor-page'
import { NoteType } from '../../note-frontmatter/note-frontmatter'
export interface ShareModalProps {
show: boolean,
show: boolean
onHide: () => void
}
@ -31,24 +31,21 @@ export const ShareModal: React.FC<ShareModalProps> = ({ show, onHide }) => {
const { id } = useParams<EditorPagePathParams>()
return (
<CommonModal
show={ show }
onHide={ onHide }
closeButton={ true }
titleI18nKey={ 'editor.modal.shareLink.title' }>
<CommonModal show={show} onHide={onHide} closeButton={true} titleI18nKey={'editor.modal.shareLink.title'}>
<Modal.Body>
<Trans i18nKey={'editor.modal.shareLink.editorDescription'} />
<CopyableField content={ `${ baseUrl }n/${ id }?${ editorMode }` } nativeShareButton={ true }
url={ `${ baseUrl }n/${ id }?${ editorMode }` }/>
<CopyableField
content={`${baseUrl}n/${id}?${editorMode}`}
nativeShareButton={true}
url={`${baseUrl}n/${id}?${editorMode}`}
/>
<ShowIf condition={noteFrontmatter.type === NoteType.SLIDE}>
<Trans i18nKey={'editor.modal.shareLink.slidesDescription'} />
<CopyableField content={ `${ baseUrl }p/${ id }` } nativeShareButton={ true }
url={ `${ baseUrl }p/${ id }` }/>
<CopyableField content={`${baseUrl}p/${id}`} nativeShareButton={true} url={`${baseUrl}p/${id}`} />
</ShowIf>
<ShowIf condition={noteFrontmatter.type === ''}>
<Trans i18nKey={'editor.modal.shareLink.viewOnlyDescription'} />
<CopyableField content={ `${ baseUrl }s/${ id }` } nativeShareButton={ true }
url={ `${ baseUrl }s/${ id }` }/>
<CopyableField content={`${baseUrl}s/${id}`} nativeShareButton={true} url={`${baseUrl}s/${id}`} />
</ShowIf>
</Modal.Body>
</CommonModal>

View file

@ -7,19 +7,31 @@
import { RefObject, useCallback } from 'react'
import { LineMarkerPosition } from '../../../markdown-renderer/types'
export const useAdaptedLineMarkerCallback = (documentRenderPaneRef: RefObject<HTMLDivElement> | undefined,
export const useAdaptedLineMarkerCallback = (
documentRenderPaneRef: RefObject<HTMLDivElement> | undefined,
rendererRef: RefObject<HTMLDivElement>,
onLineMarkerPositionChanged: ((lineMarkerPosition: LineMarkerPosition[]) => void) | undefined): ((lineMarkerPosition: LineMarkerPosition[]) => void) => {
return useCallback((linkMarkerPositions) => {
if (!onLineMarkerPositionChanged || !documentRenderPaneRef || !documentRenderPaneRef.current || !rendererRef.current) {
onLineMarkerPositionChanged: ((lineMarkerPosition: LineMarkerPosition[]) => void) | undefined
): ((lineMarkerPosition: LineMarkerPosition[]) => void) => {
return useCallback(
(linkMarkerPositions) => {
if (
!onLineMarkerPositionChanged ||
!documentRenderPaneRef ||
!documentRenderPaneRef.current ||
!rendererRef.current
) {
return
}
const documentRenderPaneTop = (documentRenderPaneRef.current.offsetTop ?? 0)
const rendererTop = (rendererRef.current.offsetTop ?? 0)
const documentRenderPaneTop = documentRenderPaneRef.current.offsetTop ?? 0
const rendererTop = rendererRef.current.offsetTop ?? 0
const offset = rendererTop - documentRenderPaneTop
onLineMarkerPositionChanged(linkMarkerPositions.map(oldMarker => ({
onLineMarkerPositionChanged(
linkMarkerPositions.map((oldMarker) => ({
line: oldMarker.line,
position: oldMarker.position + offset
})))
}, [documentRenderPaneRef, onLineMarkerPositionChanged, rendererRef])
}))
)
},
[documentRenderPaneRef, onLineMarkerPositionChanged, rendererRef]
)
}

View file

@ -7,8 +7,13 @@
import { RefObject, useCallback, useRef } from 'react'
import { IframeEditorToRendererCommunicator } from '../../../render-page/iframe-editor-to-renderer-communicator'
export const useOnIframeLoad = (frameReference: RefObject<HTMLIFrameElement>, iframeCommunicator: IframeEditorToRendererCommunicator,
rendererOrigin: string, renderPageUrl: string, onNavigateAway: () => void): () => void => {
export const useOnIframeLoad = (
frameReference: RefObject<HTMLIFrameElement>,
iframeCommunicator: IframeEditorToRendererCommunicator,
rendererOrigin: string,
renderPageUrl: string,
onNavigateAway: () => void
): (() => void) => {
const sendToRenderPage = useRef<boolean>(true)
return useCallback(() => {

View file

@ -26,7 +26,12 @@ export const ShowOnPropChangeImageLightbox: React.FC<ShowOnPropChangeImageLightb
}, [details])
return (
<ImageLightboxModal show={ show } onHide={ hideLightbox } src={ details?.src }
alt={ details?.alt } title={ details?.title }/>
<ImageLightboxModal
show={show}
onHide={hideLightbox}
src={details?.src}
alt={details?.alt}
title={details?.title}
/>
)
}

View file

@ -19,14 +19,22 @@ export const MaxLengthWarningModal: React.FC<MaxLengthWarningModalProps> = ({ sh
useTranslation()
return (
<CommonModal data-cy={ 'limitReachedModal' } show={ show } onHide={ onHide }
titleI18nKey={ 'editor.error.limitReached.title' } closeButton={ true }>
<CommonModal
data-cy={'limitReachedModal'}
show={show}
onHide={onHide}
titleI18nKey={'editor.error.limitReached.title'}
closeButton={true}>
<Modal.Body>
<Trans i18nKey={'editor.error.limitReached.description'} values={{ maxLength }} />
<strong className='mt-2 d-block'><Trans i18nKey={ 'editor.error.limitReached.advice' }/></strong>
<strong className='mt-2 d-block'>
<Trans i18nKey={'editor.error.limitReached.advice'} />
</strong>
</Modal.Body>
<Modal.Footer>
<Button onClick={ onHide }><Trans i18nKey={ 'common.close' }/></Button>
<Button onClick={onHide}>
<Trans i18nKey={'common.close'} />
</Button>
</Modal.Footer>
</CommonModal>
)

View file

@ -59,17 +59,23 @@ export const EditorPage: React.FC = () => {
rendererScrollState: { firstLineInView: 1, scrolledPercentage: 0 }
}))
const onMarkdownRendererScroll = useCallback((newScrollState: ScrollState) => {
const onMarkdownRendererScroll = useCallback(
(newScrollState: ScrollState) => {
if (scrollSource.current === ScrollSource.RENDERER && editorSyncScroll) {
setScrollState((old) => ({ editorScrollState: newScrollState, rendererScrollState: old.rendererScrollState }))
}
}, [editorSyncScroll])
},
[editorSyncScroll]
)
const onEditorScroll = useCallback((newScrollState: ScrollState) => {
const onEditorScroll = useCallback(
(newScrollState: ScrollState) => {
if (scrollSource.current === ScrollSource.EDITOR && editorSyncScroll) {
setScrollState((old) => ({ rendererScrollState: newScrollState, editorScrollState: old.editorScrollState }))
}
}, [editorSyncScroll])
},
[editorSyncScroll]
)
useViewModeShortcuts()
useApplyDarkMode()
@ -90,16 +96,21 @@ export const EditorPage: React.FC = () => {
useNotificationTest()
const leftPane = useMemo(() =>
const leftPane = useMemo(
() => (
<EditorPane
onContentChange={setNoteMarkdownContent}
content={markdownContent}
scrollState={scrollState.editorScrollState}
onScroll={onEditorScroll}
onMakeScrollSource={ setEditorToScrollSource }/>
, [markdownContent, onEditorScroll, scrollState.editorScrollState, setEditorToScrollSource])
onMakeScrollSource={setEditorToScrollSource}
/>
),
[markdownContent, onEditorScroll, scrollState.editorScrollState, setEditorToScrollSource]
)
const rightPane = useMemo(() =>
const rightPane = useMemo(
() => (
<RenderIframe
frameClasses={'h-100 w-100'}
markdownContent={markdownContent}
@ -109,9 +120,11 @@ export const EditorPage: React.FC = () => {
onFrontmatterChange={setNoteFrontmatter}
onScroll={onMarkdownRendererScroll}
scrollState={scrollState.rendererScrollState}
rendererType={ RendererType.DOCUMENT }/>
, [markdownContent, onMarkdownRendererScroll, scrollState.rendererScrollState,
setRendererToScrollSource])
rendererType={RendererType.DOCUMENT}
/>
),
[markdownContent, onMarkdownRendererScroll, scrollState.rendererScrollState, setRendererToScrollSource]
)
return (
<IframeCommunicatorContextProvider>
@ -130,7 +143,8 @@ export const EditorPage: React.FC = () => {
left={leftPane}
showRight={editorMode === EditorMode.PREVIEW || editorMode === EditorMode.BOTH}
right={rightPane}
containerClassName={ 'overflow-hidden' }/>
containerClassName={'overflow-hidden'}
/>
<Sidebar />
</div>
</ShowIf>

View file

@ -11,7 +11,8 @@ const wordRegExp = /^```((\w|-|_|\+)*)$/
let allSupportedLanguages: string[] = []
const codeBlockHint = (editor: Editor): Promise<Hints | null> => {
return import(/* webpackChunkName: "highlight.js" */ '../../../common/hljs/hljs').then((hljs) =>
return import(/* webpackChunkName: "highlight.js" */ '../../../common/hljs/hljs').then(
(hljs) =>
new Promise((resolve) => {
const searchTerm = findWordAtCursor(editor)
const searchResult = wordRegExp.exec(searchTerm.text)
@ -21,7 +22,8 @@ const codeBlockHint = (editor: Editor): Promise<Hints | null> => {
}
const term = searchResult[1]
if (allSupportedLanguages.length === 0) {
allSupportedLanguages = hljs.default.listLanguages()
allSupportedLanguages = hljs.default
.listLanguages()
.concat('csv', 'flow', 'html', 'js', 'markmap', 'abc', 'graphviz', 'mermaid', 'vega-lite')
}
const suggestions = search(term, allSupportedLanguages)
@ -30,15 +32,18 @@ const codeBlockHint = (editor: Editor): Promise<Hints | null> => {
resolve(null)
} else {
resolve({
list: suggestions.map((suggestion: string): Hint => ({
list: suggestions.map(
(suggestion: string): Hint => ({
text: '```' + suggestion + '\n\n```\n',
displayText: suggestion
})),
})
),
from: Pos(cursor.line, searchTerm.start),
to: Pos(cursor.line, searchTerm.end)
})
}
}))
})
)
}
export const CodeBlockHinter: Hinter = {

View file

@ -23,9 +23,11 @@ const collapsableBlockHint = (editor: Editor): Promise<Hints | null> => {
resolve(null)
} else {
resolve({
list: suggestions.map((suggestion: string): Hint => ({
list: suggestions.map(
(suggestion: string): Hint => ({
text: suggestion
})),
})
),
from: Pos(cursor.line, searchTerm.start),
to: Pos(cursor.line, searchTerm.end + 1)
})

View file

@ -13,10 +13,13 @@ const spoilerSuggestion: Hint = {
text: ':::spoiler Toggle label\nToggled content\n::: \n',
displayText: 'spoiler'
}
const suggestions = validAlertLevels.map((suggestion: string): Hint => ({
const suggestions = validAlertLevels
.map(
(suggestion: string): Hint => ({
text: ':::' + suggestion + '\n\n::: \n',
displayText: suggestion
}))
})
)
.concat(spoilerSuggestion)
const containerHint = (editor: Editor): Promise<Hints | null> => {

View file

@ -56,17 +56,15 @@ const generateEmojiHints = async (editor: Editor): Promise<Hints | null> => {
const cursor = editor.getCursor()
const skinTone = await emojiIndex.getPreferredSkinTone()
const emojiEventDetails: EmojiClickEventDetail[] = suggestionList
.filter(emoji => !!emoji.shortcodes)
.filter((emoji) => !!emoji.shortcodes)
.map((emoji) => ({
emoji,
skinTone: skinTone,
unicode: ((emoji as NativeEmoji).unicode ? (emoji as NativeEmoji).unicode : undefined),
unicode: (emoji as NativeEmoji).unicode ? (emoji as NativeEmoji).unicode : undefined,
name: emoji.name
}))
const hints = emojiEventDetails
.map(convertEmojiEventToHint)
.filter(o => !!o) as Hint[]
const hints = emojiEventDetails.map(convertEmojiEventToHint).filter((o) => !!o) as Hint[]
return {
list: hints,
from: Pos(cursor.line, searchTerm.start),

View file

@ -30,10 +30,12 @@ const headerHint = (editor: Editor): Promise<Hints | null> => {
resolve(null)
} else {
resolve({
list: suggestions.map((suggestion): Hint => ({
list: suggestions.map(
(suggestion): Hint => ({
text: allSupportedHeadersTextToInsert[allSupportedHeaders.indexOf(suggestion)],
displayText: suggestion
})),
})
),
from: Pos(cursor.line, searchTerm.start),
to: Pos(cursor.line, searchTerm.end)
})

View file

@ -28,9 +28,11 @@ const imageHint = (editor: Editor): Promise<Hints | null> => {
resolve(null)
} else {
resolve({
list: suggestions.map((suggestion: string): Hint => ({
list: suggestions.map(
(suggestion: string): Hint => ({
text: suggestion
})),
})
),
from: Pos(cursor.line, searchTerm.start),
to: Pos(cursor.line, searchTerm.end + 1)
})

View file

@ -15,13 +15,13 @@ import { LinkAndExtraTagHinter } from './link-and-extra-tag'
import { PDFHinter } from './pdf'
interface findWordAtCursorResponse {
start: number,
end: number,
start: number
end: number
text: string
}
export interface Hinter {
wordRegExp: RegExp,
wordRegExp: RegExp
hint: (editor: Editor) => Promise<Hints | null>
}
@ -40,8 +40,7 @@ export const findWordAtCursor = (editor: Editor): findWordAtCursorResponse => {
}
return {
text: line.slice(start, end)
.toLowerCase(),
text: line.slice(start, end).toLowerCase(),
start: start,
end: end
}
@ -49,9 +48,8 @@ export const findWordAtCursor = (editor: Editor): findWordAtCursorResponse => {
export const search = (term: string, list: string[]): string[] => {
const suggestions: string[] = []
list.forEach(item => {
if (item.toLowerCase()
.startsWith(term.toLowerCase())) {
list.forEach((item) => {
if (item.toLowerCase().startsWith(term.toLowerCase())) {
suggestions.push(item)
}
})

View file

@ -22,7 +22,6 @@ const allSupportedLinks = [
'name',
'time',
'[color=#FFFFFF]'
]
const linkAndExtraTagHint = (editor: Editor): Promise<Hints | null> => {
@ -51,8 +50,7 @@ const linkAndExtraTagHint = (editor: Editor): Promise<Hints | null> => {
case 'time':
// show the current time when the autocompletion is opened and not when the function is loaded
return {
text: `[time=${ DateTime.local()
.toFormat('DDDD T') }]`
text: `[time=${DateTime.local().toFormat('DDDD T')}]`
}
default:
return {

View file

@ -23,9 +23,11 @@ const pdfHint = (editor: Editor): Promise<Hints | null> => {
resolve(null)
} else {
resolve({
list: suggestions.map((suggestion: string): Hint => ({
list: suggestions.map(
(suggestion: string): Hint => ({
text: suggestion
})),
})
),
from: Pos(cursor.line, searchTerm.start),
to: Pos(cursor.line, searchTerm.end + 1)
})

View file

@ -64,8 +64,8 @@ const onChange = (editor: Editor) => {
}
interface DropEvent {
pageX: number,
pageY: number,
pageX: number
pageY: number
dataTransfer: {
files: FileList
effectAllowed: string
@ -73,7 +73,13 @@ interface DropEvent {
preventDefault: () => void
}
export const EditorPane: React.FC<EditorPaneProps & ScrollProps> = ({ onContentChange, content, scrollState, onScroll, onMakeScrollSource }) => {
export const EditorPane: React.FC<EditorPaneProps & ScrollProps> = ({
onContentChange,
content,
scrollState,
onScroll,
onMakeScrollSource
}) => {
const { t } = useTranslation()
const maxLength = useSelector((state: ApplicationState) => state.config.maxDocumentLength)
const smartPasteEnabled = useSelector((state: ApplicationState) => state.editorConfig.smartPaste)
@ -88,7 +94,8 @@ export const EditorPane: React.FC<EditorPaneProps & ScrollProps> = ({ onContentC
const [editorScroll, setEditorScroll] = useState<ScrollInfo>()
const onEditorScroll = useCallback((editor: Editor, data: ScrollInfo) => setEditorScroll(data), [])
const onPaste = useCallback((pasteEditor: Editor, event: PasteEvent) => {
const onPaste = useCallback(
(pasteEditor: Editor, event: PasteEvent) => {
if (!event || !event.clipboardData) {
return
}
@ -99,7 +106,9 @@ export const EditorPane: React.FC<EditorPaneProps & ScrollProps> = ({ onContentC
}
}
handleFilePaste(event, pasteEditor)
}, [smartPasteEnabled])
},
[smartPasteEnabled]
)
useEffect(() => {
if (!editor || !onScroll || !editorScroll) {
@ -112,7 +121,7 @@ export const EditorPane: React.FC<EditorPaneProps & ScrollProps> = ({ onContentC
return
}
const heightOfLine = (lineInfo.handle as { height: number }).height
const percentageRaw = (Math.max(editorScroll.top - startYOfLine, 0)) / heightOfLine
const percentageRaw = Math.max(editorScroll.top - startYOfLine, 0) / heightOfLine
const percentage = Math.floor(percentageRaw * 100)
const newScrollState: ScrollState = { firstLineInView: line + 1, scrolledPercentage: percentage }
@ -125,7 +134,7 @@ export const EditorPane: React.FC<EditorPaneProps & ScrollProps> = ({ onContentC
}
const startYOfLine = editor.heightAtLine(scrollState.firstLineInView - 1, 'local')
const heightOfLine = (editor.lineInfo(scrollState.firstLineInView - 1).handle as { height: number }).height
const newPositionRaw = startYOfLine + (heightOfLine * scrollState.scrolledPercentage / 100)
const newPositionRaw = startYOfLine + (heightOfLine * scrollState.scrolledPercentage) / 100
const newPosition = Math.floor(newPositionRaw)
if (newPosition !== lastScrollPosition.current) {
lastScrollPosition.current = newPosition
@ -133,7 +142,8 @@ export const EditorPane: React.FC<EditorPaneProps & ScrollProps> = ({ onContentC
}
}, [editor, scrollState])
const onBeforeChange = useCallback((editor: Editor, data: EditorChange, value: string) => {
const onBeforeChange = useCallback(
(editor: Editor, data: EditorChange, value: string) => {
if (value.length > maxLength && !maxLengthWarningAlreadyShown.current) {
setShowMaxLengthWarning(true)
maxLengthWarningAlreadyShown.current = true
@ -142,19 +152,34 @@ export const EditorPane: React.FC<EditorPaneProps & ScrollProps> = ({ onContentC
maxLengthWarningAlreadyShown.current = false
}
onContentChange(value)
}, [onContentChange, maxLength, maxLengthWarningAlreadyShown])
const onEditorDidMount = useCallback(mountedEditor => {
},
[onContentChange, maxLength, maxLengthWarningAlreadyShown]
)
const onEditorDidMount = useCallback(
(mountedEditor) => {
setStatusBarInfo(createStatusInfo(mountedEditor, maxLength))
setEditor(mountedEditor)
}, [maxLength])
},
[maxLength]
)
const onCursorActivity = useCallback((editorWithActivity) => {
const onCursorActivity = useCallback(
(editorWithActivity) => {
setStatusBarInfo(createStatusInfo(editorWithActivity, maxLength))
}, [maxLength])
},
[maxLength]
)
const onDrop = useCallback((dropEditor: Editor, event: DropEvent) => {
if (event && dropEditor && event.pageX && event.pageY && event.dataTransfer &&
event.dataTransfer.files && event.dataTransfer.files.length >= 1) {
if (
event &&
dropEditor &&
event.pageX &&
event.pageY &&
event.dataTransfer &&
event.dataTransfer.files &&
event.dataTransfer.files.length >= 1
) {
event.preventDefault()
const top: number = event.pageY
const left: number = event.pageX
@ -167,7 +192,8 @@ export const EditorPane: React.FC<EditorPaneProps & ScrollProps> = ({ onContentC
const onMaxLengthHide = useCallback(() => setShowMaxLengthWarning(false), [])
const codeMirrorOptions: EditorConfiguration = useMemo<EditorConfiguration>(() => ({
const codeMirrorOptions: EditorConfiguration = useMemo<EditorConfiguration>(
() => ({
...editorPreferences,
mode: 'gfm',
viewportMargin: 20,
@ -184,18 +210,16 @@ export const EditorPane: React.FC<EditorPaneProps & ScrollProps> = ({ onContentC
},
autoCloseTags: true,
foldGutter: true,
gutters: [
'CodeMirror-linenumbers',
'authorship-gutters',
'CodeMirror-foldgutter'
],
gutters: ['CodeMirror-linenumbers', 'authorship-gutters', 'CodeMirror-foldgutter'],
extraKeys: defaultKeyMap,
flattenSpans: true,
addModeClass: true,
autoRefresh: true,
// otherCursors: true,
placeholder: t('editor.placeholder')
}), [t, editorPreferences])
}),
[t, editorPreferences]
)
return (
<div className={'d-flex flex-column h-100 position-relative'} onMouseEnter={onMakeScrollSource}>

View file

@ -15,7 +15,7 @@ import {
underlineSelection
} from './tool-bar/utils/toolbarButtonUtils'
const isVim = (keyMapName?: string) => (keyMapName?.substr(0, 3) === 'vim')
const isVim = (keyMapName?: string) => keyMapName?.substr(0, 3) === 'vim'
const f10 = (editor: Editor): void | typeof Pass => editor.setOption('fullScreen', !editor.getOption('fullScreen'))
const esc = (editor: Editor): void | typeof Pass => {
@ -30,8 +30,7 @@ const tab = (editor: Editor) => {
const tab = '\t'
// contruct x length spaces
const spaces = Array((editor.getOption('indentUnit') ?? 0) + 1)
.join(' ')
const spaces = Array((editor.getOption('indentUnit') ?? 0) + 1).join(' ')
// auto indent whole line when in list or blockquote
const cursor = editor.getCursor()
@ -44,9 +43,7 @@ const tab = (editor: Editor) => {
const regex = /^(\s*)(>[> ]*|[*+-]\s|(\d+)([.)]))/
let match
const multiple = editor.getSelection()
.split('\n').length > 1 ||
editor.getSelections().length > 1
const multiple = editor.getSelection().split('\n').length > 1 || editor.getSelections().length > 1
if (multiple) {
editor.execCommand('defaultTab')

View file

@ -34,11 +34,17 @@ export const createStatusInfo = (editor: Editor, maxDocumentLength: number): Sta
remainingCharacters: maxDocumentLength - editor.getValue().length,
linesInDocument: editor.lineCount(),
selectedColumns: editor.getSelection().length,
selectedLines: editor.getSelection()
.split('\n').length
selectedLines: editor.getSelection().split('\n').length
})
export const StatusBar: React.FC<StatusBarInfo> = ({ position, selectedColumns, selectedLines, charactersInDocument, linesInDocument, remainingCharacters }) => {
export const StatusBar: React.FC<StatusBarInfo> = ({
position,
selectedColumns,
selectedLines,
charactersInDocument,
linesInDocument,
remainingCharacters
}) => {
const { t } = useTranslation()
const getLengthTooltip = useMemo(() => {
@ -52,7 +58,7 @@ export const StatusBar: React.FC<StatusBarInfo> = ({ position, selectedColumns,
}, [remainingCharacters, t])
return (
<div className="d-flex flex-row status-bar px-2">
<div className='d-flex flex-row status-bar px-2'>
<div>
<span>{t('editor.statusBar.cursor', { line: position.line + 1, columns: position.ch + 1 })}</span>
<ShowIf condition={selectedColumns !== 0 && selectedLines !== 0}>
@ -64,14 +70,13 @@ export const StatusBar: React.FC<StatusBarInfo> = ({ position, selectedColumns,
</ShowIf>
</ShowIf>
</div>
<div className="ml-auto">
<div className='ml-auto'>
<span>{t('editor.statusBar.lines', { lines: linesInDocument })}</span>
&nbsp;&nbsp;
<span
data-cy={'remainingCharacters'}
title={getLengthTooltip}
className={ remainingCharacters <= 0 ? 'text-danger' : remainingCharacters <= 100 ? 'text-warning' : '' }
>
className={remainingCharacters <= 0 ? 'text-danger' : remainingCharacters <= 100 ? 'text-warning' : ''}>
{t('editor.statusBar.length', { length: charactersInDocument })}
</span>
</div>

View file

@ -7,90 +7,83 @@
import { convertClipboardTableToMarkdown, isTable } from './table-extractor'
describe('isTable detection: ', () => {
it('empty string is no table', () => {
expect(isTable(''))
.toBe(false)
expect(isTable('')).toBe(false)
})
it('single line is no table', () => {
const input = 'some none table'
expect(isTable(input))
.toBe(false)
expect(isTable(input)).toBe(false)
})
it('multiple lines without tabs are no table', () => {
const input = 'some none table\nanother line'
expect(isTable(input))
.toBe(false)
expect(isTable(input)).toBe(false)
})
it('code blocks are no table', () => {
const input = '```python\ndef a:\n\tprint("a")\n\tprint("b")```'
expect(isTable(input))
.toBe(false)
expect(isTable(input)).toBe(false)
})
it('tab-indented text is no table', () => {
const input = '\tsome tab indented text\n\tabc\n\tdef'
expect(isTable(input))
.toBe(false)
expect(isTable(input)).toBe(false)
})
it('not equal number of tabs is no table', () => {
const input = '1 ...\n2\tabc\n3\td\te\tf\n4\t16'
expect(isTable(input))
.toBe(false)
expect(isTable(input)).toBe(false)
})
it('table without newline at end is valid', () => {
const input = '1\t1\n2\t4\n3\t9\n4\t16\n5\t25'
expect(isTable(input))
.toBe(true)
expect(isTable(input)).toBe(true)
})
it('table with newline at end is valid', () => {
const input = '1\t1\n2\t4\n3\t9\n4\t16\n5\t25\n'
expect(isTable(input))
.toBe(true)
expect(isTable(input)).toBe(true)
})
it('table with some first cells missing is valid', () => {
const input = '1\t1\n\t0\n\t0\n4\t16\n5\t25\n'
expect(isTable(input))
.toBe(true)
expect(isTable(input)).toBe(true)
})
it('table with some last cells missing is valid', () => {
const input = '1\t1\n2\t\n3\t\n4\t16\n'
expect(isTable(input))
.toBe(true)
expect(isTable(input)).toBe(true)
})
})
describe('Conversion from clipboard table to markdown format', () => {
it('normal table without newline at end converts right', () => {
const input = '1\t1\ta\n2\t4\tb\n3\t9\tc\n4\t16\td'
expect(convertClipboardTableToMarkdown(input))
.toEqual('| #1 | #2 | #3 |\n| -- | -- | -- |\n| 1 | 1 | a |\n| 2 | 4 | b |\n| 3 | 9 | c |\n| 4 | 16 | d |')
expect(convertClipboardTableToMarkdown(input)).toEqual(
'| #1 | #2 | #3 |\n| -- | -- | -- |\n| 1 | 1 | a |\n| 2 | 4 | b |\n| 3 | 9 | c |\n| 4 | 16 | d |'
)
})
it('normal table with newline at end converts right', () => {
const input = '1\t1\n2\t4\n3\t9\n4\t16\n'
expect(convertClipboardTableToMarkdown(input))
.toEqual('| #1 | #2 |\n| -- | -- |\n| 1 | 1 |\n| 2 | 4 |\n| 3 | 9 |\n| 4 | 16 |')
expect(convertClipboardTableToMarkdown(input)).toEqual(
'| #1 | #2 |\n| -- | -- |\n| 1 | 1 |\n| 2 | 4 |\n| 3 | 9 |\n| 4 | 16 |'
)
})
it('table with some first cells missing converts right', () => {
const input = '1\t1\n\t0\n\t0\n4\t16\n'
expect(convertClipboardTableToMarkdown(input))
.toEqual('| #1 | #2 |\n| -- | -- |\n| 1 | 1 |\n| | 0 |\n| | 0 |\n| 4 | 16 |')
expect(convertClipboardTableToMarkdown(input)).toEqual(
'| #1 | #2 |\n| -- | -- |\n| 1 | 1 |\n| | 0 |\n| | 0 |\n| 4 | 16 |'
)
})
it('table with some last cells missing converts right', () => {
const input = '1\t1\n2\t\n3\t\n4\t16\n'
expect(convertClipboardTableToMarkdown(input))
.toEqual('| #1 | #2 |\n| -- | -- |\n| 1 | 1 |\n| 2 | |\n| 3 | |\n| 4 | 16 |')
expect(convertClipboardTableToMarkdown(input)).toEqual(
'| #1 | #2 |\n| -- | -- |\n| 1 | 1 |\n| 2 | |\n| 3 | |\n| 4 | 16 |'
)
})
it('empty input results in empty output', () => {

View file

@ -16,42 +16,34 @@ export const isTable = (text: string): boolean => {
return false
}
const lines = text.split(/\r?\n/)
.filter(line => line.trim() !== '')
const lines = text.split(/\r?\n/).filter((line) => line.trim() !== '')
// Tab-indented text should not be matched as a table
if (lines.every(line => line.startsWith('\t'))) {
if (lines.every((line) => line.startsWith('\t'))) {
return false
}
// Every line should have the same amount of tabs (table columns)
const tabsPerLines = lines.map(line => line.match(/\t/g)?.length ?? 0)
return tabsPerLines.every(line => line === tabsPerLines[0])
const tabsPerLines = lines.map((line) => line.match(/\t/g)?.length ?? 0)
return tabsPerLines.every((line) => line === tabsPerLines[0])
}
export const convertClipboardTableToMarkdown = (pasteData: string): string => {
if (pasteData.trim() === '') {
return ''
}
const tableRows = pasteData.split(/\r?\n/)
.filter(row => row.trim() !== '')
const tableRows = pasteData.split(/\r?\n/).filter((row) => row.trim() !== '')
const tableCells = tableRows.reduce((cellsInRow, row, index) => {
cellsInRow[index] = row.split('\t')
return cellsInRow
}, [] as string[][])
const arrayMaxRows = createNumberRangeArray(tableCells.length)
const arrayMaxColumns = createNumberRangeArray(Math.max(...tableCells.map(row => row.length)))
const arrayMaxColumns = createNumberRangeArray(Math.max(...tableCells.map((row) => row.length)))
const headRow1 = arrayMaxColumns
.map(col => `| #${ col + 1 } `)
.join('') + '|'
const headRow2 = arrayMaxColumns
.map(col => `| -${ '-'.repeat((col + 1).toString().length) } `)
.join('') + '|'
const headRow1 = arrayMaxColumns.map((col) => `| #${col + 1} `).join('') + '|'
const headRow2 = arrayMaxColumns.map((col) => `| -${'-'.repeat((col + 1).toString().length)} `).join('') + '|'
const body = arrayMaxRows
.map(row => {
return arrayMaxColumns
.map(col => '| ' + tableCells[row][col] + ' ')
.join('') + '|'
.map((row) => {
return arrayMaxColumns.map((col) => '| ' + tableCells[row][col] + ' ').join('') + '|'
})
.join('\n')
return `${headRow1}\n${headRow2}\n${body}`

View file

@ -19,28 +19,33 @@ export interface EditorPreferenceBooleanProps {
}
export const EditorPreferenceBooleanProperty: React.FC<EditorPreferenceBooleanProps> = ({ property }) => {
const preference = useSelector((state: ApplicationState) => state.editorConfig.preferences[property]?.toString() || '', equal)
const preference = useSelector(
(state: ApplicationState) => state.editorConfig.preferences[property]?.toString() || '',
equal
)
const { t } = useTranslation()
const selectItem = useCallback((event: ChangeEvent<HTMLSelectElement>) => {
const selectItem = useCallback(
(event: ChangeEvent<HTMLSelectElement>) => {
const selectedItem: boolean = event.target.value === 'true'
mergeEditorPreferences({
[property]: selectedItem
} as EditorConfiguration)
}, [property])
},
[property]
)
const i18nPrefix = `editor.modal.preferences.${property}`
return (
<EditorPreferenceInput onChange={ selectItem } property={ property } type={ EditorPreferenceInputType.SELECT }
<EditorPreferenceInput
onChange={selectItem}
property={property}
type={EditorPreferenceInputType.SELECT}
value={preference}>
<option value={ 'true' }>
{ t(`${ i18nPrefix }.on`) }
</option>
<option value={ 'false' }>
{ t(`${ i18nPrefix }.off`) }
</option>
<option value={'true'}>{t(`${i18nPrefix}.on`)}</option>
<option value={'false'}>{t(`${i18nPrefix}.off`)}</option>
</EditorPreferenceInput>
)
}

View file

@ -20,13 +20,20 @@ export interface EditorPreferenceInputProps {
value?: string | number | string[]
}
export const EditorPreferenceInput: React.FC<EditorPreferenceInputProps> = ({ property, type, onChange, value, children }) => {
export const EditorPreferenceInput: React.FC<EditorPreferenceInputProps> = ({
property,
type,
onChange,
value,
children
}) => {
useTranslation()
return (
<Form.Group controlId={`editor-pref-${property}`}>
<Form.Label>
<Trans
i18nKey={ `editor.modal.preferences.${ property }${ type === EditorPreferenceInputType.NUMBER ? '' : '.label' }` }/>
i18nKey={`editor.modal.preferences.${property}${type === EditorPreferenceInputType.NUMBER ? '' : '.label'}`}
/>
</Form.Label>
<Form.Control
as={type === EditorPreferenceInputType.NUMBER ? 'input' : 'select'}

View file

@ -11,8 +11,7 @@ import { setEditorLigatures } from '../../../../../redux/editor/methods'
import { EditorPreferenceInput, EditorPreferenceInputType } from './editor-preference-input'
export const EditorPreferenceLigaturesSelect: React.FC = () => {
const ligaturesEnabled = useSelector((state: ApplicationState) => Boolean(state.editorConfig.ligatures)
.toString())
const ligaturesEnabled = useSelector((state: ApplicationState) => Boolean(state.editorConfig.ligatures).toString())
const saveLigatures = useCallback((event: ChangeEvent<HTMLSelectElement>) => {
const ligaturesActivated: boolean = event.target.value === 'true'
setEditorLigatures(ligaturesActivated)
@ -20,7 +19,10 @@ export const EditorPreferenceLigaturesSelect: React.FC = () => {
const { t } = useTranslation()
return (
<EditorPreferenceInput onChange={ saveLigatures } value={ ligaturesEnabled } property={ 'ligatures' }
<EditorPreferenceInput
onChange={saveLigatures}
value={ligaturesEnabled}
property={'ligatures'}
type={EditorPreferenceInputType.BOOLEAN}>
<option value='true'>{t(`common.yes`)}</option>
<option value='false'>{t(`common.no`)}</option>

View file

@ -18,18 +18,28 @@ export interface EditorPreferenceNumberProps {
}
export const EditorPreferenceNumberProperty: React.FC<EditorPreferenceNumberProps> = ({ property }) => {
const preference = useSelector((state: ApplicationState) => state.editorConfig.preferences[property]?.toString() || '', equal)
const preference = useSelector(
(state: ApplicationState) => state.editorConfig.preferences[property]?.toString() || '',
equal
)
const selectItem = useCallback((event: ChangeEvent<HTMLSelectElement>) => {
const selectItem = useCallback(
(event: ChangeEvent<HTMLSelectElement>) => {
const selectedItem: number = Number.parseInt(event.target.value)
mergeEditorPreferences({
[property]: selectedItem
} as EditorConfiguration)
}, [property])
},
[property]
)
return (
<EditorPreferenceInput onChange={ selectItem } property={ property } type={ EditorPreferenceInputType.NUMBER }
value={ preference }/>
<EditorPreferenceInput
onChange={selectItem}
property={property}
type={EditorPreferenceInputType.NUMBER}
value={preference}
/>
)
}

View file

@ -19,28 +19,41 @@ export interface EditorPreferenceSelectPropertyProps {
selections: string[]
}
export const EditorPreferenceSelectProperty: React.FC<EditorPreferenceSelectPropertyProps> = ({ property, selections }) => {
const preference = useSelector((state: ApplicationState) => state.editorConfig.preferences[property]?.toString() || '', equal)
export const EditorPreferenceSelectProperty: React.FC<EditorPreferenceSelectPropertyProps> = ({
property,
selections
}) => {
const preference = useSelector(
(state: ApplicationState) => state.editorConfig.preferences[property]?.toString() || '',
equal
)
const { t } = useTranslation()
const selectItem = useCallback((event: ChangeEvent<HTMLSelectElement>) => {
const selectItem = useCallback(
(event: ChangeEvent<HTMLSelectElement>) => {
const selectedItem: string = event.target.value
mergeEditorPreferences({
[property]: selectedItem
} as EditorConfiguration)
}, [property])
},
[property]
)
const i18nPrefix = `editor.modal.preferences.${property}`
return (
<EditorPreferenceInput onChange={ selectItem } property={ property } type={ EditorPreferenceInputType.SELECT }
<EditorPreferenceInput
onChange={selectItem}
property={property}
type={EditorPreferenceInputType.SELECT}
value={preference}>
{ selections.map(selection =>
{selections.map((selection) => (
<option key={selection} value={selection}>
{t(`${i18nPrefix}.${selection}`)}
</option>) }
</option>
))}
</EditorPreferenceInput>
)
}

View file

@ -11,8 +11,7 @@ import { setEditorSmartPaste } from '../../../../../redux/editor/methods'
import { EditorPreferenceInput, EditorPreferenceInputType } from './editor-preference-input'
export const EditorPreferenceSmartPasteSelect: React.FC = () => {
const smartPasteEnabled = useSelector((state: ApplicationState) => Boolean(state.editorConfig.smartPaste)
.toString())
const smartPasteEnabled = useSelector((state: ApplicationState) => Boolean(state.editorConfig.smartPaste).toString())
const saveSmartPaste = useCallback((event: ChangeEvent<HTMLSelectElement>) => {
const smartPasteActivated: boolean = event.target.value === 'true'
setEditorSmartPaste(smartPasteActivated)
@ -24,8 +23,7 @@ export const EditorPreferenceSmartPasteSelect: React.FC = () => {
onChange={saveSmartPaste}
value={smartPasteEnabled}
property={'smartPaste'}
type={ EditorPreferenceInputType.BOOLEAN }
>
type={EditorPreferenceInputType.BOOLEAN}>
<option value='true'>{t(`common.yes`)}</option>
<option value='false'>{t(`common.no`)}</option>
</EditorPreferenceInput>

View file

@ -24,12 +24,15 @@ import { EditorPreferenceSmartPasteSelect } from './editor-preference-smart-past
export const EditorPreferences: React.FC = () => {
const { t } = useTranslation()
const [showModal, setShowModal] = useState(false)
const indentWithTabs = useSelector((state: ApplicationState) => state.editorConfig.preferences.indentWithTabs ?? false, equal)
const indentWithTabs = useSelector(
(state: ApplicationState) => state.editorConfig.preferences.indentWithTabs ?? false,
equal
)
return (
<Fragment>
<Button variant='light' onClick={() => setShowModal(true)} title={t('editor.editorToolbar.preferences')}>
<ForkAwesomeIcon icon="wrench"/>
<ForkAwesomeIcon icon='wrench' />
</Button>
<CommonModal
show={showModal}
@ -40,12 +43,16 @@ export const EditorPreferences: React.FC = () => {
<Form>
<ListGroup>
<ListGroup.Item>
<EditorPreferenceSelectProperty property={ EditorPreferenceProperty.THEME }
selections={ ['one-dark', 'neat'] }/>
<EditorPreferenceSelectProperty
property={EditorPreferenceProperty.THEME}
selections={['one-dark', 'neat']}
/>
</ListGroup.Item>
<ListGroup.Item>
<EditorPreferenceSelectProperty property={ EditorPreferenceProperty.KEYMAP }
selections={ ['sublime', 'emacs', 'vim'] }/>
<EditorPreferenceSelectProperty
property={EditorPreferenceProperty.KEYMAP}
selections={['sublime', 'emacs', 'vim']}
/>
</ListGroup.Item>
<ListGroup.Item>
<EditorPreferenceBooleanProperty property={EditorPreferenceProperty.INDENT_WITH_TABS} />
@ -62,7 +69,8 @@ export const EditorPreferences: React.FC = () => {
<EditorPreferenceSmartPasteSelect />
</ListGroup.Item>
<ListGroup.Item>
<EditorPreferenceInput onChange={ () => alert('This feature is not yet implemented.') }
<EditorPreferenceInput
onChange={() => alert('This feature is not yet implemented.')}
property={EditorPreferenceProperty.SPELL_CHECK}
type={EditorPreferenceInputType.SELECT}>
<option value='off'>Off</option>

View file

@ -28,10 +28,14 @@ export const EmojiPickerButton: React.FC<EmojiPickerButtonProps> = ({ editor })
setShowEmojiPicker(false)
addEmoji(emoji, editor)
}}
onDismiss={ () => setShowEmojiPicker(false) }/>
<Button data-cy={ 'show-emoji-picker' } variant='light' onClick={ () => setShowEmojiPicker(old => !old) }
onDismiss={() => setShowEmojiPicker(false)}
/>
<Button
data-cy={'show-emoji-picker'}
variant='light'
onClick={() => setShowEmojiPicker((old) => !old)}
title={t('editor.editorToolbar.emoji')}>
<ForkAwesomeIcon icon="smile-o"/>
<ForkAwesomeIcon icon='smile-o' />
</Button>
</Fragment>
)

View file

@ -19,8 +19,7 @@ export interface EmojiPickerProps {
onDismiss: () => void
}
export const customEmojis: CustomEmoji[] = Object.keys(ForkAwesomeIcons)
.map((name) => ({
export const customEmojis: CustomEmoji[] = Object.keys(ForkAwesomeIcons).map((name) => ({
name: `fa-${name}`,
shortcodes: [`fa-${name.toLowerCase()}`],
url: forkawesomeIcon,
@ -93,7 +92,6 @@ export const EmojiPicker: React.FC<EmojiPickerProps> = ({ show, onEmojiSelected,
}, [darkModeEnabled])
return (
<div className={ `position-absolute emoji-picker-container ${ !show ? 'd-none' : '' }` }
ref={ pickerContainerRef }/>
<div className={`position-absolute emoji-picker-container ${!show ? 'd-none' : ''}`} ref={pickerContainerRef} />
)
}

View file

@ -51,7 +51,7 @@ export const CustomTableSizeModal: React.FC<CustomTableSizeModalProps> = ({ show
isInvalid={tableSize.columns <= 0}
onChange={(event) => {
const value = Number.parseInt(event.currentTarget.value)
setTableSize(old => ({
setTableSize((old) => ({
rows: old.rows,
columns: isNaN(value) ? 0 : value
}))
@ -65,11 +65,12 @@ export const CustomTableSizeModal: React.FC<CustomTableSizeModalProps> = ({ show
isInvalid={tableSize.rows <= 0}
onChange={(event) => {
const value = Number.parseInt(event.currentTarget.value)
setTableSize(old => ({
setTableSize((old) => ({
rows: isNaN(value) ? 0 : value,
columns: old.columns
}))
} }/>
}}
/>
</div>
<ModalFooter>
<Button onClick={onClick} disabled={tableSize.rows <= 0 || tableSize.columns <= 0}>

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