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

@ -9,7 +9,7 @@ import { defaultFetchConfig, expectResponseCode, getApiUrl } from '../utils'
import { isMockMode } from '../../utils/test-modes'
export const getMe = async (): Promise<UserResponse> => {
const response = await fetch(getApiUrl() + `me${ isMockMode() ? '-get' : '' }`, {
const response = await fetch(getApiUrl() + `me${isMockMode() ? '-get' : ''}`, {
...defaultFetchConfig
})
expectResponseCode(response)

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

@ -11,15 +11,15 @@ import { isMockMode } from '../../utils/test-modes'
export const getNote = async (noteId: string): Promise<NoteDto> => {
// The "-get" suffix is necessary, because in our mock api (filesystem) the note id might already be a folder.
// TODO: [mrdrogdrog] replace -get with actual api route as soon as api backend is ready.
const response = await fetch(getApiUrl() + `notes/${ noteId }${ isMockMode() ? '-get' : '' }`, {
const response = await fetch(getApiUrl() + `notes/${noteId}${isMockMode() ? '-get' : ''}`, {
...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> => {
const response = await fetch(getApiUrl() + `notes/${ noteId }`, {
const response = await fetch(getApiUrl() + `notes/${noteId}`, {
...defaultFetchConfig,
method: 'DELETE'
})

View file

@ -11,24 +11,24 @@ import { Revision, RevisionListEntry } from './types'
const revisionCache = new Cache<string, Revision>(3600)
export const getRevision = async (noteId: string, timestamp: number): Promise<Revision> => {
const cacheKey = `${ noteId }:${ timestamp }`
const cacheKey = `${noteId}:${timestamp}`
if (revisionCache.has(cacheKey)) {
return revisionCache.get(cacheKey)
}
const response = await fetch(getApiUrl() + `notes/${ noteId }/revisions/${ timestamp }`, {
const response = await fetch(getApiUrl() + `notes/${noteId}/revisions/${timestamp}`, {
...defaultFetchConfig
})
expectResponseCode(response)
const revisionData = await response.json() as Revision
const revisionData = (await response.json()) as Revision
revisionCache.put(cacheKey, revisionData)
return revisionData
}
export const getAllRevisions = async (noteId: string): Promise<RevisionListEntry[]> => {
// TODO Change 'revisions-list' to 'revisions' as soon as the backend is ready to serve some data!
const response = await fetch(getApiUrl() + `notes/${ noteId }/revisions-list`, {
const response = await fetch(getApiUrl() + `notes/${noteId}/revisions-list`, {
...defaultFetchConfig
})
expectResponseCode(response)
return await response.json() as Promise<RevisionListEntry[]>
return (await response.json()) as Promise<RevisionListEntry[]>
}

View file

@ -8,25 +8,25 @@ import { defaultFetchConfig, expectResponseCode, getApiUrl } from '../utils'
import { AccessToken, AccessTokenSecret } from './types'
export const getAccessTokenList = async (): Promise<AccessToken[]> => {
const response = await fetch(`${ getApiUrl() }tokens`, {
const response = await fetch(`${getApiUrl()}tokens`, {
...defaultFetchConfig
})
expectResponseCode(response)
return await response.json() as AccessToken[]
return (await response.json()) as AccessToken[]
}
export const postNewAccessToken = async (label: string): Promise<AccessToken & AccessTokenSecret> => {
const response = await fetch(`${ getApiUrl() }tokens`, {
const response = await fetch(`${getApiUrl()}tokens`, {
...defaultFetchConfig,
method: 'POST',
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> => {
const response = await fetch(`${ getApiUrl() }tokens/${ timestamp }`, {
const response = await fetch(`${getApiUrl()}tokens/${timestamp}`, {
...defaultFetchConfig,
method: 'DELETE'
})

View file

@ -14,7 +14,7 @@ export const getUserById = async (userid: string): Promise<UserResponse> => {
if (cache.has(userid)) {
return cache.get(userid)
}
const response = await fetch(`${ getApiUrl() }/users/${ userid }`, {
const response = await fetch(`${getApiUrl()}/users/${userid}`, {
...defaultFetchConfig
})
expectResponseCode(response)

View file

@ -24,6 +24,6 @@ export const getApiUrl = (): string => {
export const expectResponseCode = (response: Response, code = 200): void => {
if (response.status !== code) {
throw new Error(`response code is not ${ code }`)
throw new Error(`response code is not ${code}`)
}
}

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,28 +28,25 @@ 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) => {
console.error(reason)
setFailedTitle(task.name)
})
runTask(task.task).catch((reason: Error) => {
console.error(reason)
setFailedTitle(task.name)
})
}
}, [initTasks, runTask])
const tasksAreRunning = doneTasks < initTasks.length || initTasks.length === 0
if (tasksAreRunning) {
return <LoadingScreen failedTitle={ failedTitle }/>
return <LoadingScreen failedTitle={failedTitle} />
} else {
return <Suspense fallback={ (<LoadingScreen/>) }>
{ children }
</Suspense>
return <Suspense fallback={<LoadingScreen />}>{children}</Suspense>
}
}

View file

@ -11,7 +11,7 @@ export const BANNER_LOCAL_STORAGE_KEY = 'banner.lastModified'
export const fetchAndSetBanner = async (customizeAssetsUrl: string): Promise<void> => {
const cachedLastModified = window.localStorage.getItem(BANNER_LOCAL_STORAGE_KEY)
const bannerUrl = `${ customizeAssetsUrl }banner.txt`
const bannerUrl = `${customizeAssetsUrl}banner.txt`
if (cachedLastModified) {
const response = await fetch(bannerUrl, {

View file

@ -19,7 +19,7 @@ export const setUpI18n = async (frontendAssetsUrl: string): Promise<void> => {
fallbackLng: 'en',
debug: process.env.NODE_ENV !== 'production',
backend: {
loadPath: `${ frontendAssetsUrl }locales/{{lng}}.json`
loadPath: `${frontendAssetsUrl}locales/{{lng}}.json`
},
interpolation: {

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/`
apiUrl: `${backendBaseUrl}api/private/`
})
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()
}]
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">
<span className={ `d-block ${ failedTitle ? 'animation-shake' : 'animation-jump' }` }>
<HedgeDocLogo size={ HedgeDocLogoSize.BIG }/>
<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/>
<ShowIf condition={!!failedTitle}>
<Alert variant={'danger'}>
The task '{failedTitle}' failed.
<br />
For further information look into the browser console.
</Alert>
</ShowIf>

View file

@ -21,20 +21,20 @@ export const Branding: React.FC<BrandingProps> = ({ inline = false, delimiter =
const showBranding = !!branding.name || !!branding.logo
return (
<ShowIf condition={ showBranding }>
<ShowIf condition={ delimiter }>
<strong className={ `mx-1 ${ inline ? 'inline-size' : 'regular-size' }` }>@</strong>
<ShowIf condition={showBranding}>
<ShowIf condition={delimiter}>
<strong className={`mx-1 ${inline ? 'inline-size' : 'regular-size'}`}>@</strong>
</ShowIf>
{
branding.logo
? <img
src={ branding.logo }
alt={ branding.name }
title={ branding.name }
className={ inline ? 'inline-size' : 'regular-size' }
/>
: branding.name
}
{branding.logo ? (
<img
src={branding.logo}
alt={branding.name}
title={branding.name}
className={inline ? 'inline-size' : 'regular-size'}
/>
) : (
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,20 +22,21 @@ export const CopyOverlay: React.FC<CopyOverlayProps> = ({ content, clickComponen
const [tooltipId] = useState<string>(() => uuid())
const copyToClipboard = useCallback((content: string) => {
navigator.clipboard.writeText(content)
.then(() => {
setError(false)
})
.catch(() => {
setError(true)
console.error('couldn\'t copy')
})
.finally(() => {
setShowCopiedTooltip(true)
setTimeout(() => {
setShowCopiedTooltip(false)
}, 2000)
})
navigator.clipboard
.writeText(content)
.then(() => {
setError(false)
})
.catch(() => {
setError(true)
console.error("couldn't copy")
})
.finally(() => {
setShowCopiedTooltip(true)
setTimeout(() => {
setShowCopiedTooltip(false)
}, 2000)
})
}, [])
useEffect(() => {
@ -51,17 +52,17 @@ export const CopyOverlay: React.FC<CopyOverlayProps> = ({ content, clickComponen
}, [clickComponent, copyToClipboard, content])
return (
<Overlay target={ clickComponent } show={ showCopiedTooltip } placement="top">
{ (props) => (
<Tooltip id={ `copied_${ tooltipId }` } { ...props }>
<ShowIf condition={ error }>
<Trans i18nKey={ 'common.copyError' }/>
<Overlay target={clickComponent} show={showCopiedTooltip} placement='top'>
{(props) => (
<Tooltip id={`copied_${tooltipId}`} {...props}>
<ShowIf condition={error}>
<Trans i18nKey={'common.copyError'} />
</ShowIf>
<ShowIf condition={ !error }>
<Trans i18nKey={ 'common.successfullyCopied' }/>
<ShowIf condition={!error}>
<Trans i18nKey={'common.successfullyCopied'} />
</ShowIf>
</Tooltip>
) }
)}
</Overlay>
)
}

View file

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

View file

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

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 }` }/>
)
const extraClasses = `${className ?? ''} ${sizeClass || stackClass ? `fa${stackClass}${sizeClass}` : ''}`
return <i className={`fa ${fixedWithClass} fa-${icon} ${extraClasses}`} />
}

View file

@ -15,15 +15,13 @@ export interface ForkAwesomeStackProps {
export const ForkAwesomeStack: React.FC<ForkAwesomeStackProps> = ({ size, children }) => {
return (
<span className={ `fa-stack ${ size ? 'fa-' : '' }${ size ?? '' }` }>
{
React.Children.map(children, (child) => {
if (!React.isValidElement<ForkAwesomeIconProps>(child)) {
return null
}
return <ForkAwesomeIcon { ...child.props } stacked={ true }/>
})
}
<span className={`fa-stack ${size ? 'fa-' : ''}${size ?? ''}`}>
{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
}
@ -29,11 +29,11 @@ export enum HedgeDocLogoType {
export const HedgeDocLogoWithText: React.FC<HedgeDocLogoProps> = ({ size = HedgeDocLogoSize.MEDIUM, logoType }) => {
switch (logoType) {
case HedgeDocLogoType.COLOR_VERTICAL:
return <LogoColorVertical className={ 'w-auto' } title={ 'HedgeDoc logo with text' } style={ { height: size } }/>
return <LogoColorVertical className={'w-auto'} title={'HedgeDoc logo with text'} style={{ height: size }} />
case HedgeDocLogoType.BW_HORIZONTAL:
return <LogoBwHorizontal className={ 'w-auto' } title={ 'HedgeDoc logo with text' } style={ { height: size } }/>
return <LogoBwHorizontal className={'w-auto'} title={'HedgeDoc logo with text'} style={{ height: size }} />
case HedgeDocLogoType.WB_HORIZONTAL:
return <LogoWbHorizontal className={ 'w-auto' } title={ 'HedgeDoc logo with text' } style={ { height: size } }/>
return <LogoWbHorizontal className={'w-auto'} title={'HedgeDoc logo with text'} style={{ height: size }} />
default:
return null
}

View file

@ -18,5 +18,5 @@ export interface HedgeDocLogoProps {
}
export const HedgeDocLogo: React.FC<HedgeDocLogoProps> = ({ size = HedgeDocLogoSize.MEDIUM }) => {
return <LogoColor className={ 'w-auto' } title={ 'HedgeDoc logo with text' } style={ { height: size } }/>
return <LogoColor className={'w-auto'} title={'HedgeDoc logo with text'} style={{ height: size }} />
}

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 }
className={ `btn-icon p-0 d-inline-flex align-items-stretch ${ border ? 'with-border' : '' } ${ className ?? '' }` }>
<span className="icon-part d-flex align-items-center">
<ForkAwesomeIcon icon={ icon } fixedWidth={ iconFixedWidth } className={ 'icon' }/>
<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'>
<ForkAwesomeIcon icon={icon} fixedWidth={iconFixedWidth} className={'icon'} />
</span>
<ShowIf condition={ !!children }>
<span className="text-part d-flex align-items-center">
{ children }
</span>
<ShowIf condition={!!children}>
<span className='text-part d-flex align-items-center'>{children}</span>
</ShowIf>
</Button>
)

View file

@ -14,8 +14,8 @@ export interface TranslatedIconButtonProps extends IconButtonProps {
export const TranslatedIconButton: React.FC<TranslatedIconButtonProps> = ({ i18nKey, ...props }) => {
return (
<IconButton { ...props }>
<Trans i18nKey={ i18nKey }/>
<IconButton {...props}>
<Trans i18nKey={i18nKey} />
</IconButton>
)
}

View file

@ -10,20 +10,21 @@ 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'
>
<ShowIf condition={ !!icon }>
<ForkAwesomeIcon icon={ icon as IconName } fixedWidth={ true }/>&nbsp;
<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;
</ShowIf>
{ text }
{text}
</a>
)
}

View file

@ -11,17 +11,21 @@ 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 }>
<ShowIf condition={ !!icon }>
<ForkAwesomeIcon icon={ icon as IconName } fixedWidth={ true }/>&nbsp;
<Link to={href} className={className} id={id} title={title}>
<ShowIf condition={!!icon}>
<ForkAwesomeIcon icon={icon as IconName} fixedWidth={true} />
&nbsp;
</ShowIf>
{ text }
{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,18 +9,15 @@ 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
}
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'/>
}
<Button variant='dark' size='sm' onClick={() => onLockedChanged(!locked)} title={title}>
{locked ? <ForkAwesomeIcon icon='lock' /> : <ForkAwesomeIcon icon='unlock' />}
</Button>
)
}

View file

@ -23,24 +23,38 @@ 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.Header closeButton={ !!closeButton }>
<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;
<ShowIf condition={!!icon}>
<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 }
{children}
</Modal>
)
}

View file

@ -14,17 +14,23 @@ 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>
<CommonModal show={show} onHide={onHide} titleI18nKey={titleI18nKey} icon={icon} closeButton={true}>
<Modal.Body className='text-dark'>{children}</Modal.Body>
<Modal.Footer>
<Button variant="danger" onClick={ onConfirm }>
<Trans i18nKey={ deletionButtonI18nKey }/>
<Button variant='danger' onClick={onConfirm}>
<Trans i18nKey={deletionButtonI18nKey} />
</Button>
</Modal.Footer>
</CommonModal>

View file

@ -10,10 +10,8 @@ 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>
<CommonModal show={show} onHide={onHide} titleI18nKey={titleI18nKey} icon={icon} closeButton={true}>
<Modal.Body className='text-dark text-center'>{children}</Modal.Body>
</CommonModal>
)
}

View file

@ -31,22 +31,18 @@ export const MotdBanner: React.FC = () => {
}
if (!bannerState.text) {
return <span data-cy={ 'no-motd-banner' }/>
return <span data-cy={'no-motd-banner'} />
}
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,9 +13,9 @@ 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) }>
{ index + 1 }
<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,55 +33,38 @@ export const PagerPagination: React.FC<PaginationProps> = ({ numberOfPageButtons
onPageChange(pageIndex)
}, [onPageChange, pageIndex])
const correctedLowerPageIndex =
Math.min(
Math.max(
Math.min(
wantedLowerPageIndex,
wantedLowerPageIndex + lastPageIndex - wantedUpperPageIndex
),
0
),
lastPageIndex
)
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
),
0
)
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 itemIndex = correctedLowerPageIndex + index
return <PagerItem key={ itemIndex } index={ itemIndex }
onClick={ setPageIndex }/>
})
const paginationItemsBefore = Array.from(new Array(correctedPageIndex - correctedLowerPageIndex)).map((k, index) => {
const itemIndex = correctedLowerPageIndex + index
return <PagerItem key={itemIndex} index={itemIndex} onClick={setPageIndex} />
})
const paginationItemsAfter = Array.from(new Array(correctedUpperPageIndex - correctedPageIndex))
.map((k, index) => {
const itemIndex = correctedPageIndex + index + 1
return <PagerItem key={ itemIndex } index={ itemIndex } onClick={ setPageIndex }/>
})
const paginationItemsAfter = Array.from(new Array(correctedUpperPageIndex - correctedPageIndex)).map((k, index) => {
const itemIndex = correctedPageIndex + index + 1
return <PagerItem key={itemIndex} index={itemIndex} onClick={setPageIndex} />
})
return (
<Pagination dir='ltr'>
<ShowIf condition={ correctedLowerPageIndex > 0 }>
<PagerItem key={ 0 } index={ 0 } onClick={ setPageIndex }/>
<Pagination.Ellipsis disabled/>
<ShowIf condition={correctedLowerPageIndex > 0}>
<PagerItem key={0} index={0} onClick={setPageIndex} />
<Pagination.Ellipsis disabled />
</ShowIf>
{ paginationItemsBefore }
<Pagination.Item active>{ correctedPageIndex + 1 }</Pagination.Item>
{ paginationItemsAfter }
<ShowIf condition={ correctedUpperPageIndex < lastPageIndex }>
<Pagination.Ellipsis disabled/>
<PagerItem key={ lastPageIndex } index={ lastPageIndex } onClick={ setPageIndex }/>
{paginationItemsBefore}
<Pagination.Item active>{correctedPageIndex + 1}</Pagination.Item>
{paginationItemsAfter}
<ShowIf condition={correctedUpperPageIndex < lastPageIndex}>
<Pagination.Ellipsis disabled />
<PagerItem key={lastPageIndex} index={lastPageIndex} onClick={setPageIndex} />
</ShowIf>
</Pagination>
)

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)
})
}
</Fragment>
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,5 +11,5 @@ export interface ShowIfProps {
}
export const ShowIf: React.FC<ShowIfProps> = ({ children, condition }) => {
return condition ? <Fragment>{ children }</Fragment> : null
return condition ? <Fragment>{children}</Fragment> : null
}

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
}
@ -21,15 +21,15 @@ const UserAvatar: React.FC<UserAvatarProps> = ({ name, photo, size, additionalCl
const { t } = useTranslation()
return (
<span className={ 'd-inline-flex align-items-center ' + additionalClasses }>
<span className={'d-inline-flex align-items-center ' + additionalClasses}>
<img
src={ photo }
className={ `user-avatar rounded mr-1 ${ size ?? '' }` }
alt={ t('common.avatarOf', { name }) }
title={ name }
src={photo}
className={`user-avatar rounded mr-1 ${size ?? ''}`}
alt={t('common.avatarOf', { name })}
title={name}
/>
<ShowIf condition={ showName }>
<span className="mx-1 user-line-name">{ name }</span>
<ShowIf condition={showName}>
<span className='mx-1 user-line-name'>{name}</span>
</ShowIf>
</span>
)

View file

@ -9,8 +9,8 @@ import { ForkAwesomeIcon } from '../fork-awesome/fork-awesome-icon'
export const WaitSpinner: React.FC = () => {
return (
<div className={ 'm-3 d-flex align-items-center justify-content-center' }>
<ForkAwesomeIcon icon={ 'spinner' } className={ 'fa-spin' }/>
<div className={'m-3 d-flex align-items-center justify-content-center'}>
<ForkAwesomeIcon icon={'spinner'} className={'fa-spin'} />
</div>
)
}

View file

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

View file

@ -12,9 +12,9 @@ import { SimpleAlertProps } from '../common/simple-alert/simple-alert-props'
export const LoadingNoteAlert: React.FC<SimpleAlertProps> = ({ show }) => {
return (
<ShowIf condition={ show }>
<Alert variant={ 'info' } className={ 'my-2' }>
<Trans i18nKey={ 'views.readOnly.loading' }/>
<ShowIf condition={show}>
<Alert variant={'info'} className={'my-2'}>
<Trans i18nKey={'views.readOnly.loading'} />
</Alert>
</ShowIf>
)

View file

@ -39,32 +39,38 @@ export const DocumentInfobar: React.FC<DocumentInfobarProps> = ({
const assetsBaseUrl = useCustomizeAssetsUrl()
return (
<div className={ 'd-flex flex-row my-3 document-infobar' }>
<div className={ 'col-md' }>&nbsp;</div>
<div className={ 'd-flex flex-fill' }>
<div className={ 'd-flex flex-column' }>
<div className={'d-flex flex-row my-3 document-infobar'}>
<div className={'col-md'}>&nbsp;</div>
<div className={'d-flex flex-fill'}>
<div className={'d-flex flex-column'}>
<DocumentInfoTimeLine
mode={ DocumentInfoLineWithTimeMode.CREATED }
time={ createdTime }
userName={ createdAuthor }
profileImageSrc={ `${ assetsBaseUrl }/img/avatar.png` }/>
mode={DocumentInfoLineWithTimeMode.CREATED}
time={createdTime}
userName={createdAuthor}
profileImageSrc={`${assetsBaseUrl}/img/avatar.png`}
/>
<DocumentInfoTimeLine
mode={ DocumentInfoLineWithTimeMode.EDITED }
time={ changedTime }
userName={ changedAuthor }
profileImageSrc={ `${ assetsBaseUrl }/img/avatar.png` }/>
<hr/>
mode={DocumentInfoLineWithTimeMode.EDITED}
time={changedTime}
userName={changedAuthor}
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' }
className={ 'text-primary text-decoration-none mx-1' }
title={ t('views.readOnly.editNote') }/>
<span className={'ml-auto'}>
{viewCount} <Trans i18nKey={'views.readOnly.viewCount'} />
<ShowIf condition={editable}>
<InternalLink
text={''}
href={`/n/${noteId}`}
icon={'pencil'}
className={'text-primary text-decoration-none mx-1'}
title={t('views.readOnly.editNote')}
/>
</ShowIf>
</span>
</div>
<div className={ 'col-md' }>&nbsp;</div>
<div className={'col-md'}>&nbsp;</div>
</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>()
@ -39,28 +38,30 @@ export const DocumentReadOnlyPage: React.FC = () => {
const noteDetails = useSelector((state: ApplicationState) => state.noteDetails)
return (
<div className={ 'd-flex flex-column mvh-100 bg-light' }>
<MotdBanner/>
<AppBar mode={ AppBarMode.BASIC }/>
<div className={ 'container' }>
<ErrorWhileLoadingNoteAlert show={ error }/>
<LoadingNoteAlert show={ loading }/>
<div className={'d-flex flex-column mvh-100 bg-light'}>
<MotdBanner />
<AppBar mode={AppBarMode.BASIC} />
<div className={'container'}>
<ErrorWhileLoadingNoteAlert show={error} />
<LoadingNoteAlert show={loading} />
</div>
<ShowIf condition={ !error && !loading }>
<ShowIf condition={!error && !loading}>
<DocumentInfobar
changedAuthor={ noteDetails.lastChange.userName ?? '' }
changedTime={ noteDetails.lastChange.timestamp }
createdAuthor={ 'Test' }
createdTime={ noteDetails.createTime }
editable={ true }
noteId={ id }
viewCount={ noteDetails.viewCount }
changedAuthor={noteDetails.lastChange.userName ?? ''}
changedTime={noteDetails.lastChange.timestamp}
createdAuthor={'Test'}
createdTime={noteDetails.createTime}
editable={true}
noteId={id}
viewCount={noteDetails.viewCount}
/>
<RenderIframe
frameClasses={'flex-fill h-100 w-100'}
markdownContent={markdownContent}
onFirstHeadingChange={onFirstHeadingChange}
onFrontmatterChange={onFrontmatterChange}
rendererType={RendererType.DOCUMENT}
/>
<RenderIframe frameClasses={ 'flex-fill h-100 w-100' }
markdownContent={ markdownContent }
onFirstHeadingChange={ onFirstHeadingChange }
onFrontmatterChange={ onFrontmatterChange }
rendererType={RendererType.DOCUMENT}/>
</ShowIf>
</div>
)

View file

@ -36,31 +36,31 @@ export const AppBar: React.FC<AppBarProps> = ({ mode }) => {
const noteFrontmatter = useSelector((state: ApplicationState) => state.noteDetails.frontmatter, equal)
return (
<Navbar bg={ 'light' }>
<Nav className="mr-auto d-flex align-items-center">
<NavbarBranding/>
<ShowIf condition={ mode === AppBarMode.EDITOR }>
<EditorViewMode/>
<SyncScrollButtons/>
<Navbar bg={'light'}>
<Nav className='mr-auto d-flex align-items-center'>
<NavbarBranding />
<ShowIf condition={mode === AppBarMode.EDITOR}>
<EditorViewMode />
<SyncScrollButtons />
</ShowIf>
<DarkModeButton/>
<ShowIf condition={ mode === AppBarMode.EDITOR }>
<DarkModeButton />
<ShowIf condition={mode === AppBarMode.EDITOR}>
<ShowIf condition={noteFrontmatter.type === NoteType.SLIDE}>
<SlideModeButton/>
<SlideModeButton />
</ShowIf>
<ShowIf condition={noteFrontmatter.type !== NoteType.SLIDE}>
<ReadOnlyModeButton/>
<ReadOnlyModeButton />
</ShowIf>
<HelpButton/>
<HelpButton />
</ShowIf>
</Nav>
<Nav className="d-flex align-items-center text-secondary">
<NewNoteButton/>
<ShowIf condition={ !userExists }>
<SignInButton size={ 'sm' }/>
<Nav className='d-flex align-items-center text-secondary'>
<NewNoteButton />
<ShowIf condition={!userExists}>
<SignInButton size={'sm'} />
</ShowIf>
<ShowIf condition={ userExists }>
<UserDropdown/>
<ShowIf condition={userExists}>
<UserDropdown />
</ShowIf>
</Nav>
</Navbar>

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"
title={ t('editor.darkMode.switchToDark') }
onChange={ () => setDarkMode(true) }
>
<ForkAwesomeIcon icon="moon"/>
value={DarkModeState.DARK}
variant='outline-secondary'
title={t('editor.darkMode.switchToDark')}
onChange={() => setDarkMode(true)}>
<ForkAwesomeIcon icon='moon' />
</ToggleButton>
<ToggleButton
value={ DarkModeState.LIGHT }
variant="outline-secondary"
title={ t('editor.darkMode.switchToLight') }
onChange={ () => setDarkMode(false) }
>
<ForkAwesomeIcon icon="sun-o"/>
value={DarkModeState.LIGHT}
variant='outline-secondary'
title={t('editor.darkMode.switchToLight')}
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"
value={ editorMode }
onChange={ (value: EditorMode) => {
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,31 +8,38 @@ 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) => {
onTaskCheckedChange(newValue)
}, [onTaskCheckedChange])
const checkboxClick = useCallback(
(lineInMarkdown: number, newValue: boolean) => {
onTaskCheckedChange(newValue)
},
[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' }/>
<td className={'markdown-body'}>
<HighlightedCode code={code} wrapLines={true} startLineNumber={1} language={'markdown'} />
</td>
</tr>
</Suspense>

View file

@ -13,40 +13,46 @@ import { CheatsheetLine } from './cheatsheet-line'
export const Cheatsheet: React.FC = () => {
const { t } = useTranslation()
const [checked, setChecked] = useState<boolean>(false)
const codes = useMemo(() => [
`**${ t('editor.editorToolbar.bold') }**`,
`*${ t('editor.editorToolbar.italic') }*`,
`++${ t('editor.editorToolbar.underline') }++`,
`~~${ t('editor.editorToolbar.strikethrough') }~~`,
'H~2~O',
'19^th^',
`==${ t('editor.help.cheatsheet.highlightedText') }==`,
`# ${ t('editor.editorToolbar.header') }`,
`\`${ t('editor.editorToolbar.code') }\``,
'```javascript=\nvar x = 5;\n```',
`> ${ t('editor.editorToolbar.blockquote') }`,
`- ${ t('editor.editorToolbar.unorderedList') }`,
`1. ${ t('editor.editorToolbar.orderedList') }`,
`- [${ checked ? 'x' : ' ' }] ${ t('editor.editorToolbar.checkList') }`,
`[${ t('editor.editorToolbar.link') }](https://example.com)`,
`![${ t('editor.editorToolbar.image') }](/icons/apple-touch-icon.png)`,
':smile:',
`:::info\n${ t('editor.help.cheatsheet.exampleAlert') }\n:::`
], [checked, t])
const codes = useMemo(
() => [
`**${t('editor.editorToolbar.bold')}**`,
`*${t('editor.editorToolbar.italic')}*`,
`++${t('editor.editorToolbar.underline')}++`,
`~~${t('editor.editorToolbar.strikethrough')}~~`,
'H~2~O',
'19^th^',
`==${t('editor.help.cheatsheet.highlightedText')}==`,
`# ${t('editor.editorToolbar.header')}`,
`\`${t('editor.editorToolbar.code')}\``,
'```javascript=\nvar x = 5;\n```',
`> ${t('editor.editorToolbar.blockquote')}`,
`- ${t('editor.editorToolbar.unorderedList')}`,
`1. ${t('editor.editorToolbar.orderedList')}`,
`- [${checked ? 'x' : ' '}] ${t('editor.editorToolbar.checkList')}`,
`[${t('editor.editorToolbar.link')}](https://example.com)`,
`![${t('editor.editorToolbar.image')}](/icons/apple-touch-icon.png)`,
':smile:',
`:::info\n${t('editor.help.cheatsheet.exampleAlert')}\n:::`
],
[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>
</tr>
<tr>
<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,11 +17,15 @@ export const HelpButton: React.FC = () => {
return (
<Fragment>
<Button title={ t('editor.documentBar.help') } className='ml-2 text-secondary' size='sm' variant='outline-light'
onClick={ () => setShow(true) }>
<ForkAwesomeIcon icon="question-circle"/>
<Button
title={t('editor.documentBar.help')}
className='ml-2 text-secondary'
size='sm'
variant='outline-light'
onClick={() => setShow(true)}>
<ForkAwesomeIcon icon='question-circle' />
</Button>
<HelpModal show={ show } onHide={ onHide }/>
<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,40 +30,41 @@ 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])
const tabTitle = useMemo(() => t('editor.documentBar.help') + ' - ' + t(`editor.help.${ tab }`), [t, tab])
const tabTitle = useMemo(() => t('editor.documentBar.help') + ' - ' + t(`editor.help.${tab}`), [t, tab])
return (
<CommonModal icon={ 'question-circle' } show={ show } onHide={ onHide } title={ tabTitle }>
<CommonModal icon={'question-circle'} show={show} onHide={onHide} title={tabTitle}>
<Modal.Body>
<nav className='nav nav-tabs'>
<Button
variant={ 'light' }
className={ `nav-link nav-item ${ tab === HelpTabStatus.Cheatsheet ? 'active' : '' }` }
onClick={ () => setTab(HelpTabStatus.Cheatsheet) }>
<Trans i18nKey={ 'editor.help.cheatsheet.title' }/>
variant={'light'}
className={`nav-link nav-item ${tab === HelpTabStatus.Cheatsheet ? 'active' : ''}`}
onClick={() => setTab(HelpTabStatus.Cheatsheet)}>
<Trans i18nKey={'editor.help.cheatsheet.title'} />
</Button>
<Button
variant={ 'light' }
className={ `nav-link nav-item ${ tab === HelpTabStatus.Shortcuts ? 'active' : '' }` }
onClick={ () => setTab(HelpTabStatus.Shortcuts) }>
<Trans i18nKey={ 'editor.help.shortcuts.title' }/>
variant={'light'}
className={`nav-link nav-item ${tab === HelpTabStatus.Shortcuts ? 'active' : ''}`}
onClick={() => setTab(HelpTabStatus.Shortcuts)}>
<Trans i18nKey={'editor.help.shortcuts.title'} />
</Button>
<Button
variant={ 'light' }
className={ `nav-link nav-item ${ tab === HelpTabStatus.Links ? 'active' : '' }` }
onClick={ () => setTab(HelpTabStatus.Links) }>
<Trans i18nKey={ 'editor.help.links.title' }/>
variant={'light'}
className={`nav-link nav-item ${tab === HelpTabStatus.Links ? 'active' : ''}`}
onClick={() => setTab(HelpTabStatus.Links)}>
<Trans i18nKey={'editor.help.links.title'} />
</Button>
</nav>
{ tabContent }
{tabContent}
</Modal.Body>
</CommonModal>)
</CommonModal>
)
}

View file

@ -15,15 +15,17 @@ export const Links: React.FC = () => {
useTranslation()
return (
<Row className={ 'justify-content-center pt-4' }>
<Col lg={ 4 }>
<h3><Trans i18nKey='editor.help.contacts.title'/></h3>
<Row className={'justify-content-center pt-4'}>
<Col lg={4}>
<h3>
<Trans i18nKey='editor.help.contacts.title' />
</h3>
<div>
<ul className="list-unstyled">
<ul className='list-unstyled'>
<li>
<TranslatedExternalLink
i18nKey='editor.help.contacts.community'
href={ links.community }
href={links.community}
icon='users'
className='text-primary'
/>
@ -31,8 +33,8 @@ export const Links: React.FC = () => {
<li>
<TranslatedExternalLink
i18nKey='editor.help.contacts.meetUsOn'
i18nOption={ { service: 'Matrix' } }
href={ links.chat }
i18nOption={{ service: 'Matrix' }}
href={links.chat}
icon='hashtag'
className='text-primary'
/>
@ -40,7 +42,7 @@ export const Links: React.FC = () => {
<li>
<TranslatedExternalLink
i18nKey='editor.help.contacts.reportIssue'
href={ links.backendIssues }
href={links.backendIssues}
icon='tag'
className='text-primary'
/>
@ -48,7 +50,7 @@ export const Links: React.FC = () => {
<li>
<TranslatedExternalLink
i18nKey='editor.help.contacts.helpTranslating'
href={ links.translate }
href={links.translate}
icon='language'
className='text-primary'
/>
@ -56,10 +58,12 @@ export const Links: React.FC = () => {
</ul>
</div>
</Col>
<Col lg={ 4 }>
<h3><Trans i18nKey='editor.help.documents.title'/></h3>
<Col lg={4}>
<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

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

View file

@ -20,11 +20,12 @@ 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 }/>
<Branding inline={ true }/>
logoType={darkModeActivated ? HedgeDocLogoType.WB_HORIZONTAL : HedgeDocLogoType.BW_HORIZONTAL}
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

@ -17,10 +17,13 @@ export const ReadOnlyModeButton: React.FC = () => {
const { id } = useParams<EditorPagePathParams>()
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"/>
<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>
</Link>
)

View file

@ -17,10 +17,13 @@ export const SlideModeButton: React.FC = () => {
const { id } = useParams<EditorPagePathParams>()
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"/>
<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>
</Link>
)

View file

@ -20,27 +20,31 @@ 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' }
value={ syncScrollEnabled }>
<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 }
>
<EnabledScrollIcon/>
variant={'outline-secondary'}
title={t('editor.appBar.syncScroll.enable')}
onChange={() => setEditorSyncScroll(true)}
value={SyncScrollState.SYNCED}>
<EnabledScrollIcon />
</ToggleButton>
<ToggleButton
variant={ 'outline-secondary' }
title={ t('editor.appBar.syncScroll.disable') }
onChange={ () => setEditorSyncScroll(false) }
value={ SyncScrollState.UNSYNCED }
>
<DisabledScrollIcon/>
variant={'outline-secondary'}
title={t('editor.appBar.syncScroll.disable')}
onChange={() => setEditorSyncScroll(false)}
value={SyncScrollState.UNSYNCED}>
<DisabledScrollIcon />
</ToggleButton>
</ToggleButtonGroup>
)

View file

@ -15,11 +15,9 @@ export interface DocumentInfoLineProps {
export const DocumentInfoLine: React.FC<DocumentInfoLineProps> = ({ icon, size, children }) => {
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>
<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>
</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,42 +24,38 @@ 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 }) }
userName={ 'Tilman' }
profileImageSrc={ `${ assetsBaseUrl }img/avatar.png` }/>
size={'2x'}
mode={DocumentInfoLineWithTimeMode.CREATED}
time={DateTime.local().minus({ days: 11 })}
userName={'Tilman'}
profileImageSrc={`${assetsBaseUrl}img/avatar.png`}
/>
</ListGroup.Item>
<ListGroup.Item>
<DocumentInfoTimeLine
size={ '2x' }
mode={ DocumentInfoLineWithTimeMode.EDITED }
time={ DateTime.local()
.minus({ minutes: 3 }) }
userName={ 'Philip' }
profileImageSrc={ `${ assetsBaseUrl }img/avatar.png` }/>
size={'2x'}
mode={DocumentInfoLineWithTimeMode.EDITED}
time={DateTime.local().minus({ minutes: 3 })}
userName={'Philip'}
profileImageSrc={`${assetsBaseUrl}img/avatar.png`}
/>
</ListGroup.Item>
<ListGroup.Item>
<DocumentInfoLine icon={ 'users' } size={ '2x' }>
<DocumentInfoLine icon={'users'} size={'2x'}>
<Trans i18nKey='editor.modal.documentInfo.usersContributed'>
<UnitalicBoldText text={ '42' }/>
<UnitalicBoldText text={'42'} />
</Trans>
</DocumentInfoLine>
</ListGroup.Item>
<ListGroup.Item>
<DocumentInfoLine icon={ 'history' } size={ '2x' }>
<DocumentInfoLine icon={'history'} size={'2x'}>
<Trans i18nKey='editor.modal.documentInfo.revisions'>
<UnitalicBoldText text={ '192' }/>
<UnitalicBoldText text={'192'} />
</Trans>
</DocumentInfoLine>
</ListGroup.Item>

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,18 +25,31 @@ 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 }/>
<TimeFromNow time={ time }/>
<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}
/>
<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,9 +7,9 @@
import React from 'react'
export interface UnitalicBoldTextProps {
text: string;
text: string
}
export const UnitalicBoldText: React.FC<UnitalicBoldTextProps> = ({ text }) => {
return <b className={ 'font-style-normal mr-1' }>{ text }</b>
return <b className={'font-style-normal mr-1'}>{text}</b>
}

View file

@ -18,44 +18,36 @@ export interface PermissionGroupEntryProps {
export enum GroupMode {
NONE,
VIEW,
EDIT,
EDIT
}
export const PermissionGroupEntry: React.FC<PermissionGroupEntryProps> = ({ title, editMode, onChangeEditMode }) => {
const { t } = useTranslation()
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 }
>
<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}>
<ToggleButton
title={ t('editor.modal.permissions.denyGroup', { name: t(title) }) }
variant={ 'light' }
className={ 'text-secondary' }
value={ GroupMode.NONE }
>
<ForkAwesomeIcon icon='ban'/>
title={t('editor.modal.permissions.denyGroup', { name: t(title) })}
variant={'light'}
className={'text-secondary'}
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 }
>
<ForkAwesomeIcon icon='eye'/>
title={t('editor.modal.permissions.viewOnlyGroup', { name: t(title) })}
variant={'light'}
className={'text-secondary'}
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 }
>
<ForkAwesomeIcon icon='pencil'/>
title={t('editor.modal.permissions.editGroup', { name: t(title) })}
variant={'light'}
className={'text-secondary'}
value={GroupMode.EDIT}>
<ForkAwesomeIcon icon='pencil' />
</ToggleButton>
</ToggleButtonGroup>
</li>

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('')
@ -37,64 +47,56 @@ export const PermissionList: React.FC<PermissionListProps> = ({ list, identifier
}
return (
<ul className={ 'list-group' }>
{ list.map(entry => (
<li key={ entry.id } className={ 'list-group-item d-flex flex-row justify-content-between align-items-center' }>
{ identifier(entry) }
<ul className={'list-group'}>
{list.map((entry) => (
<li key={entry.id} className={'list-group-item d-flex flex-row justify-content-between align-items-center'}>
{identifier(entry)}
<div>
<Button
variant='light'
className={ 'text-danger mr-2' }
title={ t(removeI18nKey, { name: entry.name }) }
onClick={ () => removeEntry(entry.id) }
>
<ForkAwesomeIcon icon={ 'times' }/>
className={'text-danger mr-2'}
title={t(removeI18nKey, { name: entry.name })}
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) }
>
value={entry.canEdit ? EditMode.EDIT : EditMode.VIEW}
onChange={(value: EditMode) => changeEditMode(entry.id, value === EditMode.EDIT)}>
<ToggleButton
title={ t(viewI18nKey, { name: entry.name }) }
variant={ 'light' }
className={ 'text-secondary' }
value={ EditMode.VIEW }
>
<ForkAwesomeIcon icon='eye'/>
title={t(viewI18nKey, { name: entry.name })}
variant={'light'}
className={'text-secondary'}
value={EditMode.VIEW}>
<ForkAwesomeIcon icon='eye' />
</ToggleButton>
<ToggleButton
title={ t(editI18nKey, { name: entry.name }) }
variant={ 'light' }
className={ 'text-secondary' }
value={ EditMode.EDIT }
>
<ForkAwesomeIcon icon='pencil'/>
title={t(editI18nKey, { name: entry.name })}
variant={'light'}
className={'text-secondary'}
value={EditMode.EDIT}>
<ForkAwesomeIcon icon='pencil' />
</ToggleButton>
</ToggleButtonGroup>
</div>
</li>
)) }
<li className={ 'list-group-item' }>
<form onSubmit={ event => {
event.preventDefault()
addEntry()
} }>
<InputGroup className={ 'mr-1 mb-1' }>
))}
<li className={'list-group-item'}>
<form
onSubmit={(event) => {
event.preventDefault()
addEntry()
}}>
<InputGroup className={'mr-1 mb-1'}>
<FormControl
value={ newEntry }
placeholder={ t(addI18nKey) }
aria-label={ t(addI18nKey) }
onChange={ event => setNewEntry(event.currentTarget.value) }
value={newEntry}
placeholder={t(addI18nKey)}
aria-label={t(addI18nKey)}
onChange={(event) => setNewEntry(event.currentTarget.value)}
/>
<Button
variant='light'
className={ 'text-secondary ml-2' }
title={ t(addI18nKey) }
onClick={ addEntry }
>
<ForkAwesomeIcon icon={ 'plus' }/>
<Button variant='light' className={'text-secondary ml-2'} title={t(addI18nKey)} onClick={addEntry}>
<ForkAwesomeIcon icon={'plus'} />
</Button>
</InputGroup>
</form>

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: [{
username: 'emcrx',
canEdit: true
}, {
username: 'mrdrogdrog',
canEdit: false
}],
sharedToGroup: [{
id: EVERYONE_GROUP_ID,
canEdit: true
}, {
id: EVERYONE_LOGGED_IN_GROUP_ID,
canEdit: false
}]
sharedTo: [
{
username: 'emcrx',
canEdit: true
},
{
username: 'mrdrogdrog',
canEdit: false
}
],
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([{
id: response.id,
name: response.name,
photo: response.photo,
canEdit: shareUser.canEdit
}]))
.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,70 +111,74 @@ export const PermissionModal: React.FC<PermissionsModalProps> = ({ show, onHide
}, [])
const changeUserMode = (userId: Principal['id'], canEdit: Principal['canEdit']) => {
setUserList(list =>
list
.map(user => {
if (user.id === userId) {
user.canEdit = canEdit
}
return 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({
id: name,
photo: '/img/avatar.png',
name: name,
canEdit: false
}))
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>
<ShowIf condition={ error }>
<h5 className={'mb-3'}>
<Trans i18nKey={'editor.modal.permissions.owner'} />
</h5>
<ShowIf condition={error}>
<Alert variant='danger'>
<Trans i18nKey='editor.modal.permissions.error'/>
<Trans i18nKey='editor.modal.permissions.error' />
</Alert>
</ShowIf>
<ul className={ 'list-group' }>
<li className={ 'list-group-item d-flex flex-row align-items-center' }>
<UserAvatar name={ owner?.name ?? '' } photo={ owner?.photo ?? '' }/>
<ul className={'list-group'}>
<li className={'list-group-item d-flex flex-row align-items-center'}>
<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 }/>) }
changeEditMode={ changeUserMode }
removeEntry={ removeUser }
createEntry={ addUser }
editI18nKey={ 'editor.modal.permissions.editUser' }
viewI18nKey={ 'editor.modal.permissions.viewOnlyUser' }
removeI18nKey={ 'editor.modal.permissions.removeUser' }
addI18nKey={ 'editor.modal.permissions.addUser' }
list={userList}
identifier={(entry) => <UserAvatar name={entry.name} photo={entry.photo} />}
changeEditMode={changeUserMode}
removeEntry={removeUser}
createEntry={addUser}
editI18nKey={'editor.modal.permissions.editUser'}
viewI18nKey={'editor.modal.permissions.viewOnlyUser'}
removeI18nKey={'editor.modal.permissions.removeUser'}
addI18nKey={'editor.modal.permissions.addUser'}
/>
<h5 className={ 'my-3' }><Trans i18nKey={ 'editor.modal.permissions.sharedWithGroups' }/></h5>
<ul className={ 'list-group' }>
<h5 className={'my-3'}>
<Trans i18nKey={'editor.modal.permissions.sharedWithGroups'} />
</h5>
<ul className={'list-group'}>
<PermissionGroupEntry
title={ 'editor.modal.permissions.allUser' }
editMode={ allUserPermissions }
onChangeEditMode={ setAllUserPermissions }
title={'editor.modal.permissions.allUser'}
editMode={allUserPermissions}
onChangeEditMode={setAllUserPermissions}
/>
<PermissionGroupEntry
title={ 'editor.modal.permissions.allLoggedInUser' }
editMode={ allLoggedInUserPermissions }
onChangeEditMode={ setAllLoggedInUserPermissions }
title={'editor.modal.permissions.allLoggedInUser'}
editMode={allLoggedInUserPermissions}
onChangeEditMode={setAllLoggedInUserPermissions}
/>
</ul>
</Modal.Body>

View file

@ -20,33 +20,32 @@ 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'
>
active={active}
onClick={onClick}
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') }
<ForkAwesomeIcon icon={'clock-o'} className='mx-2' />
{DateTime.fromMillis(revision.timestamp * 1000).toFormat('DDDD T')}
</span>
<span>
<ForkAwesomeIcon icon={ 'file-text-o' } className='mx-2'/>
<Trans i18nKey={ 'editor.modal.revision.length' }/>: { revision.length }
<ForkAwesomeIcon icon={'file-text-o'} className='mx-2' />
<Trans i18nKey={'editor.modal.revision.length'} />: {revision.length}
</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) => {
return (
<UserAvatar name={ user.name } photo={ user.photo } showName={ false }
additionalClasses={ 'mx-1' } key={ index }/>
)
})
}
<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) => {
return (
<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,60 +64,62 @@ 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' }>
<Col lg={4} className={'scroll-col'}>
<ListGroup as='ul'>
{
revisions.map((revision, revisionIndex) => (
<RevisionModalListEntry
key={ revisionIndex }
active={ selectedRevisionTimestamp === revision.timestamp }
revision={ revision }
revisionAuthorListMap={ revisionAuthorListMap.current }
onClick={ () => setSelectedRevisionTimestamp(revision.timestamp) }
/>
))
}
{revisions.map((revision, revisionIndex) => (
<RevisionModalListEntry
key={revisionIndex}
active={selectedRevisionTimestamp === revision.timestamp}
revision={revision}
revisionAuthorListMap={revisionAuthorListMap.current}
onClick={() => setSelectedRevisionTimestamp(revision.timestamp)}
/>
))}
</ListGroup>
</Col>
<Col lg={ 8 } className={ 'scroll-col' }>
<ShowIf condition={ error }>
<Col lg={8} className={'scroll-col'}>
<ShowIf condition={error}>
<Alert variant='danger'>
<Trans i18nKey='editor.modal.revision.error'/>
<Trans i18nKey='editor.modal.revision.error' />
</Alert>
</ShowIf>
<ShowIf condition={ !error && !!selectedRevision }>
<ShowIf condition={!error && !!selectedRevision}>
<ReactDiffViewer
oldValue={ selectedRevision?.content }
newValue={ markdownContent }
splitView={ false }
compareMethod={ DiffMethod.WORDS }
useDarkTheme={ darkModeEnabled }
oldValue={selectedRevision?.content}
newValue={markdownContent}
splitView={false}
compareMethod={DiffMethod.WORDS}
useDarkTheme={darkModeEnabled}
/>
</ShowIf>
</Col>
</Row>
</Modal.Body>
<Modal.Footer>
<Button
variant='secondary'
onClick={ onHide }>
<Trans i18nKey={ 'common.close' }/>
<Button variant='secondary' onClick={onHide}>
<Trans i18nKey={'common.close'} />
</Button>
<Button
variant='danger'
disabled={ !selectedRevisionTimestamp }
onClick={ () => window.alert('Not yet implemented. Requires websocket.') }>
<Trans i18nKey={ 'editor.modal.revision.revertButton' }/>
disabled={!selectedRevisionTimestamp}
onClick={() => window.alert('Not yet implemented. Requires websocket.')}>
<Trans i18nKey={'editor.modal.revision.revertButton'} />
</Button>
<Button
variant='primary'
disabled={ !selectedRevisionTimestamp }
onClick={ () => downloadRevision(id, selectedRevision) }>
<Trans i18nKey={ 'editor.modal.revision.download' }/>
disabled={!selectedRevisionTimestamp}
onClick={() => downloadRevision(id, selectedRevision)}>
<Trans i18nKey={'editor.modal.revision.download'} />
</Button>
</Modal.Footer>
</CommonModal>

View file

@ -13,7 +13,7 @@ export const downloadRevision = (noteId: string, revision: Revision | null): voi
if (!revision) {
return
}
download(revision.content, `${ noteId }-${ revision.timestamp }.md`, 'text/markdown')
download(revision.content, `${noteId}-${revision.timestamp}.md`, 'text/markdown')
}
export const getUserDataForRevision = (authors: string[]): UserResponse[] => {
@ -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 }` }/>
<ShowIf condition={ noteFrontmatter.type === NoteType.SLIDE }>
<Trans i18nKey={ 'editor.modal.shareLink.slidesDescription' }/>
<CopyableField content={ `${ baseUrl }p/${ id }` } nativeShareButton={ true }
url={ `${ baseUrl }p/${ id }` }/>
<Trans i18nKey={'editor.modal.shareLink.editorDescription'} />
<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}`} />
</ShowIf>
<ShowIf condition={ noteFrontmatter.type === '' }>
<Trans i18nKey={ 'editor.modal.shareLink.viewOnlyDescription' }/>
<CopyableField content={ `${ baseUrl }s/${ id }` } nativeShareButton={ true }
url={ `${ baseUrl }s/${ id }` }/>
<ShowIf condition={noteFrontmatter.type === ''}>
<Trans i18nKey={'editor.modal.shareLink.viewOnlyDescription'} />
<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) {
return
}
const documentRenderPaneTop = (documentRenderPaneRef.current.offsetTop ?? 0)
const rendererTop = (rendererRef.current.offsetTop ?? 0)
const offset = rendererTop - documentRenderPaneTop
onLineMarkerPositionChanged(linkMarkerPositions.map(oldMarker => ({
line: oldMarker.line,
position: oldMarker.position + offset
})))
}, [documentRenderPaneRef, onLineMarkerPositionChanged, rendererRef])
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 offset = rendererTop - documentRenderPaneTop
onLineMarkerPositionChanged(
linkMarkerPositions.map((oldMarker) => ({
line: oldMarker.line,
position: oldMarker.position + offset
}))
)
},
[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>
<Trans i18nKey={'editor.error.limitReached.description'} values={{ maxLength }} />
<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) => {
if (scrollSource.current === ScrollSource.RENDERER && editorSyncScroll) {
setScrollState((old) => ({ editorScrollState: newScrollState, rendererScrollState: old.rendererScrollState }))
}
}, [editorSyncScroll])
const onMarkdownRendererScroll = useCallback(
(newScrollState: ScrollState) => {
if (scrollSource.current === ScrollSource.RENDERER && editorSyncScroll) {
setScrollState((old) => ({ editorScrollState: newScrollState, rendererScrollState: old.rendererScrollState }))
}
},
[editorSyncScroll]
)
const onEditorScroll = useCallback((newScrollState: ScrollState) => {
if (scrollSource.current === ScrollSource.EDITOR && editorSyncScroll) {
setScrollState((old) => ({ rendererScrollState: newScrollState, editorScrollState: old.editorScrollState }))
}
}, [editorSyncScroll])
const onEditorScroll = useCallback(
(newScrollState: ScrollState) => {
if (scrollSource.current === ScrollSource.EDITOR && editorSyncScroll) {
setScrollState((old) => ({ rendererScrollState: newScrollState, editorScrollState: old.editorScrollState }))
}
},
[editorSyncScroll]
)
useViewModeShortcuts()
useApplyDarkMode()
@ -90,48 +96,56 @@ 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])
onContentChange={setNoteMarkdownContent}
content={markdownContent}
scrollState={scrollState.editorScrollState}
onScroll={onEditorScroll}
onMakeScrollSource={setEditorToScrollSource}
/>
),
[markdownContent, onEditorScroll, scrollState.editorScrollState, setEditorToScrollSource]
)
const rightPane = useMemo(() =>
const rightPane = useMemo(
() => (
<RenderIframe
frameClasses={ 'h-100 w-100' }
markdownContent={ markdownContent }
onMakeScrollSource={ setRendererToScrollSource }
onFirstHeadingChange={ updateNoteTitleByFirstHeading }
onTaskCheckedChange={ SetCheckboxInMarkdownContent }
onFrontmatterChange={ setNoteFrontmatter }
onScroll={ onMarkdownRendererScroll }
scrollState={ scrollState.rendererScrollState }
rendererType={ RendererType.DOCUMENT }/>
, [markdownContent, onMarkdownRendererScroll, scrollState.rendererScrollState,
setRendererToScrollSource])
frameClasses={'h-100 w-100'}
markdownContent={markdownContent}
onMakeScrollSource={setRendererToScrollSource}
onFirstHeadingChange={updateNoteTitleByFirstHeading}
onTaskCheckedChange={SetCheckboxInMarkdownContent}
onFrontmatterChange={setNoteFrontmatter}
onScroll={onMarkdownRendererScroll}
scrollState={scrollState.rendererScrollState}
rendererType={RendererType.DOCUMENT}
/>
),
[markdownContent, onMarkdownRendererScroll, scrollState.rendererScrollState, setRendererToScrollSource]
)
return (
<IframeCommunicatorContextProvider>
<UiNotifications/>
<MotdBanner/>
<div className={ 'd-flex flex-column vh-100' }>
<AppBar mode={ AppBarMode.EDITOR }/>
<div className={ 'container' }>
<ErrorWhileLoadingNoteAlert show={ error }/>
<LoadingNoteAlert show={ loading }/>
<UiNotifications />
<MotdBanner />
<div className={'d-flex flex-column vh-100'}>
<AppBar mode={AppBarMode.EDITOR} />
<div className={'container'}>
<ErrorWhileLoadingNoteAlert show={error} />
<LoadingNoteAlert show={loading} />
</div>
<ShowIf condition={ !error && !loading }>
<div className={ 'flex-fill d-flex h-100 w-100 overflow-hidden flex-row' }>
<ShowIf condition={!error && !loading}>
<div className={'flex-fill d-flex h-100 w-100 overflow-hidden flex-row'}>
<Splitter
showLeft={ editorMode === EditorMode.EDITOR || editorMode === EditorMode.BOTH }
left={ leftPane }
showRight={ editorMode === EditorMode.PREVIEW || editorMode === EditorMode.BOTH }
right={ rightPane }
containerClassName={ 'overflow-hidden' }/>
<Sidebar/>
showLeft={editorMode === EditorMode.EDITOR || editorMode === EditorMode.BOTH}
left={leftPane}
showRight={editorMode === EditorMode.PREVIEW || editorMode === EditorMode.BOTH}
right={rightPane}
containerClassName={'overflow-hidden'}
/>
<Sidebar />
</div>
</ShowIf>
</div>

View file

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

View file

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

View file

@ -40,7 +40,7 @@ const convertEmojiEventToHint = (emojiData: EmojiClickEventDetail): Hint | undef
text: shortCode,
render: (parent: HTMLLIElement) => {
const wrapper = document.createElement('div')
wrapper.innerHTML = `${ getEmojiIcon(emojiData) } ${ shortCode }`
wrapper.innerHTML = `${getEmojiIcon(emojiData)} ${shortCode}`
parent.appendChild(wrapper)
}
}
@ -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 => ({
text: allSupportedHeadersTextToInsert[allSupportedHeaders.indexOf(suggestion)],
displayText: suggestion
})),
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 => ({
text: suggestion
})),
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> => {
@ -46,13 +45,12 @@ const linkAndExtraTagHint = (editor: Editor): Promise<Hints | null> => {
case 'name':
// Get the user when a completion happens, this prevents to early calls resulting in 'Anonymous'
return {
text: `[name=${ userName }]`
text: `[name=${userName}]`
}
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 => ({
text: suggestion
})),
list: suggestions.map(
(suggestion: string): Hint => ({
text: suggestion
})
),
from: Pos(cursor.line, searchTerm.start),
to: Pos(cursor.line, searchTerm.end + 1)
})

View file

@ -64,16 +64,22 @@ const onChange = (editor: Editor) => {
}
interface DropEvent {
pageX: number,
pageY: number,
pageX: number
pageY: number
dataTransfer: {
files: FileList
effectAllowed: string
} | null
files: FileList
effectAllowed: string
} | null
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,18 +94,21 @@ 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) => {
if (!event || !event.clipboardData) {
return
}
if (smartPasteEnabled) {
const tableInserted = handleTablePaste(event, pasteEditor)
if (tableInserted) {
const onPaste = useCallback(
(pasteEditor: Editor, event: PasteEvent) => {
if (!event || !event.clipboardData) {
return
}
}
handleFilePaste(event, pasteEditor)
}, [smartPasteEnabled])
if (smartPasteEnabled) {
const tableInserted = handleTablePaste(event, pasteEditor)
if (tableInserted) {
return
}
}
handleFilePaste(event, pasteEditor)
},
[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,28 +142,44 @@ export const EditorPane: React.FC<EditorPaneProps & ScrollProps> = ({ onContentC
}
}, [editor, scrollState])
const onBeforeChange = useCallback((editor: Editor, data: EditorChange, value: string) => {
if (value.length > maxLength && !maxLengthWarningAlreadyShown.current) {
setShowMaxLengthWarning(true)
maxLengthWarningAlreadyShown.current = true
}
if (value.length <= maxLength) {
maxLengthWarningAlreadyShown.current = false
}
onContentChange(value)
}, [onContentChange, maxLength, maxLengthWarningAlreadyShown])
const onEditorDidMount = useCallback(mountedEditor => {
setStatusBarInfo(createStatusInfo(mountedEditor, maxLength))
setEditor(mountedEditor)
}, [maxLength])
const onBeforeChange = useCallback(
(editor: Editor, data: EditorChange, value: string) => {
if (value.length > maxLength && !maxLengthWarningAlreadyShown.current) {
setShowMaxLengthWarning(true)
maxLengthWarningAlreadyShown.current = true
}
if (value.length <= maxLength) {
maxLengthWarningAlreadyShown.current = false
}
onContentChange(value)
},
[onContentChange, maxLength, maxLengthWarningAlreadyShown]
)
const onEditorDidMount = useCallback(
(mountedEditor) => {
setStatusBarInfo(createStatusInfo(mountedEditor, maxLength))
setEditor(mountedEditor)
},
[maxLength]
)
const onCursorActivity = useCallback((editorWithActivity) => {
setStatusBarInfo(createStatusInfo(editorWithActivity, maxLength))
}, [maxLength])
const onCursorActivity = useCallback(
(editorWithActivity) => {
setStatusBarInfo(createStatusInfo(editorWithActivity, 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,53 +192,52 @@ export const EditorPane: React.FC<EditorPaneProps & ScrollProps> = ({ onContentC
const onMaxLengthHide = useCallback(() => setShowMaxLengthWarning(false), [])
const codeMirrorOptions: EditorConfiguration = useMemo<EditorConfiguration>(() => ({
...editorPreferences,
mode: 'gfm',
viewportMargin: 20,
styleActiveLine: true,
lineNumbers: true,
lineWrapping: true,
showCursorWhenSelecting: true,
highlightSelectionMatches: true,
inputStyle: 'textarea',
matchBrackets: true,
autoCloseBrackets: true,
matchTags: {
bothTags: true
},
autoCloseTags: true,
foldGutter: true,
gutters: [
'CodeMirror-linenumbers',
'authorship-gutters',
'CodeMirror-foldgutter'
],
extraKeys: defaultKeyMap,
flattenSpans: true,
addModeClass: true,
autoRefresh: true,
// otherCursors: true,
placeholder: t('editor.placeholder')
}), [t, editorPreferences])
const codeMirrorOptions: EditorConfiguration = useMemo<EditorConfiguration>(
() => ({
...editorPreferences,
mode: 'gfm',
viewportMargin: 20,
styleActiveLine: true,
lineNumbers: true,
lineWrapping: true,
showCursorWhenSelecting: true,
highlightSelectionMatches: true,
inputStyle: 'textarea',
matchBrackets: true,
autoCloseBrackets: true,
matchTags: {
bothTags: true
},
autoCloseTags: true,
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'authorship-gutters', 'CodeMirror-foldgutter'],
extraKeys: defaultKeyMap,
flattenSpans: true,
addModeClass: true,
autoRefresh: true,
// otherCursors: true,
placeholder: t('editor.placeholder')
}),
[t, editorPreferences]
)
return (
<div className={ 'd-flex flex-column h-100 position-relative' } onMouseEnter={ onMakeScrollSource }>
<MaxLengthWarningModal show={ showMaxLengthWarning } onHide={ onMaxLengthHide } maxLength={ maxLength }/>
<ToolBar editor={ editor }/>
<div className={'d-flex flex-column h-100 position-relative'} onMouseEnter={onMakeScrollSource}>
<MaxLengthWarningModal show={showMaxLengthWarning} onHide={onMaxLengthHide} maxLength={maxLength} />
<ToolBar editor={editor} />
<ControlledCodeMirror
className={ `overflow-hidden w-100 flex-fill ${ ligaturesEnabled ? '' : 'no-ligatures' }` }
value={ content }
options={ codeMirrorOptions }
onChange={ onChange }
onPaste={ onPaste }
onDrop={ onDrop }
onCursorActivity={ onCursorActivity }
editorDidMount={ onEditorDidMount }
onBeforeChange={ onBeforeChange }
onScroll={ onEditorScroll }
className={`overflow-hidden w-100 flex-fill ${ligaturesEnabled ? '' : 'no-ligatures'}`}
value={content}
options={codeMirrorOptions}
onChange={onChange}
onPaste={onPaste}
onDrop={onDrop}
onCursorActivity={onCursorActivity}
editorDidMount={onEditorDidMount}
onBeforeChange={onBeforeChange}
onScroll={onEditorScroll}
/>
<StatusBar { ...statusBarInfo } />
<StatusBar {...statusBarInfo} />
</div>
)
}

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')
@ -72,35 +69,35 @@ const tab = (editor: Editor) => {
export const defaultKeyMap: KeyMap = !isMac
? {
F9: suppressKey,
F10: f10,
Esc: esc,
'Ctrl-S': suppressKey,
Enter: 'newlineAndIndentContinueMarkdownList',
Tab: tab,
Home: 'goLineLeftSmart',
End: 'goLineRight',
'Ctrl-I': makeSelectionItalic,
'Ctrl-B': makeSelectionBold,
'Ctrl-U': underlineSelection,
'Ctrl-D': strikeThroughSelection,
'Ctrl-M': markSelection,
'Ctrl-K': addLink
}
F9: suppressKey,
F10: f10,
Esc: esc,
'Ctrl-S': suppressKey,
Enter: 'newlineAndIndentContinueMarkdownList',
Tab: tab,
Home: 'goLineLeftSmart',
End: 'goLineRight',
'Ctrl-I': makeSelectionItalic,
'Ctrl-B': makeSelectionBold,
'Ctrl-U': underlineSelection,
'Ctrl-D': strikeThroughSelection,
'Ctrl-M': markSelection,
'Ctrl-K': addLink
}
: {
F9: suppressKey,
F10: f10,
Esc: esc,
'Cmd-S': suppressKey,
Enter: 'newlineAndIndentContinueMarkdownList',
Tab: tab,
'Cmd-Left': 'goLineLeftSmart',
'Cmd-Right': 'goLineRight',
Home: 'goLineLeftSmart',
End: 'goLineRight',
'Cmd-I': makeSelectionItalic,
'Cmd-B': makeSelectionBold,
'Cmd-U': underlineSelection,
'Cmd-D': strikeThroughSelection,
'Cmd-M': markSelection
}
F9: suppressKey,
F10: f10,
Esc: esc,
'Cmd-S': suppressKey,
Enter: 'newlineAndIndentContinueMarkdownList',
Tab: tab,
'Cmd-Left': 'goLineLeftSmart',
'Cmd-Right': 'goLineRight',
Home: 'goLineLeftSmart',
End: 'goLineRight',
'Cmd-I': makeSelectionItalic,
'Cmd-B': makeSelectionBold,
'Cmd-U': underlineSelection,
'Cmd-D': strikeThroughSelection,
'Cmd-M': markSelection
}

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,27 +58,26 @@ 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 }>
<ShowIf condition={ selectedLines === 1 }>
<span>&nbsp;&nbsp;{ t('editor.statusBar.selection.column', { count: selectedColumns }) }</span>
<span>{t('editor.statusBar.cursor', { line: position.line + 1, columns: position.ch + 1 })}</span>
<ShowIf condition={selectedColumns !== 0 && selectedLines !== 0}>
<ShowIf condition={selectedLines === 1}>
<span>&nbsp;&nbsp;{t('editor.statusBar.selection.column', { count: selectedColumns })}</span>
</ShowIf>
<ShowIf condition={ selectedLines > 1 }>
<span>&nbsp;&nbsp;{ t('editor.statusBar.selection.line', { count: selectedLines }) }</span>
<ShowIf condition={selectedLines > 1}>
<span>&nbsp;&nbsp;{t('editor.statusBar.selection.line', { count: selectedLines })}</span>
</ShowIf>
</ShowIf>
</div>
<div className="ml-auto">
<span>{ t('editor.statusBar.lines', { lines: linesInDocument }) }</span>
<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' : '' }
>
{ t('editor.statusBar.length', { length: charactersInDocument }) }
data-cy={'remainingCharacters'}
title={getLengthTooltip}
className={remainingCharacters <= 0 ? 'text-danger' : remainingCharacters <= 100 ? 'text-warning' : ''}>
{t('editor.statusBar.length', { length: charactersInDocument })}
</span>
</div>
</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,43 +16,35 @@ 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 }`
return `${headRow1}\n${headRow2}\n${body}`
}

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