mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-21 17:26:29 -05:00
Add prettier for codestyle and re-format everything (#1294)
This commit is contained in:
parent
8b78154075
commit
0aae1f70d2
319 changed files with 4809 additions and 3936 deletions
28
.github/workflows/lint.yml
vendored
28
.github/workflows/lint.yml
vendored
|
@ -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
1
.gitignore
vendored
|
@ -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
6
.idea/prettier.xml
Normal 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
1
.prettierignore
Normal file
|
@ -0,0 +1 @@
|
|||
node_modules/
|
4
.prettierignore.license
Normal file
4
.prettierignore.license
Normal file
|
@ -0,0 +1,4 @@
|
|||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
|
19
package.json
19
package.json
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
60
src/api/config/types.d.ts
vendored
60
src/api/config/types.d.ts
vendored
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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> => {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
})
|
||||
|
|
|
@ -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[]>
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}`)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
60
src/components/common/cache/cache.test.ts
vendored
60
src/components/common/cache/cache.test.ts
vendored
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
5
src/components/common/cache/cache.ts
vendored
5
src/components/common/cache/cache.ts
vendored
|
@ -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(),
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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}`} />
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 }} />
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 }/>
|
||||
<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} />
|
||||
|
||||
</ShowIf>
|
||||
{ text }
|
||||
{text}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 }/>
|
||||
<Link to={href} className={className} id={id} title={title}>
|
||||
<ShowIf condition={!!icon}>
|
||||
<ForkAwesomeIcon icon={icon as IconName} fixedWidth={true} />
|
||||
|
||||
</ShowIf>
|
||||
{ text }
|
||||
{text}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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} />
|
||||
}
|
||||
|
|
|
@ -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} />
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 }/>
|
||||
<ShowIf condition={!!icon}>
|
||||
<ForkAwesomeIcon icon={icon as IconName} />
|
||||
|
||||
</ShowIf>
|
||||
{ titleI18nKey
|
||||
? <Trans i18nKey={ titleI18nKey }/>
|
||||
: <span>{ title }</span>
|
||||
}
|
||||
{titleI18nKey ? <Trans i18nKey={titleI18nKey} /> : <span>{title}</span>}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
{ children }
|
||||
{children}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -5,6 +5,5 @@
|
|||
*/
|
||||
|
||||
export const createNumberRangeArray = (length: number): number[] => {
|
||||
return Array.from(Array(length)
|
||||
.keys())
|
||||
return Array.from(Array(length).keys())
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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' }> </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'}> </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' }> </div>
|
||||
<div className={'col-md'}> </div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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]
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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> – { 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> – {t('editor.statusBar.selection.column', { count: selectedColumns })}</span>
|
||||
</ShowIf>
|
||||
<ShowIf condition={ selectedLines > 1 }>
|
||||
<span> – { t('editor.statusBar.selection.line', { count: selectedLines }) }</span>
|
||||
<ShowIf condition={selectedLines > 1}>
|
||||
<span> – {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>
|
||||
–
|
||||
<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>
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue