mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-13 22:54:42 -04: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
|
run: yarn install --frozen-lockfile --prefer-offline
|
||||||
- name: Lint code
|
- name: Lint code
|
||||||
run: yarn lint
|
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
|
||||||
!.idea/dictionaries/hedgedoc.xml
|
!.idea/dictionaries/hedgedoc.xml
|
||||||
!.idea/copyright
|
!.idea/copyright
|
||||||
|
!.idea/prettier.xml
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.env.local
|
.env.local
|
||||||
|
|
6
.idea/prettier.xml
generated
Normal file
6
.idea/prettier.xml
generated
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",
|
"analyze": "cross-env ANALYZE=true yarn build:mock",
|
||||||
"test": "craco test",
|
"test": "craco test",
|
||||||
"lint": "eslint --max-warnings=0 --ext .ts,.tsx src",
|
"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",
|
"eject": "react-scripts eject",
|
||||||
"cy:open": "cypress open",
|
"cy:open": "cypress open",
|
||||||
"cy:run:chrome": "cypress run --browser chrome",
|
"cy:run:chrome": "cypress run --browser chrome",
|
||||||
|
@ -145,9 +148,21 @@
|
||||||
"plugin:@typescript-eslint/recommended",
|
"plugin:@typescript-eslint/recommended",
|
||||||
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||||
"plugin:import/recommended",
|
"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": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
">0.2%",
|
">0.2%",
|
||||||
|
@ -169,9 +184,11 @@
|
||||||
"cypress": "7.4.0",
|
"cypress": "7.4.0",
|
||||||
"cypress-commands": "1.1.0",
|
"cypress-commands": "1.1.0",
|
||||||
"cypress-file-upload": "5.0.7",
|
"cypress-file-upload": "5.0.7",
|
||||||
|
"eslint-config-prettier": "8.3.0",
|
||||||
"eslint-plugin-chai-friendly": "0.7.1",
|
"eslint-plugin-chai-friendly": "0.7.1",
|
||||||
"eslint-plugin-cypress": "2.11.3",
|
"eslint-plugin-cypress": "2.11.3",
|
||||||
"http-server": "0.12.3",
|
"http-server": "0.12.3",
|
||||||
|
"prettier": "2.3.0",
|
||||||
"redux-devtools": "3.7.0",
|
"redux-devtools": "3.7.0",
|
||||||
"redux-devtools-extension": "2.13.9",
|
"redux-devtools-extension": "2.13.9",
|
||||||
"ts-loader": "9.2.2",
|
"ts-loader": "9.2.2",
|
||||||
|
|
|
@ -12,5 +12,5 @@ export const getConfig = async (): Promise<Config> => {
|
||||||
...defaultFetchConfig
|
...defaultFetchConfig
|
||||||
})
|
})
|
||||||
expectResponseCode(response)
|
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 {
|
export interface Config {
|
||||||
allowAnonymous: boolean,
|
allowAnonymous: boolean
|
||||||
allowRegister: boolean,
|
allowRegister: boolean
|
||||||
authProviders: AuthProvidersState,
|
authProviders: AuthProvidersState
|
||||||
branding: BrandingConfig,
|
branding: BrandingConfig
|
||||||
customAuthNames: CustomAuthNames,
|
customAuthNames: CustomAuthNames
|
||||||
useImageProxy: boolean,
|
useImageProxy: boolean
|
||||||
specialUrls: SpecialUrls,
|
specialUrls: SpecialUrls
|
||||||
version: BackendVersion,
|
version: BackendVersion
|
||||||
plantumlServer: string | null,
|
plantumlServer: string | null
|
||||||
maxDocumentLength: number,
|
maxDocumentLength: number
|
||||||
iframeCommunication: iframeCommunicationConfig
|
iframeCommunication: iframeCommunicationConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface iframeCommunicationConfig {
|
export interface iframeCommunicationConfig {
|
||||||
editorOrigin: string,
|
editorOrigin: string
|
||||||
rendererOrigin: string
|
rendererOrigin: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BrandingConfig {
|
export interface BrandingConfig {
|
||||||
name: string,
|
name: string
|
||||||
logo: string,
|
logo: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BackendVersion {
|
export interface BackendVersion {
|
||||||
|
@ -37,27 +37,27 @@ export interface BackendVersion {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthProvidersState {
|
export interface AuthProvidersState {
|
||||||
facebook: boolean,
|
facebook: boolean
|
||||||
github: boolean,
|
github: boolean
|
||||||
twitter: boolean,
|
twitter: boolean
|
||||||
gitlab: boolean,
|
gitlab: boolean
|
||||||
dropbox: boolean,
|
dropbox: boolean
|
||||||
ldap: boolean,
|
ldap: boolean
|
||||||
google: boolean,
|
google: boolean
|
||||||
saml: boolean,
|
saml: boolean
|
||||||
oauth2: boolean,
|
oauth2: boolean
|
||||||
internal: boolean,
|
internal: boolean
|
||||||
openid: boolean,
|
openid: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CustomAuthNames {
|
export interface CustomAuthNames {
|
||||||
ldap: string;
|
ldap: string
|
||||||
oauth2: string;
|
oauth2: string
|
||||||
saml: string;
|
saml: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SpecialUrls {
|
export interface SpecialUrls {
|
||||||
privacy?: string,
|
privacy?: string
|
||||||
termsOfUse?: string,
|
termsOfUse?: string
|
||||||
imprint?: string,
|
imprint?: string
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { HistoryEntryDto, HistoryEntryPutDto, HistoryEntryUpdateDto } from './ty
|
||||||
export const getHistory = async (): Promise<HistoryEntryDto[]> => {
|
export const getHistory = async (): Promise<HistoryEntryDto[]> => {
|
||||||
const response = await fetch(getApiUrl() + 'me/history')
|
const response = await fetch(getApiUrl() + 'me/history')
|
||||||
expectResponseCode(response)
|
expectResponseCode(response)
|
||||||
return await response.json() as Promise<HistoryEntryDto[]>
|
return (await response.json()) as Promise<HistoryEntryDto[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const postHistory = async (entries: HistoryEntryPutDto[]): Promise<void> => {
|
export const postHistory = async (entries: HistoryEntryPutDto[]): Promise<void> => {
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { defaultFetchConfig, expectResponseCode, getApiUrl } from '../utils'
|
||||||
import { isMockMode } from '../../utils/test-modes'
|
import { isMockMode } from '../../utils/test-modes'
|
||||||
|
|
||||||
export const getMe = async (): Promise<UserResponse> => {
|
export const getMe = async (): Promise<UserResponse> => {
|
||||||
const response = await fetch(getApiUrl() + `me${ isMockMode() ? '-get' : '' }`, {
|
const response = await fetch(getApiUrl() + `me${isMockMode() ? '-get' : ''}`, {
|
||||||
...defaultFetchConfig
|
...defaultFetchConfig
|
||||||
})
|
})
|
||||||
expectResponseCode(response)
|
expectResponseCode(response)
|
||||||
|
|
|
@ -16,7 +16,7 @@ export const getProxiedUrl = async (imageUrl: string): Promise<ImageProxyRespons
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
expectResponseCode(response)
|
expectResponseCode(response)
|
||||||
return await response.json() as Promise<ImageProxyResponse>
|
return (await response.json()) as Promise<ImageProxyResponse>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UploadedMedia {
|
export interface UploadedMedia {
|
||||||
|
@ -34,5 +34,5 @@ export const uploadFile = async (noteId: string, contentType: string, media: Blo
|
||||||
body: media
|
body: media
|
||||||
})
|
})
|
||||||
expectResponseCode(response, 201)
|
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> => {
|
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.
|
// 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.
|
// 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
|
...defaultFetchConfig
|
||||||
})
|
})
|
||||||
expectResponseCode(response)
|
expectResponseCode(response)
|
||||||
return await response.json() as Promise<NoteDto>
|
return (await response.json()) as Promise<NoteDto>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deleteNote = async (noteId: string): Promise<void> => {
|
export const deleteNote = async (noteId: string): Promise<void> => {
|
||||||
const response = await fetch(getApiUrl() + `notes/${ noteId }`, {
|
const response = await fetch(getApiUrl() + `notes/${noteId}`, {
|
||||||
...defaultFetchConfig,
|
...defaultFetchConfig,
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
})
|
})
|
||||||
|
|
|
@ -11,24 +11,24 @@ import { Revision, RevisionListEntry } from './types'
|
||||||
const revisionCache = new Cache<string, Revision>(3600)
|
const revisionCache = new Cache<string, Revision>(3600)
|
||||||
|
|
||||||
export const getRevision = async (noteId: string, timestamp: number): Promise<Revision> => {
|
export const getRevision = async (noteId: string, timestamp: number): Promise<Revision> => {
|
||||||
const cacheKey = `${ noteId }:${ timestamp }`
|
const cacheKey = `${noteId}:${timestamp}`
|
||||||
if (revisionCache.has(cacheKey)) {
|
if (revisionCache.has(cacheKey)) {
|
||||||
return revisionCache.get(cacheKey)
|
return revisionCache.get(cacheKey)
|
||||||
}
|
}
|
||||||
const response = await fetch(getApiUrl() + `notes/${ noteId }/revisions/${ timestamp }`, {
|
const response = await fetch(getApiUrl() + `notes/${noteId}/revisions/${timestamp}`, {
|
||||||
...defaultFetchConfig
|
...defaultFetchConfig
|
||||||
})
|
})
|
||||||
expectResponseCode(response)
|
expectResponseCode(response)
|
||||||
const revisionData = await response.json() as Revision
|
const revisionData = (await response.json()) as Revision
|
||||||
revisionCache.put(cacheKey, revisionData)
|
revisionCache.put(cacheKey, revisionData)
|
||||||
return revisionData
|
return revisionData
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getAllRevisions = async (noteId: string): Promise<RevisionListEntry[]> => {
|
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!
|
// 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
|
...defaultFetchConfig
|
||||||
})
|
})
|
||||||
expectResponseCode(response)
|
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'
|
import { AccessToken, AccessTokenSecret } from './types'
|
||||||
|
|
||||||
export const getAccessTokenList = async (): Promise<AccessToken[]> => {
|
export const getAccessTokenList = async (): Promise<AccessToken[]> => {
|
||||||
const response = await fetch(`${ getApiUrl() }tokens`, {
|
const response = await fetch(`${getApiUrl()}tokens`, {
|
||||||
...defaultFetchConfig
|
...defaultFetchConfig
|
||||||
})
|
})
|
||||||
expectResponseCode(response)
|
expectResponseCode(response)
|
||||||
return await response.json() as AccessToken[]
|
return (await response.json()) as AccessToken[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const postNewAccessToken = async (label: string): Promise<AccessToken & AccessTokenSecret> => {
|
export const postNewAccessToken = async (label: string): Promise<AccessToken & AccessTokenSecret> => {
|
||||||
const response = await fetch(`${ getApiUrl() }tokens`, {
|
const response = await fetch(`${getApiUrl()}tokens`, {
|
||||||
...defaultFetchConfig,
|
...defaultFetchConfig,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: label
|
body: label
|
||||||
})
|
})
|
||||||
expectResponseCode(response)
|
expectResponseCode(response)
|
||||||
return await response.json() as (AccessToken & AccessTokenSecret)
|
return (await response.json()) as AccessToken & AccessTokenSecret
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deleteAccessToken = async (timestamp: number): Promise<void> => {
|
export const deleteAccessToken = async (timestamp: number): Promise<void> => {
|
||||||
const response = await fetch(`${ getApiUrl() }tokens/${ timestamp }`, {
|
const response = await fetch(`${getApiUrl()}tokens/${timestamp}`, {
|
||||||
...defaultFetchConfig,
|
...defaultFetchConfig,
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
})
|
})
|
||||||
|
|
|
@ -14,7 +14,7 @@ export const getUserById = async (userid: string): Promise<UserResponse> => {
|
||||||
if (cache.has(userid)) {
|
if (cache.has(userid)) {
|
||||||
return cache.get(userid)
|
return cache.get(userid)
|
||||||
}
|
}
|
||||||
const response = await fetch(`${ getApiUrl() }/users/${ userid }`, {
|
const response = await fetch(`${getApiUrl()}/users/${userid}`, {
|
||||||
...defaultFetchConfig
|
...defaultFetchConfig
|
||||||
})
|
})
|
||||||
expectResponseCode(response)
|
expectResponseCode(response)
|
||||||
|
|
|
@ -24,6 +24,6 @@ export const getApiUrl = (): string => {
|
||||||
|
|
||||||
export const expectResponseCode = (response: Response, code = 200): void => {
|
export const expectResponseCode = (response: Response, code = 200): void => {
|
||||||
if (response.status !== code) {
|
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 backendBaseUrl = useBackendBaseUrl()
|
||||||
const customizeAssetsUrl = useCustomizeAssetsUrl()
|
const customizeAssetsUrl = useCustomizeAssetsUrl()
|
||||||
|
|
||||||
const setUpTasks = useCallback(() => createSetUpTaskList(frontendAssetsUrl, customizeAssetsUrl, backendBaseUrl),
|
const setUpTasks = useCallback(
|
||||||
[backendBaseUrl, customizeAssetsUrl, frontendAssetsUrl])
|
() => createSetUpTaskList(frontendAssetsUrl, customizeAssetsUrl, backendBaseUrl),
|
||||||
|
[backendBaseUrl, customizeAssetsUrl, frontendAssetsUrl]
|
||||||
|
)
|
||||||
|
|
||||||
const [failedTitle, setFailedTitle] = useState<string>('')
|
const [failedTitle, setFailedTitle] = useState<string>('')
|
||||||
const [doneTasks, setDoneTasks] = useState<number>(0)
|
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> => {
|
const runTask = useCallback(async (task: Promise<void>): Promise<void> => {
|
||||||
await task
|
await task
|
||||||
setDoneTasks(prevDoneTasks => {
|
setDoneTasks((prevDoneTasks) => {
|
||||||
return prevDoneTasks + 1
|
return prevDoneTasks + 1
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
for (const task of initTasks) {
|
for (const task of initTasks) {
|
||||||
runTask(task.task)
|
runTask(task.task).catch((reason: Error) => {
|
||||||
.catch((reason: Error) => {
|
console.error(reason)
|
||||||
console.error(reason)
|
setFailedTitle(task.name)
|
||||||
setFailedTitle(task.name)
|
})
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}, [initTasks, runTask])
|
}, [initTasks, runTask])
|
||||||
|
|
||||||
const tasksAreRunning = doneTasks < initTasks.length || initTasks.length === 0
|
const tasksAreRunning = doneTasks < initTasks.length || initTasks.length === 0
|
||||||
|
|
||||||
if (tasksAreRunning) {
|
if (tasksAreRunning) {
|
||||||
return <LoadingScreen failedTitle={ failedTitle }/>
|
return <LoadingScreen failedTitle={failedTitle} />
|
||||||
} else {
|
} else {
|
||||||
return <Suspense fallback={ (<LoadingScreen/>) }>
|
return <Suspense fallback={<LoadingScreen />}>{children}</Suspense>
|
||||||
{ children }
|
|
||||||
</Suspense>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ export const BANNER_LOCAL_STORAGE_KEY = 'banner.lastModified'
|
||||||
|
|
||||||
export const fetchAndSetBanner = async (customizeAssetsUrl: string): Promise<void> => {
|
export const fetchAndSetBanner = async (customizeAssetsUrl: string): Promise<void> => {
|
||||||
const cachedLastModified = window.localStorage.getItem(BANNER_LOCAL_STORAGE_KEY)
|
const cachedLastModified = window.localStorage.getItem(BANNER_LOCAL_STORAGE_KEY)
|
||||||
const bannerUrl = `${ customizeAssetsUrl }banner.txt`
|
const bannerUrl = `${customizeAssetsUrl}banner.txt`
|
||||||
|
|
||||||
if (cachedLastModified) {
|
if (cachedLastModified) {
|
||||||
const response = await fetch(bannerUrl, {
|
const response = await fetch(bannerUrl, {
|
||||||
|
|
|
@ -19,7 +19,7 @@ export const setUpI18n = async (frontendAssetsUrl: string): Promise<void> => {
|
||||||
fallbackLng: 'en',
|
fallbackLng: 'en',
|
||||||
debug: process.env.NODE_ENV !== 'production',
|
debug: process.env.NODE_ENV !== 'production',
|
||||||
backend: {
|
backend: {
|
||||||
loadPath: `${ frontendAssetsUrl }locales/{{lng}}.json`
|
loadPath: `${frontendAssetsUrl}locales/{{lng}}.json`
|
||||||
},
|
},
|
||||||
|
|
||||||
interpolation: {
|
interpolation: {
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { fetchFrontendConfig } from './fetch-frontend-config'
|
||||||
|
|
||||||
const customDelay: () => Promise<void> = async () => {
|
const customDelay: () => Promise<void> = async () => {
|
||||||
if (window.localStorage.getItem('customDelay')) {
|
if (window.localStorage.getItem('customDelay')) {
|
||||||
return new Promise(resolve => setTimeout(resolve, 5000))
|
return new Promise((resolve) => setTimeout(resolve, 5000))
|
||||||
} else {
|
} else {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
|
@ -24,28 +24,39 @@ export interface InitTask {
|
||||||
task: Promise<void>
|
task: Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createSetUpTaskList = (frontendAssetsUrl: string, customizeAssetsUrl: string, backendBaseUrl: string): InitTask[] => {
|
export const createSetUpTaskList = (
|
||||||
|
frontendAssetsUrl: string,
|
||||||
|
customizeAssetsUrl: string,
|
||||||
|
backendBaseUrl: string
|
||||||
|
): InitTask[] => {
|
||||||
setApiUrl({
|
setApiUrl({
|
||||||
apiUrl: `${ backendBaseUrl }api/private/`
|
apiUrl: `${backendBaseUrl}api/private/`
|
||||||
})
|
})
|
||||||
|
|
||||||
return [{
|
return [
|
||||||
name: 'Load Translations',
|
{
|
||||||
task: setUpI18n(frontendAssetsUrl)
|
name: 'Load Translations',
|
||||||
}, {
|
task: setUpI18n(frontendAssetsUrl)
|
||||||
name: 'Load config',
|
},
|
||||||
task: fetchFrontendConfig()
|
{
|
||||||
}, {
|
name: 'Load config',
|
||||||
name: 'Fetch user information',
|
task: fetchFrontendConfig()
|
||||||
task: fetchAndSetUser()
|
},
|
||||||
}, {
|
{
|
||||||
name: 'Banner',
|
name: 'Fetch user information',
|
||||||
task: fetchAndSetBanner(customizeAssetsUrl)
|
task: fetchAndSetUser()
|
||||||
}, {
|
},
|
||||||
name: 'Load history state',
|
{
|
||||||
task: refreshHistoryState()
|
name: 'Banner',
|
||||||
}, {
|
task: fetchAndSetBanner(customizeAssetsUrl)
|
||||||
name: 'Add Delay',
|
},
|
||||||
task: customDelay()
|
{
|
||||||
}]
|
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 }) => {
|
export const LoadingScreen: React.FC<LoadingScreenProps> = ({ failedTitle }) => {
|
||||||
return (
|
return (
|
||||||
<div className="loader middle text-light overflow-hidden">
|
<div className='loader middle text-light overflow-hidden'>
|
||||||
<div className="mb-3 text-light">
|
<div className='mb-3 text-light'>
|
||||||
<span className={ `d-block ${ failedTitle ? 'animation-shake' : 'animation-jump' }` }>
|
<span className={`d-block ${failedTitle ? 'animation-shake' : 'animation-jump'}`}>
|
||||||
<HedgeDocLogo size={ HedgeDocLogoSize.BIG }/>
|
<HedgeDocLogo size={HedgeDocLogoSize.BIG} />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<ShowIf condition={ !!failedTitle }>
|
<ShowIf condition={!!failedTitle}>
|
||||||
<Alert variant={ 'danger' }>
|
<Alert variant={'danger'}>
|
||||||
The task '{ failedTitle }' failed.<br/>
|
The task '{failedTitle}' failed.
|
||||||
|
<br />
|
||||||
For further information look into the browser console.
|
For further information look into the browser console.
|
||||||
</Alert>
|
</Alert>
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
|
|
|
@ -21,20 +21,20 @@ export const Branding: React.FC<BrandingProps> = ({ inline = false, delimiter =
|
||||||
const showBranding = !!branding.name || !!branding.logo
|
const showBranding = !!branding.name || !!branding.logo
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ShowIf condition={ showBranding }>
|
<ShowIf condition={showBranding}>
|
||||||
<ShowIf condition={ delimiter }>
|
<ShowIf condition={delimiter}>
|
||||||
<strong className={ `mx-1 ${ inline ? 'inline-size' : 'regular-size' }` }>@</strong>
|
<strong className={`mx-1 ${inline ? 'inline-size' : 'regular-size'}`}>@</strong>
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
{
|
{branding.logo ? (
|
||||||
branding.logo
|
<img
|
||||||
? <img
|
src={branding.logo}
|
||||||
src={ branding.logo }
|
alt={branding.name}
|
||||||
alt={ branding.name }
|
title={branding.name}
|
||||||
title={ branding.name }
|
className={inline ? 'inline-size' : 'regular-size'}
|
||||||
className={ inline ? 'inline-size' : 'regular-size' }
|
/>
|
||||||
/>
|
) : (
|
||||||
: branding.name
|
branding.name
|
||||||
}
|
)}
|
||||||
</ShowIf>
|
</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', () => {
|
it('initialize with right lifetime, no entry limit', () => {
|
||||||
const lifetime = 1000
|
const lifetime = 1000
|
||||||
const lifetimedCache = new Cache<string, string>(lifetime)
|
const lifetimedCache = new Cache<string, string>(lifetime)
|
||||||
expect(lifetimedCache.entryLifetime)
|
expect(lifetimedCache.entryLifetime).toEqual(lifetime)
|
||||||
.toEqual(lifetime)
|
expect(lifetimedCache.maxEntries).toEqual(0)
|
||||||
expect(lifetimedCache.maxEntries)
|
|
||||||
.toEqual(0)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('initialize with right lifetime, given entry limit', () => {
|
it('initialize with right lifetime, given entry limit', () => {
|
||||||
const lifetime = 1000
|
const lifetime = 1000
|
||||||
const maxEntries = 10
|
const maxEntries = 10
|
||||||
const limitedCache = new Cache<string, string>(lifetime, maxEntries)
|
const limitedCache = new Cache<string, string>(lifetime, maxEntries)
|
||||||
expect(limitedCache.entryLifetime)
|
expect(limitedCache.entryLifetime).toEqual(lifetime)
|
||||||
.toEqual(lifetime)
|
expect(limitedCache.maxEntries).toEqual(maxEntries)
|
||||||
expect(limitedCache.maxEntries)
|
|
||||||
.toEqual(maxEntries)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('entry exists after inserting', () => {
|
it('entry exists after inserting', () => {
|
||||||
testCache.put('test', 123)
|
testCache.put('test', 123)
|
||||||
expect(testCache.has('test'))
|
expect(testCache.has('test')).toBe(true)
|
||||||
.toBe(true)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('entry does not exist prior inserting', () => {
|
it('entry does not exist prior inserting', () => {
|
||||||
expect(testCache.has('test'))
|
expect(testCache.has('test')).toBe(false)
|
||||||
.toBe(false)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('entry does expire', () => {
|
it('entry does expire', () => {
|
||||||
const shortLivingCache = new Cache<string, number>(2)
|
const shortLivingCache = new Cache<string, number>(2)
|
||||||
shortLivingCache.put('test', 123)
|
shortLivingCache.put('test', 123)
|
||||||
expect(shortLivingCache.has('test'))
|
expect(shortLivingCache.has('test')).toBe(true)
|
||||||
.toBe(true)
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
expect(shortLivingCache.has('test'))
|
expect(shortLivingCache.has('test')).toBe(false)
|
||||||
.toBe(false)
|
|
||||||
}, 2000)
|
}, 2000)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('entry value does not change', () => {
|
it('entry value does not change', () => {
|
||||||
const testValue = Date.now()
|
const testValue = Date.now()
|
||||||
testCache.put('test', testValue)
|
testCache.put('test', testValue)
|
||||||
expect(testCache.get('test'))
|
expect(testCache.get('test')).toEqual(testValue)
|
||||||
.toEqual(testValue)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('error is thrown on non-existent entry', () => {
|
it('error is thrown on non-existent entry', () => {
|
||||||
const accessNonExistentEntry = () => {
|
const accessNonExistentEntry = () => {
|
||||||
testCache.get('test')
|
testCache.get('test')
|
||||||
}
|
}
|
||||||
expect(accessNonExistentEntry)
|
expect(accessNonExistentEntry).toThrow(Error)
|
||||||
.toThrow(Error)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('newer item replaces older item', () => {
|
it('newer item replaces older item', () => {
|
||||||
testCache.put('test', 123)
|
testCache.put('test', 123)
|
||||||
testCache.put('test', 456)
|
testCache.put('test', 456)
|
||||||
expect(testCache.get('test'))
|
expect(testCache.get('test')).toEqual(456)
|
||||||
.toEqual(456)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('entry limit is respected', () => {
|
it('entry limit is respected', () => {
|
||||||
const limitedCache = new Cache<string, number>(1000, 2)
|
const limitedCache = new Cache<string, number>(1000, 2)
|
||||||
limitedCache.put('first', 1)
|
limitedCache.put('first', 1)
|
||||||
expect(limitedCache.has('first'))
|
expect(limitedCache.has('first')).toBe(true)
|
||||||
.toBe(true)
|
expect(limitedCache.has('second')).toBe(false)
|
||||||
expect(limitedCache.has('second'))
|
expect(limitedCache.has('third')).toBe(false)
|
||||||
.toBe(false)
|
|
||||||
expect(limitedCache.has('third'))
|
|
||||||
.toBe(false)
|
|
||||||
limitedCache.put('second', 2)
|
limitedCache.put('second', 2)
|
||||||
expect(limitedCache.has('first'))
|
expect(limitedCache.has('first')).toBe(true)
|
||||||
.toBe(true)
|
expect(limitedCache.has('second')).toBe(true)
|
||||||
expect(limitedCache.has('second'))
|
expect(limitedCache.has('third')).toBe(false)
|
||||||
.toBe(true)
|
|
||||||
expect(limitedCache.has('third'))
|
|
||||||
.toBe(false)
|
|
||||||
limitedCache.put('third', 3)
|
limitedCache.put('third', 3)
|
||||||
expect(limitedCache.has('first'))
|
expect(limitedCache.has('first')).toBe(false)
|
||||||
.toBe(false)
|
expect(limitedCache.has('second')).toBe(true)
|
||||||
expect(limitedCache.has('second'))
|
expect(limitedCache.has('third')).toBe(true)
|
||||||
.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
|
return false
|
||||||
}
|
}
|
||||||
const entry = this.store.get(key)
|
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 {
|
get(key: K): V {
|
||||||
|
@ -40,8 +40,7 @@ export class Cache<K, V> {
|
||||||
|
|
||||||
put(key: K, value: V): void {
|
put(key: K, value: V): void {
|
||||||
if (this.maxEntries > 0 && this.store.size === this.maxEntries) {
|
if (this.maxEntries > 0 && this.store.size === this.maxEntries) {
|
||||||
this.store.delete(this.store.keys()
|
this.store.delete(this.store.keys().next().value)
|
||||||
.next().value)
|
|
||||||
}
|
}
|
||||||
this.store.set(key, {
|
this.store.set(key, {
|
||||||
entryCreated: Date.now(),
|
entryCreated: Date.now(),
|
||||||
|
|
|
@ -22,20 +22,21 @@ export const CopyOverlay: React.FC<CopyOverlayProps> = ({ content, clickComponen
|
||||||
const [tooltipId] = useState<string>(() => uuid())
|
const [tooltipId] = useState<string>(() => uuid())
|
||||||
|
|
||||||
const copyToClipboard = useCallback((content: string) => {
|
const copyToClipboard = useCallback((content: string) => {
|
||||||
navigator.clipboard.writeText(content)
|
navigator.clipboard
|
||||||
.then(() => {
|
.writeText(content)
|
||||||
setError(false)
|
.then(() => {
|
||||||
})
|
setError(false)
|
||||||
.catch(() => {
|
})
|
||||||
setError(true)
|
.catch(() => {
|
||||||
console.error('couldn\'t copy')
|
setError(true)
|
||||||
})
|
console.error("couldn't copy")
|
||||||
.finally(() => {
|
})
|
||||||
setShowCopiedTooltip(true)
|
.finally(() => {
|
||||||
setTimeout(() => {
|
setShowCopiedTooltip(true)
|
||||||
setShowCopiedTooltip(false)
|
setTimeout(() => {
|
||||||
}, 2000)
|
setShowCopiedTooltip(false)
|
||||||
})
|
}, 2000)
|
||||||
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -51,17 +52,17 @@ export const CopyOverlay: React.FC<CopyOverlayProps> = ({ content, clickComponen
|
||||||
}, [clickComponent, copyToClipboard, content])
|
}, [clickComponent, copyToClipboard, content])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Overlay target={ clickComponent } show={ showCopiedTooltip } placement="top">
|
<Overlay target={clickComponent} show={showCopiedTooltip} placement='top'>
|
||||||
{ (props) => (
|
{(props) => (
|
||||||
<Tooltip id={ `copied_${ tooltipId }` } { ...props }>
|
<Tooltip id={`copied_${tooltipId}`} {...props}>
|
||||||
<ShowIf condition={ error }>
|
<ShowIf condition={error}>
|
||||||
<Trans i18nKey={ 'common.copyError' }/>
|
<Trans i18nKey={'common.copyError'} />
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
<ShowIf condition={ !error }>
|
<ShowIf condition={!error}>
|
||||||
<Trans i18nKey={ 'common.successfullyCopied' }/>
|
<Trans i18nKey={'common.successfullyCopied'} />
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) }
|
)}
|
||||||
</Overlay>
|
</Overlay>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,11 +29,15 @@ export const CopyToClipboardButton: React.FC<CopyToClipboardButtonProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Button ref={ button } size={ size } variant={ variant } title={ t('renderer.highlightCode.copyCode') }
|
<Button
|
||||||
data-cy={ props['data-cy'] }>
|
ref={button}
|
||||||
<ForkAwesomeIcon icon='files-o'/>
|
size={size}
|
||||||
|
variant={variant}
|
||||||
|
title={t('renderer.highlightCode.copyCode')}
|
||||||
|
data-cy={props['data-cy']}>
|
||||||
|
<ForkAwesomeIcon icon='files-o' />
|
||||||
</Button>
|
</Button>
|
||||||
<CopyOverlay content={ content } clickComponent={ button }/>
|
<CopyOverlay content={content} clickComponent={button} />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,35 +22,36 @@ export const CopyableField: React.FC<CopyableFieldProps> = ({ content, nativeSha
|
||||||
const copyButton = useRef<HTMLButtonElement>(null)
|
const copyButton = useRef<HTMLButtonElement>(null)
|
||||||
|
|
||||||
const doShareAction = useCallback(() => {
|
const doShareAction = useCallback(() => {
|
||||||
navigator.share({
|
navigator
|
||||||
text: content,
|
.share({
|
||||||
url: url
|
text: content,
|
||||||
})
|
url: url
|
||||||
.catch(err => {
|
})
|
||||||
console.error('Native sharing failed: ', err)
|
.catch((err) => {
|
||||||
})
|
console.error('Native sharing failed: ', err)
|
||||||
|
})
|
||||||
}, [content, url])
|
}, [content, url])
|
||||||
|
|
||||||
const sharingSupported = typeof navigator.share === 'function'
|
const sharingSupported = typeof navigator.share === 'function'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<InputGroup className="my-3">
|
<InputGroup className='my-3'>
|
||||||
<FormControl readOnly={ true } className={ 'text-center' } value={ content }/>
|
<FormControl readOnly={true} className={'text-center'} value={content} />
|
||||||
<InputGroup.Append>
|
<InputGroup.Append>
|
||||||
<Button variant="outline-secondary" ref={ copyButton } title={ 'Copy' }>
|
<Button variant='outline-secondary' ref={copyButton} title={'Copy'}>
|
||||||
<ForkAwesomeIcon icon='files-o'/>
|
<ForkAwesomeIcon icon='files-o' />
|
||||||
</Button>
|
</Button>
|
||||||
</InputGroup.Append>
|
</InputGroup.Append>
|
||||||
<ShowIf condition={ !!nativeShareButton && sharingSupported }>
|
<ShowIf condition={!!nativeShareButton && sharingSupported}>
|
||||||
<InputGroup.Append>
|
<InputGroup.Append>
|
||||||
<Button variant="outline-secondary" title={ 'Share' } onClick={ doShareAction }>
|
<Button variant='outline-secondary' title={'Share'} onClick={doShareAction}>
|
||||||
<ForkAwesomeIcon icon='share-alt'/>
|
<ForkAwesomeIcon icon='share-alt' />
|
||||||
</Button>
|
</Button>
|
||||||
</InputGroup.Append>
|
</InputGroup.Append>
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
<CopyOverlay content={ content } clickComponent={ copyButton }/>
|
<CopyOverlay content={content} clickComponent={copyButton} />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,12 +15,16 @@ export interface ForkAwesomeIconProps {
|
||||||
stacked?: boolean
|
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 fixedWithClass = fixedWidth ? 'fa-fw' : ''
|
||||||
const sizeClass = size ? `-${ size }` : (stacked ? '-1x' : '')
|
const sizeClass = size ? `-${size}` : stacked ? '-1x' : ''
|
||||||
const stackClass = stacked ? '-stack' : ''
|
const stackClass = stacked ? '-stack' : ''
|
||||||
const extraClasses = `${ className ?? '' } ${ sizeClass || stackClass ? `fa${ stackClass }${ sizeClass }` : '' }`
|
const extraClasses = `${className ?? ''} ${sizeClass || stackClass ? `fa${stackClass}${sizeClass}` : ''}`
|
||||||
return (
|
return <i className={`fa ${fixedWithClass} fa-${icon} ${extraClasses}`} />
|
||||||
<i className={ `fa ${ fixedWithClass } fa-${ icon } ${ extraClasses }` }/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,15 +15,13 @@ export interface ForkAwesomeStackProps {
|
||||||
|
|
||||||
export const ForkAwesomeStack: React.FC<ForkAwesomeStackProps> = ({ size, children }) => {
|
export const ForkAwesomeStack: React.FC<ForkAwesomeStackProps> = ({ size, children }) => {
|
||||||
return (
|
return (
|
||||||
<span className={ `fa-stack ${ size ? 'fa-' : '' }${ size ?? '' }` }>
|
<span className={`fa-stack ${size ? 'fa-' : ''}${size ?? ''}`}>
|
||||||
{
|
{React.Children.map(children, (child) => {
|
||||||
React.Children.map(children, (child) => {
|
if (!React.isValidElement<ForkAwesomeIconProps>(child)) {
|
||||||
if (!React.isValidElement<ForkAwesomeIconProps>(child)) {
|
return null
|
||||||
return null
|
}
|
||||||
}
|
return <ForkAwesomeIcon {...child.props} stacked={true} />
|
||||||
return <ForkAwesomeIcon { ...child.props } stacked={ true }/>
|
})}
|
||||||
})
|
|
||||||
}
|
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ export enum HedgeDocLogoSize {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HedgeDocLogoProps {
|
export interface HedgeDocLogoProps {
|
||||||
size?: HedgeDocLogoSize | number,
|
size?: HedgeDocLogoSize | number
|
||||||
logoType: HedgeDocLogoType
|
logoType: HedgeDocLogoType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,11 +29,11 @@ export enum HedgeDocLogoType {
|
||||||
export const HedgeDocLogoWithText: React.FC<HedgeDocLogoProps> = ({ size = HedgeDocLogoSize.MEDIUM, logoType }) => {
|
export const HedgeDocLogoWithText: React.FC<HedgeDocLogoProps> = ({ size = HedgeDocLogoSize.MEDIUM, logoType }) => {
|
||||||
switch (logoType) {
|
switch (logoType) {
|
||||||
case HedgeDocLogoType.COLOR_VERTICAL:
|
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:
|
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:
|
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:
|
default:
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,5 +18,5 @@ export interface HedgeDocLogoProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HedgeDocLogo: React.FC<HedgeDocLogoProps> = ({ size = HedgeDocLogoSize.MEDIUM }) => {
|
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
|
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 (
|
return (
|
||||||
<Button { ...props }
|
<Button
|
||||||
className={ `btn-icon p-0 d-inline-flex align-items-stretch ${ border ? 'with-border' : '' } ${ className ?? '' }` }>
|
{...props}
|
||||||
<span className="icon-part d-flex align-items-center">
|
className={`btn-icon p-0 d-inline-flex align-items-stretch ${border ? 'with-border' : ''} ${className ?? ''}`}>
|
||||||
<ForkAwesomeIcon icon={ icon } fixedWidth={ iconFixedWidth } className={ 'icon' }/>
|
<span className='icon-part d-flex align-items-center'>
|
||||||
|
<ForkAwesomeIcon icon={icon} fixedWidth={iconFixedWidth} className={'icon'} />
|
||||||
</span>
|
</span>
|
||||||
<ShowIf condition={ !!children }>
|
<ShowIf condition={!!children}>
|
||||||
<span className="text-part d-flex align-items-center">
|
<span className='text-part d-flex align-items-center'>{children}</span>
|
||||||
{ children }
|
|
||||||
</span>
|
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
|
|
|
@ -14,8 +14,8 @@ export interface TranslatedIconButtonProps extends IconButtonProps {
|
||||||
|
|
||||||
export const TranslatedIconButton: React.FC<TranslatedIconButtonProps> = ({ i18nKey, ...props }) => {
|
export const TranslatedIconButton: React.FC<TranslatedIconButtonProps> = ({ i18nKey, ...props }) => {
|
||||||
return (
|
return (
|
||||||
<IconButton { ...props }>
|
<IconButton {...props}>
|
||||||
<Trans i18nKey={ i18nKey }/>
|
<Trans i18nKey={i18nKey} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,20 +10,21 @@ import { IconName } from '../fork-awesome/types'
|
||||||
import { ShowIf } from '../show-if/show-if'
|
import { ShowIf } from '../show-if/show-if'
|
||||||
import { LinkWithTextProps } from './types'
|
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 (
|
return (
|
||||||
<a href={ href }
|
<a href={href} target='_blank' rel='noopener noreferrer' id={id} className={className} title={title} dir='auto'>
|
||||||
target="_blank"
|
<ShowIf condition={!!icon}>
|
||||||
rel="noopener noreferrer"
|
<ForkAwesomeIcon icon={icon as IconName} fixedWidth={true} />
|
||||||
id={ id }
|
|
||||||
className={ className }
|
|
||||||
title={ title }
|
|
||||||
dir='auto'
|
|
||||||
>
|
|
||||||
<ShowIf condition={ !!icon }>
|
|
||||||
<ForkAwesomeIcon icon={ icon as IconName } fixedWidth={ true }/>
|
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
{ text }
|
{text}
|
||||||
</a>
|
</a>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,17 +11,21 @@ import { IconName } from '../fork-awesome/types'
|
||||||
import { ShowIf } from '../show-if/show-if'
|
import { ShowIf } from '../show-if/show-if'
|
||||||
import { LinkWithTextProps } from './types'
|
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 (
|
return (
|
||||||
<Link
|
<Link to={href} className={className} id={id} title={title}>
|
||||||
to={ href }
|
<ShowIf condition={!!icon}>
|
||||||
className={ className }
|
<ForkAwesomeIcon icon={icon as IconName} fixedWidth={true} />
|
||||||
id={ id }
|
|
||||||
title={ title }>
|
|
||||||
<ShowIf condition={ !!icon }>
|
|
||||||
<ForkAwesomeIcon icon={ icon as IconName } fixedWidth={ true }/>
|
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
{ text }
|
{text}
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,5 @@ import { TranslatedLinkProps } from './types'
|
||||||
|
|
||||||
export const TranslatedExternalLink: React.FC<TranslatedLinkProps> = ({ i18nKey, i18nOption, ...props }) => {
|
export const TranslatedExternalLink: React.FC<TranslatedLinkProps> = ({ i18nKey, i18nOption, ...props }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
return (
|
return <ExternalLink text={t(i18nKey, i18nOption)} {...props} />
|
||||||
<ExternalLink text={ t(i18nKey, i18nOption) } { ...props }/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,5 @@ import { TranslatedLinkProps } from './types'
|
||||||
|
|
||||||
export const TranslatedInternalLink: React.FC<TranslatedLinkProps> = ({ i18nKey, i18nOption, ...props }) => {
|
export const TranslatedInternalLink: React.FC<TranslatedLinkProps> = ({ i18nKey, i18nOption, ...props }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
return (
|
return <InternalLink text={t(i18nKey, i18nOption)} {...props} />
|
||||||
<InternalLink text={ t(i18nKey, i18nOption) } { ...props }/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,18 +9,15 @@ import { Button } from 'react-bootstrap'
|
||||||
import { ForkAwesomeIcon } from '../fork-awesome/fork-awesome-icon'
|
import { ForkAwesomeIcon } from '../fork-awesome/fork-awesome-icon'
|
||||||
|
|
||||||
export interface LockButtonProps {
|
export interface LockButtonProps {
|
||||||
locked: boolean,
|
locked: boolean
|
||||||
onLockedChanged: (newState: boolean) => void
|
onLockedChanged: (newState: boolean) => void
|
||||||
title: string
|
title: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LockButton: React.FC<LockButtonProps> = ({ locked, onLockedChanged, title }) => {
|
export const LockButton: React.FC<LockButtonProps> = ({ locked, onLockedChanged, title }) => {
|
||||||
return (
|
return (
|
||||||
<Button variant='dark' size='sm' onClick={ () => onLockedChanged(!locked) } title={ title }>
|
<Button variant='dark' size='sm' onClick={() => onLockedChanged(!locked)} title={title}>
|
||||||
{ locked
|
{locked ? <ForkAwesomeIcon icon='lock' /> : <ForkAwesomeIcon icon='unlock' />}
|
||||||
? <ForkAwesomeIcon icon='lock'/>
|
|
||||||
: <ForkAwesomeIcon icon='unlock'/>
|
|
||||||
}
|
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,24 +23,38 @@ export interface CommonModalProps {
|
||||||
'data-cy'?: string
|
'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()
|
useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal data-cy={ props['data-cy'] } show={ show } onHide={ onHide } animation={ true }
|
<Modal
|
||||||
dialogClassName={ `text-dark ${ additionalClasses ?? '' }` } size={ size }>
|
data-cy={props['data-cy']}
|
||||||
<Modal.Header closeButton={ !!closeButton }>
|
show={show}
|
||||||
|
onHide={onHide}
|
||||||
|
animation={true}
|
||||||
|
dialogClassName={`text-dark ${additionalClasses ?? ''}`}
|
||||||
|
size={size}>
|
||||||
|
<Modal.Header closeButton={!!closeButton}>
|
||||||
<Modal.Title>
|
<Modal.Title>
|
||||||
<ShowIf condition={ !!icon }>
|
<ShowIf condition={!!icon}>
|
||||||
<ForkAwesomeIcon icon={ icon as IconName }/>
|
<ForkAwesomeIcon icon={icon as IconName} />
|
||||||
|
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
{ titleI18nKey
|
{titleI18nKey ? <Trans i18nKey={titleI18nKey} /> : <span>{title}</span>}
|
||||||
? <Trans i18nKey={ titleI18nKey }/>
|
|
||||||
: <span>{ title }</span>
|
|
||||||
}
|
|
||||||
</Modal.Title>
|
</Modal.Title>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
{ children }
|
{children}
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,17 +14,23 @@ export interface DeletionModalProps extends CommonModalProps {
|
||||||
deletionButtonI18nKey: string
|
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()
|
useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommonModal show={ show } onHide={ onHide } titleI18nKey={ titleI18nKey } icon={ icon } closeButton={ true }>
|
<CommonModal show={show} onHide={onHide} titleI18nKey={titleI18nKey} icon={icon} closeButton={true}>
|
||||||
<Modal.Body className="text-dark">
|
<Modal.Body className='text-dark'>{children}</Modal.Body>
|
||||||
{ children }
|
|
||||||
</Modal.Body>
|
|
||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
<Button variant="danger" onClick={ onConfirm }>
|
<Button variant='danger' onClick={onConfirm}>
|
||||||
<Trans i18nKey={ deletionButtonI18nKey }/>
|
<Trans i18nKey={deletionButtonI18nKey} />
|
||||||
</Button>
|
</Button>
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</CommonModal>
|
</CommonModal>
|
||||||
|
|
|
@ -10,10 +10,8 @@ import { CommonModal, CommonModalProps } from './common-modal'
|
||||||
|
|
||||||
export const ErrorModal: React.FC<CommonModalProps> = ({ show, onHide, titleI18nKey, icon, children }) => {
|
export const ErrorModal: React.FC<CommonModalProps> = ({ show, onHide, titleI18nKey, icon, children }) => {
|
||||||
return (
|
return (
|
||||||
<CommonModal show={ show } onHide={ onHide } titleI18nKey={ titleI18nKey } icon={ icon } closeButton={ true }>
|
<CommonModal show={show} onHide={onHide} titleI18nKey={titleI18nKey} icon={icon} closeButton={true}>
|
||||||
<Modal.Body className="text-dark text-center">
|
<Modal.Body className='text-dark text-center'>{children}</Modal.Body>
|
||||||
{ children }
|
|
||||||
</Modal.Body>
|
|
||||||
</CommonModal>
|
</CommonModal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,22 +31,18 @@ export const MotdBanner: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!bannerState.text) {
|
if (!bannerState.text) {
|
||||||
return <span data-cy={ 'no-motd-banner' }/>
|
return <span data-cy={'no-motd-banner'} />
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert data-cy={ 'motd-banner' } variant="primary" dir="auto"
|
<Alert
|
||||||
className="mb-0 text-center d-flex flex-row justify-content-center">
|
data-cy={'motd-banner'}
|
||||||
<span className="flex-grow-1 align-self-center text-black">
|
variant='primary'
|
||||||
{ bannerState.text }
|
dir='auto'
|
||||||
</span>
|
className='mb-0 text-center d-flex flex-row justify-content-center'>
|
||||||
<Button
|
<span className='flex-grow-1 align-self-center text-black'>{bannerState.text}</span>
|
||||||
data-cy={ 'motd-dismiss' }
|
<Button data-cy={'motd-dismiss'} variant='outline-primary' size='sm' className='mx-2' onClick={dismissBanner}>
|
||||||
variant="outline-primary"
|
<ForkAwesomeIcon icon='times' />
|
||||||
size="sm"
|
|
||||||
className="mx-2"
|
|
||||||
onClick={ dismissBanner }>
|
|
||||||
<ForkAwesomeIcon icon="times"/>
|
|
||||||
</Button>
|
</Button>
|
||||||
</Alert>
|
</Alert>
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,6 +5,5 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const createNumberRangeArray = (length: number): number[] => {
|
export const createNumberRangeArray = (length: number): number[] => {
|
||||||
return Array.from(Array(length)
|
return Array.from(Array(length).keys())
|
||||||
.keys())
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,9 +13,9 @@ export interface PageItemProps {
|
||||||
|
|
||||||
export const PagerItem: React.FC<PageItemProps> = ({ index, onClick }) => {
|
export const PagerItem: React.FC<PageItemProps> = ({ index, onClick }) => {
|
||||||
return (
|
return (
|
||||||
<li className="page-item">
|
<li className='page-item'>
|
||||||
<span className="page-link" role="button" onClick={ () => onClick(index) }>
|
<span className='page-link' role='button' onClick={() => onClick(index)}>
|
||||||
{ index + 1 }
|
{index + 1}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
|
|
|
@ -15,7 +15,11 @@ export interface PaginationProps {
|
||||||
lastPageIndex: number
|
lastPageIndex: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PagerPagination: React.FC<PaginationProps> = ({ numberOfPageButtonsToShowAfterAndBeforeCurrent, onPageChange, lastPageIndex }) => {
|
export const PagerPagination: React.FC<PaginationProps> = ({
|
||||||
|
numberOfPageButtonsToShowAfterAndBeforeCurrent,
|
||||||
|
onPageChange,
|
||||||
|
lastPageIndex
|
||||||
|
}) => {
|
||||||
if (numberOfPageButtonsToShowAfterAndBeforeCurrent % 2 !== 0) {
|
if (numberOfPageButtonsToShowAfterAndBeforeCurrent % 2 !== 0) {
|
||||||
throw new Error('number of pages to show must be even!')
|
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)
|
||||||
}, [onPageChange, pageIndex])
|
}, [onPageChange, pageIndex])
|
||||||
|
|
||||||
const correctedLowerPageIndex =
|
const correctedLowerPageIndex = Math.min(
|
||||||
Math.min(
|
Math.max(Math.min(wantedLowerPageIndex, wantedLowerPageIndex + lastPageIndex - wantedUpperPageIndex), 0),
|
||||||
Math.max(
|
lastPageIndex
|
||||||
Math.min(
|
)
|
||||||
wantedLowerPageIndex,
|
|
||||||
wantedLowerPageIndex + lastPageIndex - wantedUpperPageIndex
|
|
||||||
),
|
|
||||||
0
|
|
||||||
),
|
|
||||||
lastPageIndex
|
|
||||||
)
|
|
||||||
|
|
||||||
const correctedUpperPageIndex =
|
const correctedUpperPageIndex = Math.max(
|
||||||
Math.max(
|
Math.min(Math.max(wantedUpperPageIndex, wantedUpperPageIndex - wantedLowerPageIndex), lastPageIndex),
|
||||||
Math.min(
|
0
|
||||||
Math.max(
|
)
|
||||||
wantedUpperPageIndex,
|
|
||||||
wantedUpperPageIndex - wantedLowerPageIndex
|
|
||||||
),
|
|
||||||
lastPageIndex
|
|
||||||
),
|
|
||||||
0
|
|
||||||
)
|
|
||||||
|
|
||||||
const paginationItemsBefore = Array.from(new Array(correctedPageIndex - correctedLowerPageIndex))
|
const paginationItemsBefore = Array.from(new Array(correctedPageIndex - correctedLowerPageIndex)).map((k, index) => {
|
||||||
.map((k, index) => {
|
const itemIndex = correctedLowerPageIndex + index
|
||||||
const itemIndex = correctedLowerPageIndex + index
|
return <PagerItem key={itemIndex} index={itemIndex} onClick={setPageIndex} />
|
||||||
return <PagerItem key={ itemIndex } index={ itemIndex }
|
})
|
||||||
onClick={ setPageIndex }/>
|
|
||||||
})
|
|
||||||
|
|
||||||
const paginationItemsAfter = Array.from(new Array(correctedUpperPageIndex - correctedPageIndex))
|
const paginationItemsAfter = Array.from(new Array(correctedUpperPageIndex - correctedPageIndex)).map((k, index) => {
|
||||||
.map((k, index) => {
|
const itemIndex = correctedPageIndex + index + 1
|
||||||
const itemIndex = correctedPageIndex + index + 1
|
return <PagerItem key={itemIndex} index={itemIndex} onClick={setPageIndex} />
|
||||||
return <PagerItem key={ itemIndex } index={ itemIndex } onClick={ setPageIndex }/>
|
})
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pagination dir='ltr'>
|
<Pagination dir='ltr'>
|
||||||
<ShowIf condition={ correctedLowerPageIndex > 0 }>
|
<ShowIf condition={correctedLowerPageIndex > 0}>
|
||||||
<PagerItem key={ 0 } index={ 0 } onClick={ setPageIndex }/>
|
<PagerItem key={0} index={0} onClick={setPageIndex} />
|
||||||
<Pagination.Ellipsis disabled/>
|
<Pagination.Ellipsis disabled />
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
{ paginationItemsBefore }
|
{paginationItemsBefore}
|
||||||
<Pagination.Item active>{ correctedPageIndex + 1 }</Pagination.Item>
|
<Pagination.Item active>{correctedPageIndex + 1}</Pagination.Item>
|
||||||
{ paginationItemsAfter }
|
{paginationItemsAfter}
|
||||||
<ShowIf condition={ correctedUpperPageIndex < lastPageIndex }>
|
<ShowIf condition={correctedUpperPageIndex < lastPageIndex}>
|
||||||
<Pagination.Ellipsis disabled/>
|
<Pagination.Ellipsis disabled />
|
||||||
<PagerItem key={ lastPageIndex } index={ lastPageIndex } onClick={ setPageIndex }/>
|
<PagerItem key={lastPageIndex} index={lastPageIndex} onClick={setPageIndex} />
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
</Pagination>
|
</Pagination>
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,7 +12,12 @@ export interface PagerPageProps {
|
||||||
onLastPageIndexChange: (lastPageIndex: number) => void
|
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 maxPageIndex = Math.ceil(React.Children.count(children) / numberOfElementsPerPage) - 1
|
||||||
const correctedPageIndex = Math.min(maxPageIndex, Math.max(0, pageIndex))
|
const correctedPageIndex = Math.min(maxPageIndex, Math.max(0, pageIndex))
|
||||||
|
|
||||||
|
@ -20,13 +25,12 @@ export const Pager: React.FC<PagerPageProps> = ({ children, numberOfElementsPerP
|
||||||
onLastPageIndexChange(maxPageIndex)
|
onLastPageIndexChange(maxPageIndex)
|
||||||
}, [children, maxPageIndex, numberOfElementsPerPage, onLastPageIndexChange])
|
}, [children, maxPageIndex, numberOfElementsPerPage, onLastPageIndexChange])
|
||||||
|
|
||||||
return <Fragment>
|
return (
|
||||||
{
|
<Fragment>
|
||||||
React.Children.toArray(children)
|
{React.Children.toArray(children).filter((value, index) => {
|
||||||
.filter((value, index) => {
|
const pageOfElement = Math.floor(index / numberOfElementsPerPage)
|
||||||
const pageOfElement = Math.floor((index) / numberOfElementsPerPage)
|
return pageOfElement === correctedPageIndex
|
||||||
return (pageOfElement === correctedPageIndex)
|
})}
|
||||||
})
|
</Fragment>
|
||||||
}
|
)
|
||||||
</Fragment>
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,9 @@ export const NotFoundErrorScreen: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<LandingLayout>
|
<LandingLayout>
|
||||||
<div className='text-light d-flex align-items-center justify-content-center my-5'>
|
<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>
|
</div>
|
||||||
</LandingLayout>
|
</LandingLayout>
|
||||||
)
|
)
|
||||||
|
|
|
@ -26,10 +26,10 @@ export const Redirector: React.FC = () => {
|
||||||
}, [id])
|
}, [id])
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (<NotFoundErrorScreen/>)
|
return <NotFoundErrorScreen />
|
||||||
} else if (!error && error != null) {
|
} else if (!error && error != null) {
|
||||||
return (<Redirect to={ `/n/${ id }` }/>)
|
return <Redirect to={`/n/${id}`} />
|
||||||
} else {
|
} else {
|
||||||
return (<span>Loading</span>)
|
return <span>Loading</span>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,5 +11,5 @@ export interface ShowIfProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowIf: React.FC<ShowIfProps> = ({ children, condition }) => {
|
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 {
|
export interface UserAvatarProps {
|
||||||
size?: 'sm' | 'lg'
|
size?: 'sm' | 'lg'
|
||||||
name: string;
|
name: string
|
||||||
photo: string;
|
photo: string
|
||||||
additionalClasses?: string;
|
additionalClasses?: string
|
||||||
showName?: boolean
|
showName?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,15 +21,15 @@ const UserAvatar: React.FC<UserAvatarProps> = ({ name, photo, size, additionalCl
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={ 'd-inline-flex align-items-center ' + additionalClasses }>
|
<span className={'d-inline-flex align-items-center ' + additionalClasses}>
|
||||||
<img
|
<img
|
||||||
src={ photo }
|
src={photo}
|
||||||
className={ `user-avatar rounded mr-1 ${ size ?? '' }` }
|
className={`user-avatar rounded mr-1 ${size ?? ''}`}
|
||||||
alt={ t('common.avatarOf', { name }) }
|
alt={t('common.avatarOf', { name })}
|
||||||
title={ name }
|
title={name}
|
||||||
/>
|
/>
|
||||||
<ShowIf condition={ showName }>
|
<ShowIf condition={showName}>
|
||||||
<span className="mx-1 user-line-name">{ name }</span>
|
<span className='mx-1 user-line-name'>{name}</span>
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,8 +9,8 @@ import { ForkAwesomeIcon } from '../fork-awesome/fork-awesome-icon'
|
||||||
|
|
||||||
export const WaitSpinner: React.FC = () => {
|
export const WaitSpinner: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className={ 'm-3 d-flex align-items-center justify-content-center' }>
|
<div className={'m-3 d-flex align-items-center justify-content-center'}>
|
||||||
<ForkAwesomeIcon icon={ 'spinner' } className={ 'fa-spin' }/>
|
<ForkAwesomeIcon icon={'spinner'} className={'fa-spin'} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,11 +14,13 @@ export const ErrorWhileLoadingNoteAlert: React.FC<SimpleAlertProps> = ({ show })
|
||||||
useTranslation()
|
useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ShowIf condition={ show }>
|
<ShowIf condition={show}>
|
||||||
<Alert variant={ 'danger' } className={ 'my-2' }>
|
<Alert variant={'danger'} className={'my-2'}>
|
||||||
<b><Trans i18nKey={ 'views.readOnly.error.title' }/></b>
|
<b>
|
||||||
<br/>
|
<Trans i18nKey={'views.readOnly.error.title'} />
|
||||||
<Trans i18nKey={ 'views.readOnly.error.description' }/>
|
</b>
|
||||||
|
<br />
|
||||||
|
<Trans i18nKey={'views.readOnly.error.description'} />
|
||||||
</Alert>
|
</Alert>
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,9 +12,9 @@ import { SimpleAlertProps } from '../common/simple-alert/simple-alert-props'
|
||||||
|
|
||||||
export const LoadingNoteAlert: React.FC<SimpleAlertProps> = ({ show }) => {
|
export const LoadingNoteAlert: React.FC<SimpleAlertProps> = ({ show }) => {
|
||||||
return (
|
return (
|
||||||
<ShowIf condition={ show }>
|
<ShowIf condition={show}>
|
||||||
<Alert variant={ 'info' } className={ 'my-2' }>
|
<Alert variant={'info'} className={'my-2'}>
|
||||||
<Trans i18nKey={ 'views.readOnly.loading' }/>
|
<Trans i18nKey={'views.readOnly.loading'} />
|
||||||
</Alert>
|
</Alert>
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
)
|
)
|
||||||
|
|
|
@ -39,32 +39,38 @@ export const DocumentInfobar: React.FC<DocumentInfobarProps> = ({
|
||||||
const assetsBaseUrl = useCustomizeAssetsUrl()
|
const assetsBaseUrl = useCustomizeAssetsUrl()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ 'd-flex flex-row my-3 document-infobar' }>
|
<div className={'d-flex flex-row my-3 document-infobar'}>
|
||||||
<div className={ 'col-md' }> </div>
|
<div className={'col-md'}> </div>
|
||||||
<div className={ 'd-flex flex-fill' }>
|
<div className={'d-flex flex-fill'}>
|
||||||
<div className={ 'd-flex flex-column' }>
|
<div className={'d-flex flex-column'}>
|
||||||
<DocumentInfoTimeLine
|
<DocumentInfoTimeLine
|
||||||
mode={ DocumentInfoLineWithTimeMode.CREATED }
|
mode={DocumentInfoLineWithTimeMode.CREATED}
|
||||||
time={ createdTime }
|
time={createdTime}
|
||||||
userName={ createdAuthor }
|
userName={createdAuthor}
|
||||||
profileImageSrc={ `${ assetsBaseUrl }/img/avatar.png` }/>
|
profileImageSrc={`${assetsBaseUrl}/img/avatar.png`}
|
||||||
|
/>
|
||||||
<DocumentInfoTimeLine
|
<DocumentInfoTimeLine
|
||||||
mode={ DocumentInfoLineWithTimeMode.EDITED }
|
mode={DocumentInfoLineWithTimeMode.EDITED}
|
||||||
time={ changedTime }
|
time={changedTime}
|
||||||
userName={ changedAuthor }
|
userName={changedAuthor}
|
||||||
profileImageSrc={ `${ assetsBaseUrl }/img/avatar.png` }/>
|
profileImageSrc={`${assetsBaseUrl}/img/avatar.png`}
|
||||||
<hr/>
|
/>
|
||||||
|
<hr />
|
||||||
</div>
|
</div>
|
||||||
<span className={ 'ml-auto' }>
|
<span className={'ml-auto'}>
|
||||||
{ viewCount } <Trans i18nKey={ 'views.readOnly.viewCount' }/>
|
{viewCount} <Trans i18nKey={'views.readOnly.viewCount'} />
|
||||||
<ShowIf condition={ editable }>
|
<ShowIf condition={editable}>
|
||||||
<InternalLink text={ '' } href={ `/n/${ noteId }` } icon={ 'pencil' }
|
<InternalLink
|
||||||
className={ 'text-primary text-decoration-none mx-1' }
|
text={''}
|
||||||
title={ t('views.readOnly.editNote') }/>
|
href={`/n/${noteId}`}
|
||||||
|
icon={'pencil'}
|
||||||
|
className={'text-primary text-decoration-none mx-1'}
|
||||||
|
title={t('views.readOnly.editNote')}
|
||||||
|
/>
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={ 'col-md' }> </div>
|
<div className={'col-md'}> </div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,6 @@ import { LoadingNoteAlert } from './LoadingNoteAlert'
|
||||||
import { RendererType } from '../render-page/rendering-message'
|
import { RendererType } from '../render-page/rendering-message'
|
||||||
|
|
||||||
export const DocumentReadOnlyPage: React.FC = () => {
|
export const DocumentReadOnlyPage: React.FC = () => {
|
||||||
|
|
||||||
useTranslation()
|
useTranslation()
|
||||||
const { id } = useParams<EditorPagePathParams>()
|
const { id } = useParams<EditorPagePathParams>()
|
||||||
|
|
||||||
|
@ -39,28 +38,30 @@ export const DocumentReadOnlyPage: React.FC = () => {
|
||||||
const noteDetails = useSelector((state: ApplicationState) => state.noteDetails)
|
const noteDetails = useSelector((state: ApplicationState) => state.noteDetails)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ 'd-flex flex-column mvh-100 bg-light' }>
|
<div className={'d-flex flex-column mvh-100 bg-light'}>
|
||||||
<MotdBanner/>
|
<MotdBanner />
|
||||||
<AppBar mode={ AppBarMode.BASIC }/>
|
<AppBar mode={AppBarMode.BASIC} />
|
||||||
<div className={ 'container' }>
|
<div className={'container'}>
|
||||||
<ErrorWhileLoadingNoteAlert show={ error }/>
|
<ErrorWhileLoadingNoteAlert show={error} />
|
||||||
<LoadingNoteAlert show={ loading }/>
|
<LoadingNoteAlert show={loading} />
|
||||||
</div>
|
</div>
|
||||||
<ShowIf condition={ !error && !loading }>
|
<ShowIf condition={!error && !loading}>
|
||||||
<DocumentInfobar
|
<DocumentInfobar
|
||||||
changedAuthor={ noteDetails.lastChange.userName ?? '' }
|
changedAuthor={noteDetails.lastChange.userName ?? ''}
|
||||||
changedTime={ noteDetails.lastChange.timestamp }
|
changedTime={noteDetails.lastChange.timestamp}
|
||||||
createdAuthor={ 'Test' }
|
createdAuthor={'Test'}
|
||||||
createdTime={ noteDetails.createTime }
|
createdTime={noteDetails.createTime}
|
||||||
editable={ true }
|
editable={true}
|
||||||
noteId={ id }
|
noteId={id}
|
||||||
viewCount={ noteDetails.viewCount }
|
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>
|
</ShowIf>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -36,31 +36,31 @@ export const AppBar: React.FC<AppBarProps> = ({ mode }) => {
|
||||||
const noteFrontmatter = useSelector((state: ApplicationState) => state.noteDetails.frontmatter, equal)
|
const noteFrontmatter = useSelector((state: ApplicationState) => state.noteDetails.frontmatter, equal)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Navbar bg={ 'light' }>
|
<Navbar bg={'light'}>
|
||||||
<Nav className="mr-auto d-flex align-items-center">
|
<Nav className='mr-auto d-flex align-items-center'>
|
||||||
<NavbarBranding/>
|
<NavbarBranding />
|
||||||
<ShowIf condition={ mode === AppBarMode.EDITOR }>
|
<ShowIf condition={mode === AppBarMode.EDITOR}>
|
||||||
<EditorViewMode/>
|
<EditorViewMode />
|
||||||
<SyncScrollButtons/>
|
<SyncScrollButtons />
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
<DarkModeButton/>
|
<DarkModeButton />
|
||||||
<ShowIf condition={ mode === AppBarMode.EDITOR }>
|
<ShowIf condition={mode === AppBarMode.EDITOR}>
|
||||||
<ShowIf condition={noteFrontmatter.type === NoteType.SLIDE}>
|
<ShowIf condition={noteFrontmatter.type === NoteType.SLIDE}>
|
||||||
<SlideModeButton/>
|
<SlideModeButton />
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
<ShowIf condition={noteFrontmatter.type !== NoteType.SLIDE}>
|
<ShowIf condition={noteFrontmatter.type !== NoteType.SLIDE}>
|
||||||
<ReadOnlyModeButton/>
|
<ReadOnlyModeButton />
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
<HelpButton/>
|
<HelpButton />
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
</Nav>
|
</Nav>
|
||||||
<Nav className="d-flex align-items-center text-secondary">
|
<Nav className='d-flex align-items-center text-secondary'>
|
||||||
<NewNoteButton/>
|
<NewNoteButton />
|
||||||
<ShowIf condition={ !userExists }>
|
<ShowIf condition={!userExists}>
|
||||||
<SignInButton size={ 'sm' }/>
|
<SignInButton size={'sm'} />
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
<ShowIf condition={ userExists }>
|
<ShowIf condition={userExists}>
|
||||||
<UserDropdown/>
|
<UserDropdown />
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
</Nav>
|
</Nav>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
|
|
|
@ -21,27 +21,20 @@ const DarkModeButton: React.FC = () => {
|
||||||
const darkModeEnabled = useIsDarkModeActivated() ? DarkModeState.DARK : DarkModeState.LIGHT
|
const darkModeEnabled = useIsDarkModeActivated() ? DarkModeState.DARK : DarkModeState.LIGHT
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToggleButtonGroup
|
<ToggleButtonGroup type='radio' name='dark-mode' value={darkModeEnabled} className='ml-2'>
|
||||||
type="radio"
|
|
||||||
name="dark-mode"
|
|
||||||
value={ darkModeEnabled }
|
|
||||||
className="ml-2"
|
|
||||||
>
|
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
value={ DarkModeState.DARK }
|
value={DarkModeState.DARK}
|
||||||
variant="outline-secondary"
|
variant='outline-secondary'
|
||||||
title={ t('editor.darkMode.switchToDark') }
|
title={t('editor.darkMode.switchToDark')}
|
||||||
onChange={ () => setDarkMode(true) }
|
onChange={() => setDarkMode(true)}>
|
||||||
>
|
<ForkAwesomeIcon icon='moon' />
|
||||||
<ForkAwesomeIcon icon="moon"/>
|
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
value={ DarkModeState.LIGHT }
|
value={DarkModeState.LIGHT}
|
||||||
variant="outline-secondary"
|
variant='outline-secondary'
|
||||||
title={ t('editor.darkMode.switchToLight') }
|
title={t('editor.darkMode.switchToLight')}
|
||||||
onChange={ () => setDarkMode(false) }
|
onChange={() => setDarkMode(false)}>
|
||||||
>
|
<ForkAwesomeIcon icon='sun-o' />
|
||||||
<ForkAwesomeIcon icon="sun-o"/>
|
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
</ToggleButtonGroup>
|
</ToggleButtonGroup>
|
||||||
)
|
)
|
||||||
|
|
|
@ -23,20 +23,20 @@ export const EditorViewMode: React.FC = () => {
|
||||||
const editorMode = useSelector((state: ApplicationState) => state.editorConfig.editorMode)
|
const editorMode = useSelector((state: ApplicationState) => state.editorConfig.editorMode)
|
||||||
return (
|
return (
|
||||||
<ToggleButtonGroup
|
<ToggleButtonGroup
|
||||||
type="radio"
|
type='radio'
|
||||||
name="options"
|
name='options'
|
||||||
value={ editorMode }
|
value={editorMode}
|
||||||
onChange={ (value: EditorMode) => {
|
onChange={(value: EditorMode) => {
|
||||||
setEditorMode(value)
|
setEditorMode(value)
|
||||||
} }>
|
}}>
|
||||||
<ToggleButton value={ EditorMode.PREVIEW } variant="outline-secondary" title={ t('editor.viewMode.view') }>
|
<ToggleButton value={EditorMode.PREVIEW} variant='outline-secondary' title={t('editor.viewMode.view')}>
|
||||||
<ForkAwesomeIcon icon="eye"/>
|
<ForkAwesomeIcon icon='eye' />
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
<ToggleButton value={ EditorMode.BOTH } variant="outline-secondary" title={ t('editor.viewMode.both') }>
|
<ToggleButton value={EditorMode.BOTH} variant='outline-secondary' title={t('editor.viewMode.both')}>
|
||||||
<ForkAwesomeIcon icon="columns"/>
|
<ForkAwesomeIcon icon='columns' />
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
<ToggleButton value={ EditorMode.EDITOR } variant="outline-secondary" title={ t('editor.viewMode.edit') }>
|
<ToggleButton value={EditorMode.EDITOR} variant='outline-secondary' title={t('editor.viewMode.edit')}>
|
||||||
<ForkAwesomeIcon icon="pencil"/>
|
<ForkAwesomeIcon icon='pencil' />
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
</ToggleButtonGroup>
|
</ToggleButtonGroup>
|
||||||
)
|
)
|
||||||
|
|
|
@ -8,31 +8,38 @@ import React, { Suspense, useCallback } from 'react'
|
||||||
import { WaitSpinner } from '../../../common/wait-spinner/wait-spinner'
|
import { WaitSpinner } from '../../../common/wait-spinner/wait-spinner'
|
||||||
|
|
||||||
export interface CheatsheetLineProps {
|
export interface CheatsheetLineProps {
|
||||||
code: string,
|
code: string
|
||||||
onTaskCheckedChange: (newValue: boolean) => void
|
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'))
|
const BasicMarkdownRenderer = React.lazy(() => import('../../../markdown-renderer/basic-markdown-renderer'))
|
||||||
|
|
||||||
export const CheatsheetLine: React.FC<CheatsheetLineProps> = ({ code, onTaskCheckedChange }) => {
|
export const CheatsheetLine: React.FC<CheatsheetLineProps> = ({ code, onTaskCheckedChange }) => {
|
||||||
const checkboxClick = useCallback((lineInMarkdown: number, newValue: boolean) => {
|
const checkboxClick = useCallback(
|
||||||
onTaskCheckedChange(newValue)
|
(lineInMarkdown: number, newValue: boolean) => {
|
||||||
}, [onTaskCheckedChange])
|
onTaskCheckedChange(newValue)
|
||||||
|
},
|
||||||
|
[onTaskCheckedChange]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={ <tr>
|
<Suspense
|
||||||
<td colSpan={ 2 }><WaitSpinner/></td>
|
fallback={
|
||||||
</tr> }>
|
<tr>
|
||||||
|
<td colSpan={2}>
|
||||||
|
<WaitSpinner />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<BasicMarkdownRenderer
|
<BasicMarkdownRenderer content={code} baseUrl={'https://example.org'} onTaskCheckedChange={checkboxClick} />
|
||||||
content={ code }
|
|
||||||
baseUrl={ 'https://example.org' }
|
|
||||||
onTaskCheckedChange={ checkboxClick }/>
|
|
||||||
</td>
|
</td>
|
||||||
<td className={ 'markdown-body' }>
|
<td className={'markdown-body'}>
|
||||||
<HighlightedCode code={ code } wrapLines={ true } startLineNumber={ 1 } language={ 'markdown' }/>
|
<HighlightedCode code={code} wrapLines={true} startLineNumber={1} language={'markdown'} />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
|
@ -13,40 +13,46 @@ import { CheatsheetLine } from './cheatsheet-line'
|
||||||
export const Cheatsheet: React.FC = () => {
|
export const Cheatsheet: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [checked, setChecked] = useState<boolean>(false)
|
const [checked, setChecked] = useState<boolean>(false)
|
||||||
const codes = useMemo(() => [
|
const codes = useMemo(
|
||||||
`**${ t('editor.editorToolbar.bold') }**`,
|
() => [
|
||||||
`*${ t('editor.editorToolbar.italic') }*`,
|
`**${t('editor.editorToolbar.bold')}**`,
|
||||||
`++${ t('editor.editorToolbar.underline') }++`,
|
`*${t('editor.editorToolbar.italic')}*`,
|
||||||
`~~${ t('editor.editorToolbar.strikethrough') }~~`,
|
`++${t('editor.editorToolbar.underline')}++`,
|
||||||
'H~2~O',
|
`~~${t('editor.editorToolbar.strikethrough')}~~`,
|
||||||
'19^th^',
|
'H~2~O',
|
||||||
`==${ t('editor.help.cheatsheet.highlightedText') }==`,
|
'19^th^',
|
||||||
`# ${ t('editor.editorToolbar.header') }`,
|
`==${t('editor.help.cheatsheet.highlightedText')}==`,
|
||||||
`\`${ t('editor.editorToolbar.code') }\``,
|
`# ${t('editor.editorToolbar.header')}`,
|
||||||
'```javascript=\nvar x = 5;\n```',
|
`\`${t('editor.editorToolbar.code')}\``,
|
||||||
`> ${ t('editor.editorToolbar.blockquote') }`,
|
'```javascript=\nvar x = 5;\n```',
|
||||||
`- ${ t('editor.editorToolbar.unorderedList') }`,
|
`> ${t('editor.editorToolbar.blockquote')}`,
|
||||||
`1. ${ t('editor.editorToolbar.orderedList') }`,
|
`- ${t('editor.editorToolbar.unorderedList')}`,
|
||||||
`- [${ checked ? 'x' : ' ' }] ${ t('editor.editorToolbar.checkList') }`,
|
`1. ${t('editor.editorToolbar.orderedList')}`,
|
||||||
`[${ t('editor.editorToolbar.link') }](https://example.com)`,
|
`- [${checked ? 'x' : ' '}] ${t('editor.editorToolbar.checkList')}`,
|
||||||
``,
|
`[${t('editor.editorToolbar.link')}](https://example.com)`,
|
||||||
':smile:',
|
``,
|
||||||
`:::info\n${ t('editor.help.cheatsheet.exampleAlert') }\n:::`
|
':smile:',
|
||||||
], [checked, t])
|
`:::info\n${t('editor.help.cheatsheet.exampleAlert')}\n:::`
|
||||||
|
],
|
||||||
|
[checked, t]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table className="table-condensed table-cheatsheet">
|
<Table className='table-condensed table-cheatsheet'>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th><Trans i18nKey='editor.help.cheatsheet.example'/></th>
|
<th>
|
||||||
<th><Trans i18nKey='editor.help.cheatsheet.syntax'/></th>
|
<Trans i18nKey='editor.help.cheatsheet.example' />
|
||||||
</tr>
|
</th>
|
||||||
|
<th>
|
||||||
|
<Trans i18nKey='editor.help.cheatsheet.syntax' />
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{
|
{codes.map((code) => (
|
||||||
codes.map((code) =>
|
<CheatsheetLine code={code} key={code} onTaskCheckedChange={setChecked} />
|
||||||
<CheatsheetLine code={ code } key={ code } onTaskCheckedChange={ setChecked }/>)
|
))}
|
||||||
}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</Table>
|
</Table>
|
||||||
)
|
)
|
||||||
|
|
|
@ -17,11 +17,15 @@ export const HelpButton: React.FC = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Button title={ t('editor.documentBar.help') } className='ml-2 text-secondary' size='sm' variant='outline-light'
|
<Button
|
||||||
onClick={ () => setShow(true) }>
|
title={t('editor.documentBar.help')}
|
||||||
<ForkAwesomeIcon icon="question-circle"/>
|
className='ml-2 text-secondary'
|
||||||
|
size='sm'
|
||||||
|
variant='outline-light'
|
||||||
|
onClick={() => setShow(true)}>
|
||||||
|
<ForkAwesomeIcon icon='question-circle' />
|
||||||
</Button>
|
</Button>
|
||||||
<HelpModal show={ show } onHide={ onHide }/>
|
<HelpModal show={show} onHide={onHide} />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ export enum HelpTabStatus {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HelpModalProps {
|
export interface HelpModalProps {
|
||||||
show: boolean,
|
show: boolean
|
||||||
onHide: () => void
|
onHide: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,40 +30,41 @@ export const HelpModal: React.FC<HelpModalProps> = ({ show, onHide }) => {
|
||||||
const tabContent = useMemo(() => {
|
const tabContent = useMemo(() => {
|
||||||
switch (tab) {
|
switch (tab) {
|
||||||
case HelpTabStatus.Cheatsheet:
|
case HelpTabStatus.Cheatsheet:
|
||||||
return (<Cheatsheet/>)
|
return <Cheatsheet />
|
||||||
case HelpTabStatus.Shortcuts:
|
case HelpTabStatus.Shortcuts:
|
||||||
return (<Shortcut/>)
|
return <Shortcut />
|
||||||
case HelpTabStatus.Links:
|
case HelpTabStatus.Links:
|
||||||
return (<Links/>)
|
return <Links />
|
||||||
}
|
}
|
||||||
}, [tab])
|
}, [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 (
|
return (
|
||||||
<CommonModal icon={ 'question-circle' } show={ show } onHide={ onHide } title={ tabTitle }>
|
<CommonModal icon={'question-circle'} show={show} onHide={onHide} title={tabTitle}>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<nav className='nav nav-tabs'>
|
<nav className='nav nav-tabs'>
|
||||||
<Button
|
<Button
|
||||||
variant={ 'light' }
|
variant={'light'}
|
||||||
className={ `nav-link nav-item ${ tab === HelpTabStatus.Cheatsheet ? 'active' : '' }` }
|
className={`nav-link nav-item ${tab === HelpTabStatus.Cheatsheet ? 'active' : ''}`}
|
||||||
onClick={ () => setTab(HelpTabStatus.Cheatsheet) }>
|
onClick={() => setTab(HelpTabStatus.Cheatsheet)}>
|
||||||
<Trans i18nKey={ 'editor.help.cheatsheet.title' }/>
|
<Trans i18nKey={'editor.help.cheatsheet.title'} />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={ 'light' }
|
variant={'light'}
|
||||||
className={ `nav-link nav-item ${ tab === HelpTabStatus.Shortcuts ? 'active' : '' }` }
|
className={`nav-link nav-item ${tab === HelpTabStatus.Shortcuts ? 'active' : ''}`}
|
||||||
onClick={ () => setTab(HelpTabStatus.Shortcuts) }>
|
onClick={() => setTab(HelpTabStatus.Shortcuts)}>
|
||||||
<Trans i18nKey={ 'editor.help.shortcuts.title' }/>
|
<Trans i18nKey={'editor.help.shortcuts.title'} />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={ 'light' }
|
variant={'light'}
|
||||||
className={ `nav-link nav-item ${ tab === HelpTabStatus.Links ? 'active' : '' }` }
|
className={`nav-link nav-item ${tab === HelpTabStatus.Links ? 'active' : ''}`}
|
||||||
onClick={ () => setTab(HelpTabStatus.Links) }>
|
onClick={() => setTab(HelpTabStatus.Links)}>
|
||||||
<Trans i18nKey={ 'editor.help.links.title' }/>
|
<Trans i18nKey={'editor.help.links.title'} />
|
||||||
</Button>
|
</Button>
|
||||||
</nav>
|
</nav>
|
||||||
{ tabContent }
|
{tabContent}
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
</CommonModal>)
|
</CommonModal>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,15 +15,17 @@ export const Links: React.FC = () => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row className={ 'justify-content-center pt-4' }>
|
<Row className={'justify-content-center pt-4'}>
|
||||||
<Col lg={ 4 }>
|
<Col lg={4}>
|
||||||
<h3><Trans i18nKey='editor.help.contacts.title'/></h3>
|
<h3>
|
||||||
|
<Trans i18nKey='editor.help.contacts.title' />
|
||||||
|
</h3>
|
||||||
<div>
|
<div>
|
||||||
<ul className="list-unstyled">
|
<ul className='list-unstyled'>
|
||||||
<li>
|
<li>
|
||||||
<TranslatedExternalLink
|
<TranslatedExternalLink
|
||||||
i18nKey='editor.help.contacts.community'
|
i18nKey='editor.help.contacts.community'
|
||||||
href={ links.community }
|
href={links.community}
|
||||||
icon='users'
|
icon='users'
|
||||||
className='text-primary'
|
className='text-primary'
|
||||||
/>
|
/>
|
||||||
|
@ -31,8 +33,8 @@ export const Links: React.FC = () => {
|
||||||
<li>
|
<li>
|
||||||
<TranslatedExternalLink
|
<TranslatedExternalLink
|
||||||
i18nKey='editor.help.contacts.meetUsOn'
|
i18nKey='editor.help.contacts.meetUsOn'
|
||||||
i18nOption={ { service: 'Matrix' } }
|
i18nOption={{ service: 'Matrix' }}
|
||||||
href={ links.chat }
|
href={links.chat}
|
||||||
icon='hashtag'
|
icon='hashtag'
|
||||||
className='text-primary'
|
className='text-primary'
|
||||||
/>
|
/>
|
||||||
|
@ -40,7 +42,7 @@ export const Links: React.FC = () => {
|
||||||
<li>
|
<li>
|
||||||
<TranslatedExternalLink
|
<TranslatedExternalLink
|
||||||
i18nKey='editor.help.contacts.reportIssue'
|
i18nKey='editor.help.contacts.reportIssue'
|
||||||
href={ links.backendIssues }
|
href={links.backendIssues}
|
||||||
icon='tag'
|
icon='tag'
|
||||||
className='text-primary'
|
className='text-primary'
|
||||||
/>
|
/>
|
||||||
|
@ -48,7 +50,7 @@ export const Links: React.FC = () => {
|
||||||
<li>
|
<li>
|
||||||
<TranslatedExternalLink
|
<TranslatedExternalLink
|
||||||
i18nKey='editor.help.contacts.helpTranslating'
|
i18nKey='editor.help.contacts.helpTranslating'
|
||||||
href={ links.translate }
|
href={links.translate}
|
||||||
icon='language'
|
icon='language'
|
||||||
className='text-primary'
|
className='text-primary'
|
||||||
/>
|
/>
|
||||||
|
@ -56,10 +58,12 @@ export const Links: React.FC = () => {
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col lg={ 4 }>
|
<Col lg={4}>
|
||||||
<h3><Trans i18nKey='editor.help.documents.title'/></h3>
|
<h3>
|
||||||
|
<Trans i18nKey='editor.help.documents.title' />
|
||||||
|
</h3>
|
||||||
<div>
|
<div>
|
||||||
<ul className="list-unstyled">
|
<ul className='list-unstyled'>
|
||||||
<li>
|
<li>
|
||||||
<TranslatedInternalLink
|
<TranslatedInternalLink
|
||||||
i18nKey='editor.help.documents.features'
|
i18nKey='editor.help.documents.features'
|
||||||
|
|
|
@ -29,31 +29,30 @@ export const Shortcut: React.FC = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Row className={ 'justify-content-center pt-4' }>
|
<Row className={'justify-content-center pt-4'}>
|
||||||
{ Object.keys(shortcutMap)
|
{Object.keys(shortcutMap).map((category) => {
|
||||||
.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 (
|
return (
|
||||||
<Card key={ category } className={ 'm-2 w-50' }>
|
<ListGroup.Item key={functionName} className={'d-flex justify-content-between'}>
|
||||||
<Card.Header>{ category }</Card.Header>
|
<span>
|
||||||
<ListGroup variant="flush">
|
<Trans i18nKey={functionName} />
|
||||||
{ 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>)
|
|
||||||
}
|
|
||||||
</span>
|
</span>
|
||||||
</ListGroup.Item>
|
<span>
|
||||||
)
|
{shortcuts.map((shortcut, shortcutIndex) => (
|
||||||
}) }
|
<Fragment key={shortcutIndex}>{shortcut}</Fragment>
|
||||||
</ListGroup>
|
))}
|
||||||
</Card>)
|
</span>
|
||||||
})
|
</ListGroup.Item>
|
||||||
}
|
)
|
||||||
|
})}
|
||||||
|
</ListGroup>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</Row>
|
</Row>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,11 +20,12 @@ export const NavbarBranding: React.FC = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Navbar.Brand>
|
<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
|
<HedgeDocLogoWithText
|
||||||
logoType={ darkModeActivated ? HedgeDocLogoType.WB_HORIZONTAL : HedgeDocLogoType.BW_HORIZONTAL }
|
logoType={darkModeActivated ? HedgeDocLogoType.WB_HORIZONTAL : HedgeDocLogoType.BW_HORIZONTAL}
|
||||||
size={ HedgeDocLogoSize.SMALL }/>
|
size={HedgeDocLogoSize.SMALL}
|
||||||
<Branding inline={ true }/>
|
/>
|
||||||
|
<Branding inline={true} />
|
||||||
</Link>
|
</Link>
|
||||||
</Navbar.Brand>
|
</Navbar.Brand>
|
||||||
)
|
)
|
||||||
|
|
|
@ -13,8 +13,8 @@ export const NewNoteButton: React.FC = () => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button className="mx-2" size="sm" variant="primary">
|
<Button className='mx-2' size='sm' variant='primary'>
|
||||||
<ForkAwesomeIcon icon="plus"/> <Trans i18nKey="editor.appBar.new"/>
|
<ForkAwesomeIcon icon='plus' /> <Trans i18nKey='editor.appBar.new' />
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,10 +17,13 @@ export const ReadOnlyModeButton: React.FC = () => {
|
||||||
const { id } = useParams<EditorPagePathParams>()
|
const { id } = useParams<EditorPagePathParams>()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={ `/s/${ id }` } target='_blank'>
|
<Link to={`/s/${id}`} target='_blank'>
|
||||||
<Button title={ t('editor.documentBar.readOnlyMode') } className="ml-2 text-secondary" size="sm"
|
<Button
|
||||||
variant="outline-light">
|
title={t('editor.documentBar.readOnlyMode')}
|
||||||
<ForkAwesomeIcon icon="file-text-o"/>
|
className='ml-2 text-secondary'
|
||||||
|
size='sm'
|
||||||
|
variant='outline-light'>
|
||||||
|
<ForkAwesomeIcon icon='file-text-o' />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
|
|
|
@ -17,10 +17,13 @@ export const SlideModeButton: React.FC = () => {
|
||||||
const { id } = useParams<EditorPagePathParams>()
|
const { id } = useParams<EditorPagePathParams>()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={ `/p/${ id }` } target='_blank'>
|
<Link to={`/p/${id}`} target='_blank'>
|
||||||
<Button title={ t('editor.documentBar.slideMode') } className="ml-2 text-secondary" size="sm"
|
<Button
|
||||||
variant="outline-light">
|
title={t('editor.documentBar.slideMode')}
|
||||||
<ForkAwesomeIcon icon="television"/>
|
className='ml-2 text-secondary'
|
||||||
|
size='sm'
|
||||||
|
variant='outline-light'>
|
||||||
|
<ForkAwesomeIcon icon='television' />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
|
|
|
@ -20,27 +20,31 @@ enum SyncScrollState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SyncScrollButtons: React.FC = () => {
|
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()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToggleButtonGroup type="radio" defaultValue={ [] } name="sync-scroll" className={ 'ml-2 sync-scroll-buttons' }
|
<ToggleButtonGroup
|
||||||
value={ syncScrollEnabled }>
|
type='radio'
|
||||||
|
defaultValue={[]}
|
||||||
|
name='sync-scroll'
|
||||||
|
className={'ml-2 sync-scroll-buttons'}
|
||||||
|
value={syncScrollEnabled}>
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
variant={ 'outline-secondary' }
|
variant={'outline-secondary'}
|
||||||
title={ t('editor.appBar.syncScroll.enable') }
|
title={t('editor.appBar.syncScroll.enable')}
|
||||||
onChange={ () => setEditorSyncScroll(true) }
|
onChange={() => setEditorSyncScroll(true)}
|
||||||
value={ SyncScrollState.SYNCED }
|
value={SyncScrollState.SYNCED}>
|
||||||
>
|
<EnabledScrollIcon />
|
||||||
<EnabledScrollIcon/>
|
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
variant={ 'outline-secondary' }
|
variant={'outline-secondary'}
|
||||||
title={ t('editor.appBar.syncScroll.disable') }
|
title={t('editor.appBar.syncScroll.disable')}
|
||||||
onChange={ () => setEditorSyncScroll(false) }
|
onChange={() => setEditorSyncScroll(false)}
|
||||||
value={ SyncScrollState.UNSYNCED }
|
value={SyncScrollState.UNSYNCED}>
|
||||||
>
|
<DisabledScrollIcon />
|
||||||
<DisabledScrollIcon/>
|
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
</ToggleButtonGroup>
|
</ToggleButtonGroup>
|
||||||
)
|
)
|
||||||
|
|
|
@ -15,11 +15,9 @@ export interface DocumentInfoLineProps {
|
||||||
|
|
||||||
export const DocumentInfoLine: React.FC<DocumentInfoLineProps> = ({ icon, size, children }) => {
|
export const DocumentInfoLine: React.FC<DocumentInfoLineProps> = ({ icon, size, children }) => {
|
||||||
return (
|
return (
|
||||||
<span className={ 'd-flex align-items-center' }>
|
<span className={'d-flex align-items-center'}>
|
||||||
<ForkAwesomeIcon icon={ icon } size={ size } fixedWidth={ true } className={ 'mx-2' }/>
|
<ForkAwesomeIcon icon={icon} size={size} fixedWidth={true} className={'mx-2'} />
|
||||||
<i className={ 'd-flex align-items-center' }>
|
<i className={'d-flex align-items-center'}>{children}</i>
|
||||||
{ children }
|
|
||||||
</i>
|
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { UnitalicBoldText } from './unitalic-bold-text'
|
||||||
import { useCustomizeAssetsUrl } from '../../../../hooks/common/use-customize-assets-url'
|
import { useCustomizeAssetsUrl } from '../../../../hooks/common/use-customize-assets-url'
|
||||||
|
|
||||||
export interface DocumentInfoModalProps {
|
export interface DocumentInfoModalProps {
|
||||||
show: boolean,
|
show: boolean
|
||||||
onHide: () => void
|
onHide: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,42 +24,38 @@ export const DocumentInfoModal: React.FC<DocumentInfoModalProps> = ({ show, onHi
|
||||||
|
|
||||||
// TODO Replace hardcoded mock data with real/mock API requests
|
// TODO Replace hardcoded mock data with real/mock API requests
|
||||||
return (
|
return (
|
||||||
<CommonModal
|
<CommonModal show={show} onHide={onHide} closeButton={true} titleI18nKey={'editor.modal.documentInfo.title'}>
|
||||||
show={ show }
|
|
||||||
onHide={ onHide }
|
|
||||||
closeButton={ true }
|
|
||||||
titleI18nKey={ 'editor.modal.documentInfo.title' }>
|
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<ListGroup>
|
<ListGroup>
|
||||||
<ListGroup.Item>
|
<ListGroup.Item>
|
||||||
<DocumentInfoTimeLine
|
<DocumentInfoTimeLine
|
||||||
size={ '2x' }
|
size={'2x'}
|
||||||
mode={ DocumentInfoLineWithTimeMode.CREATED }
|
mode={DocumentInfoLineWithTimeMode.CREATED}
|
||||||
time={ DateTime.local()
|
time={DateTime.local().minus({ days: 11 })}
|
||||||
.minus({ days: 11 }) }
|
userName={'Tilman'}
|
||||||
userName={ 'Tilman' }
|
profileImageSrc={`${assetsBaseUrl}img/avatar.png`}
|
||||||
profileImageSrc={ `${ assetsBaseUrl }img/avatar.png` }/>
|
/>
|
||||||
</ListGroup.Item>
|
</ListGroup.Item>
|
||||||
<ListGroup.Item>
|
<ListGroup.Item>
|
||||||
<DocumentInfoTimeLine
|
<DocumentInfoTimeLine
|
||||||
size={ '2x' }
|
size={'2x'}
|
||||||
mode={ DocumentInfoLineWithTimeMode.EDITED }
|
mode={DocumentInfoLineWithTimeMode.EDITED}
|
||||||
time={ DateTime.local()
|
time={DateTime.local().minus({ minutes: 3 })}
|
||||||
.minus({ minutes: 3 }) }
|
userName={'Philip'}
|
||||||
userName={ 'Philip' }
|
profileImageSrc={`${assetsBaseUrl}img/avatar.png`}
|
||||||
profileImageSrc={ `${ assetsBaseUrl }img/avatar.png` }/>
|
/>
|
||||||
</ListGroup.Item>
|
</ListGroup.Item>
|
||||||
<ListGroup.Item>
|
<ListGroup.Item>
|
||||||
<DocumentInfoLine icon={ 'users' } size={ '2x' }>
|
<DocumentInfoLine icon={'users'} size={'2x'}>
|
||||||
<Trans i18nKey='editor.modal.documentInfo.usersContributed'>
|
<Trans i18nKey='editor.modal.documentInfo.usersContributed'>
|
||||||
<UnitalicBoldText text={ '42' }/>
|
<UnitalicBoldText text={'42'} />
|
||||||
</Trans>
|
</Trans>
|
||||||
</DocumentInfoLine>
|
</DocumentInfoLine>
|
||||||
</ListGroup.Item>
|
</ListGroup.Item>
|
||||||
<ListGroup.Item>
|
<ListGroup.Item>
|
||||||
<DocumentInfoLine icon={ 'history' } size={ '2x' }>
|
<DocumentInfoLine icon={'history'} size={'2x'}>
|
||||||
<Trans i18nKey='editor.modal.documentInfo.revisions'>
|
<Trans i18nKey='editor.modal.documentInfo.revisions'>
|
||||||
<UnitalicBoldText text={ '192' }/>
|
<UnitalicBoldText text={'192'} />
|
||||||
</Trans>
|
</Trans>
|
||||||
</DocumentInfoLine>
|
</DocumentInfoLine>
|
||||||
</ListGroup.Item>
|
</ListGroup.Item>
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { TimeFromNow } from './time-from-now'
|
||||||
|
|
||||||
export interface DocumentInfoLineWithTimeProps {
|
export interface DocumentInfoLineWithTimeProps {
|
||||||
size?: '2x' | '3x' | '4x' | '5x' | undefined
|
size?: '2x' | '3x' | '4x' | '5x' | undefined
|
||||||
time: DateTime,
|
time: DateTime
|
||||||
mode: DocumentInfoLineWithTimeMode
|
mode: DocumentInfoLineWithTimeMode
|
||||||
userName: string
|
userName: string
|
||||||
profileImageSrc: string
|
profileImageSrc: string
|
||||||
|
@ -25,18 +25,31 @@ export enum DocumentInfoLineWithTimeMode {
|
||||||
EDITED
|
EDITED
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DocumentInfoTimeLine: React.FC<DocumentInfoLineWithTimeProps> = ({ time, mode, userName, profileImageSrc, size }) => {
|
export const DocumentInfoTimeLine: React.FC<DocumentInfoLineWithTimeProps> = ({
|
||||||
|
time,
|
||||||
|
mode,
|
||||||
|
userName,
|
||||||
|
profileImageSrc,
|
||||||
|
size
|
||||||
|
}) => {
|
||||||
useTranslation()
|
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'
|
const icon: IconName = mode === DocumentInfoLineWithTimeMode.CREATED ? 'plus' : 'pencil'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DocumentInfoLine icon={ icon } size={ size }>
|
<DocumentInfoLine icon={icon} size={size}>
|
||||||
<Trans i18nKey={ i18nKey }>
|
<Trans i18nKey={i18nKey}>
|
||||||
<UserAvatar photo={ profileImageSrc } additionalClasses={ 'font-style-normal bold font-weight-bold' }
|
<UserAvatar
|
||||||
name={ userName } size={ size ? 'lg' : undefined }/>
|
photo={profileImageSrc}
|
||||||
<TimeFromNow time={ time }/>
|
additionalClasses={'font-style-normal bold font-weight-bold'}
|
||||||
|
name={userName}
|
||||||
|
size={size ? 'lg' : undefined}
|
||||||
|
/>
|
||||||
|
<TimeFromNow time={time} />
|
||||||
</Trans>
|
</Trans>
|
||||||
</DocumentInfoLine>
|
</DocumentInfoLine>
|
||||||
)
|
)
|
||||||
|
|
|
@ -14,7 +14,8 @@ export interface TimeFromNowProps {
|
||||||
|
|
||||||
export const TimeFromNow: React.FC<TimeFromNowProps> = ({ time }) => {
|
export const TimeFromNow: React.FC<TimeFromNowProps> = ({ time }) => {
|
||||||
return (
|
return (
|
||||||
<time className={ 'mx-1' } title={ time.toFormat('DDDD T') }
|
<time className={'mx-1'} title={time.toFormat('DDDD T')} dateTime={time.toString()}>
|
||||||
dateTime={ time.toString() }>{ time.toRelative() }</time>
|
{time.toRelative()}
|
||||||
|
</time>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,9 +7,9 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
export interface UnitalicBoldTextProps {
|
export interface UnitalicBoldTextProps {
|
||||||
text: string;
|
text: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UnitalicBoldText: React.FC<UnitalicBoldTextProps> = ({ text }) => {
|
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 {
|
export enum GroupMode {
|
||||||
NONE,
|
NONE,
|
||||||
VIEW,
|
VIEW,
|
||||||
EDIT,
|
EDIT
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PermissionGroupEntry: React.FC<PermissionGroupEntryProps> = ({ title, editMode, onChangeEditMode }) => {
|
export const PermissionGroupEntry: React.FC<PermissionGroupEntryProps> = ({ title, editMode, onChangeEditMode }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className={ 'list-group-item d-flex flex-row justify-content-between align-items-center' }>
|
<li className={'list-group-item d-flex flex-row justify-content-between align-items-center'}>
|
||||||
<Trans i18nKey={ title }/>
|
<Trans i18nKey={title} />
|
||||||
<ToggleButtonGroup
|
<ToggleButtonGroup type='radio' name='edit-mode' value={editMode} onChange={onChangeEditMode}>
|
||||||
type='radio'
|
|
||||||
name='edit-mode'
|
|
||||||
value={ editMode }
|
|
||||||
onChange={ onChangeEditMode }
|
|
||||||
>
|
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
title={ t('editor.modal.permissions.denyGroup', { name: t(title) }) }
|
title={t('editor.modal.permissions.denyGroup', { name: t(title) })}
|
||||||
variant={ 'light' }
|
variant={'light'}
|
||||||
className={ 'text-secondary' }
|
className={'text-secondary'}
|
||||||
value={ GroupMode.NONE }
|
value={GroupMode.NONE}>
|
||||||
>
|
<ForkAwesomeIcon icon='ban' />
|
||||||
<ForkAwesomeIcon icon='ban'/>
|
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
title={ t('editor.modal.permissions.viewOnlyGroup', { name: t(title) }) }
|
title={t('editor.modal.permissions.viewOnlyGroup', { name: t(title) })}
|
||||||
variant={ 'light' }
|
variant={'light'}
|
||||||
className={ 'text-secondary' }
|
className={'text-secondary'}
|
||||||
value={ GroupMode.VIEW }
|
value={GroupMode.VIEW}>
|
||||||
>
|
<ForkAwesomeIcon icon='eye' />
|
||||||
<ForkAwesomeIcon icon='eye'/>
|
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
title={ t('editor.modal.permissions.editGroup', { name: t(title) }) }
|
title={t('editor.modal.permissions.editGroup', { name: t(title) })}
|
||||||
variant={ 'light' }
|
variant={'light'}
|
||||||
className={ 'text-secondary' }
|
className={'text-secondary'}
|
||||||
value={ GroupMode.EDIT }
|
value={GroupMode.EDIT}>
|
||||||
>
|
<ForkAwesomeIcon icon='pencil' />
|
||||||
<ForkAwesomeIcon icon='pencil'/>
|
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
</ToggleButtonGroup>
|
</ToggleButtonGroup>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -27,7 +27,17 @@ export enum EditMode {
|
||||||
EDIT
|
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 { t } = useTranslation()
|
||||||
const [newEntry, setNewEntry] = useState('')
|
const [newEntry, setNewEntry] = useState('')
|
||||||
|
|
||||||
|
@ -37,64 +47,56 @@ export const PermissionList: React.FC<PermissionListProps> = ({ list, identifier
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className={ 'list-group' }>
|
<ul className={'list-group'}>
|
||||||
{ list.map(entry => (
|
{list.map((entry) => (
|
||||||
<li key={ entry.id } className={ 'list-group-item d-flex flex-row justify-content-between align-items-center' }>
|
<li key={entry.id} className={'list-group-item d-flex flex-row justify-content-between align-items-center'}>
|
||||||
{ identifier(entry) }
|
{identifier(entry)}
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
variant='light'
|
variant='light'
|
||||||
className={ 'text-danger mr-2' }
|
className={'text-danger mr-2'}
|
||||||
title={ t(removeI18nKey, { name: entry.name }) }
|
title={t(removeI18nKey, { name: entry.name })}
|
||||||
onClick={ () => removeEntry(entry.id) }
|
onClick={() => removeEntry(entry.id)}>
|
||||||
>
|
<ForkAwesomeIcon icon={'times'} />
|
||||||
<ForkAwesomeIcon icon={ 'times' }/>
|
|
||||||
</Button>
|
</Button>
|
||||||
<ToggleButtonGroup
|
<ToggleButtonGroup
|
||||||
type='radio'
|
type='radio'
|
||||||
name='edit-mode'
|
name='edit-mode'
|
||||||
value={ entry.canEdit ? EditMode.EDIT : EditMode.VIEW }
|
value={entry.canEdit ? EditMode.EDIT : EditMode.VIEW}
|
||||||
onChange={ (value: EditMode) => changeEditMode(entry.id, value === EditMode.EDIT) }
|
onChange={(value: EditMode) => changeEditMode(entry.id, value === EditMode.EDIT)}>
|
||||||
>
|
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
title={ t(viewI18nKey, { name: entry.name }) }
|
title={t(viewI18nKey, { name: entry.name })}
|
||||||
variant={ 'light' }
|
variant={'light'}
|
||||||
className={ 'text-secondary' }
|
className={'text-secondary'}
|
||||||
value={ EditMode.VIEW }
|
value={EditMode.VIEW}>
|
||||||
>
|
<ForkAwesomeIcon icon='eye' />
|
||||||
<ForkAwesomeIcon icon='eye'/>
|
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
title={ t(editI18nKey, { name: entry.name }) }
|
title={t(editI18nKey, { name: entry.name })}
|
||||||
variant={ 'light' }
|
variant={'light'}
|
||||||
className={ 'text-secondary' }
|
className={'text-secondary'}
|
||||||
value={ EditMode.EDIT }
|
value={EditMode.EDIT}>
|
||||||
>
|
<ForkAwesomeIcon icon='pencil' />
|
||||||
<ForkAwesomeIcon icon='pencil'/>
|
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
</ToggleButtonGroup>
|
</ToggleButtonGroup>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
)) }
|
))}
|
||||||
<li className={ 'list-group-item' }>
|
<li className={'list-group-item'}>
|
||||||
<form onSubmit={ event => {
|
<form
|
||||||
event.preventDefault()
|
onSubmit={(event) => {
|
||||||
addEntry()
|
event.preventDefault()
|
||||||
} }>
|
addEntry()
|
||||||
<InputGroup className={ 'mr-1 mb-1' }>
|
}}>
|
||||||
|
<InputGroup className={'mr-1 mb-1'}>
|
||||||
<FormControl
|
<FormControl
|
||||||
value={ newEntry }
|
value={newEntry}
|
||||||
placeholder={ t(addI18nKey) }
|
placeholder={t(addI18nKey)}
|
||||||
aria-label={ t(addI18nKey) }
|
aria-label={t(addI18nKey)}
|
||||||
onChange={ event => setNewEntry(event.currentTarget.value) }
|
onChange={(event) => setNewEntry(event.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button variant='light' className={'text-secondary ml-2'} title={t(addI18nKey)} onClick={addEntry}>
|
||||||
variant='light'
|
<ForkAwesomeIcon icon={'plus'} />
|
||||||
className={ 'text-secondary ml-2' }
|
|
||||||
title={ t(addI18nKey) }
|
|
||||||
onClick={ addEntry }
|
|
||||||
>
|
|
||||||
<ForkAwesomeIcon icon={ 'plus' }/>
|
|
||||||
</Button>
|
</Button>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { GroupMode, PermissionGroupEntry } from './permission-group-entry'
|
||||||
import { PermissionList } from './permission-list'
|
import { PermissionList } from './permission-list'
|
||||||
|
|
||||||
export interface PermissionsModalProps {
|
export interface PermissionsModalProps {
|
||||||
show: boolean,
|
show: boolean
|
||||||
onHide: () => void
|
onHide: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ interface NotePermissions {
|
||||||
sharedTo: {
|
sharedTo: {
|
||||||
username: string
|
username: string
|
||||||
canEdit: boolean
|
canEdit: boolean
|
||||||
}[],
|
}[]
|
||||||
sharedToGroup: {
|
sharedToGroup: {
|
||||||
id: string
|
id: string
|
||||||
canEdit: boolean
|
canEdit: boolean
|
||||||
|
@ -43,20 +43,26 @@ export const EVERYONE_LOGGED_IN_GROUP_ID = '2'
|
||||||
|
|
||||||
const permissionsApiResponse: NotePermissions = {
|
const permissionsApiResponse: NotePermissions = {
|
||||||
owner: 'dermolly',
|
owner: 'dermolly',
|
||||||
sharedTo: [{
|
sharedTo: [
|
||||||
username: 'emcrx',
|
{
|
||||||
canEdit: true
|
username: 'emcrx',
|
||||||
}, {
|
canEdit: true
|
||||||
username: 'mrdrogdrog',
|
},
|
||||||
canEdit: false
|
{
|
||||||
}],
|
username: 'mrdrogdrog',
|
||||||
sharedToGroup: [{
|
canEdit: false
|
||||||
id: EVERYONE_GROUP_ID,
|
}
|
||||||
canEdit: true
|
],
|
||||||
}, {
|
sharedToGroup: [
|
||||||
id: EVERYONE_LOGGED_IN_GROUP_ID,
|
{
|
||||||
canEdit: false
|
id: EVERYONE_GROUP_ID,
|
||||||
}]
|
canEdit: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: EVERYONE_LOGGED_IN_GROUP_ID,
|
||||||
|
canEdit: false
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PermissionModal: React.FC<PermissionsModalProps> = ({ show, onHide }) => {
|
export const PermissionModal: React.FC<PermissionsModalProps> = ({ show, onHide }) => {
|
||||||
|
@ -70,7 +76,7 @@ export const PermissionModal: React.FC<PermissionsModalProps> = ({ show, onHide
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// set owner
|
// set owner
|
||||||
getUserById(permissionsApiResponse.owner)
|
getUserById(permissionsApiResponse.owner)
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
setOwner({
|
setOwner({
|
||||||
name: response.name,
|
name: response.name,
|
||||||
photo: response.photo
|
photo: response.photo
|
||||||
|
@ -78,20 +84,24 @@ export const PermissionModal: React.FC<PermissionsModalProps> = ({ show, onHide
|
||||||
})
|
})
|
||||||
.catch(() => setError(true))
|
.catch(() => setError(true))
|
||||||
// set user List
|
// set user List
|
||||||
permissionsApiResponse.sharedTo.forEach(shareUser => {
|
permissionsApiResponse.sharedTo.forEach((shareUser) => {
|
||||||
getUserById(shareUser.username)
|
getUserById(shareUser.username)
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
setUserList(list => list.concat([{
|
setUserList((list) =>
|
||||||
id: response.id,
|
list.concat([
|
||||||
name: response.name,
|
{
|
||||||
photo: response.photo,
|
id: response.id,
|
||||||
canEdit: shareUser.canEdit
|
name: response.name,
|
||||||
}]))
|
photo: response.photo,
|
||||||
|
canEdit: shareUser.canEdit
|
||||||
|
}
|
||||||
|
])
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.catch(() => setError(true))
|
.catch(() => setError(true))
|
||||||
})
|
})
|
||||||
// set group List
|
// set group List
|
||||||
permissionsApiResponse.sharedToGroup.forEach(sharedGroup => {
|
permissionsApiResponse.sharedToGroup.forEach((sharedGroup) => {
|
||||||
if (sharedGroup.id === EVERYONE_GROUP_ID) {
|
if (sharedGroup.id === EVERYONE_GROUP_ID) {
|
||||||
setAllUserPermissions(sharedGroup.canEdit ? GroupMode.EDIT : GroupMode.VIEW)
|
setAllUserPermissions(sharedGroup.canEdit ? GroupMode.EDIT : GroupMode.VIEW)
|
||||||
} else if (sharedGroup.id === EVERYONE_LOGGED_IN_GROUP_ID) {
|
} 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']) => {
|
const changeUserMode = (userId: Principal['id'], canEdit: Principal['canEdit']) => {
|
||||||
setUserList(list =>
|
setUserList((list) =>
|
||||||
list
|
list.map((user) => {
|
||||||
.map(user => {
|
if (user.id === userId) {
|
||||||
if (user.id === userId) {
|
user.canEdit = canEdit
|
||||||
user.canEdit = canEdit
|
}
|
||||||
}
|
return user
|
||||||
return user
|
})
|
||||||
}))
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeUser = (userId: Principal['id']) => {
|
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']) => {
|
const addUser = (name: Principal['name']) => {
|
||||||
setUserList(list => list.concat({
|
setUserList((list) =>
|
||||||
id: name,
|
list.concat({
|
||||||
photo: '/img/avatar.png',
|
id: name,
|
||||||
name: name,
|
photo: '/img/avatar.png',
|
||||||
canEdit: false
|
name: name,
|
||||||
}))
|
canEdit: false
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommonModal
|
<CommonModal show={show} onHide={onHide} closeButton={true} titleI18nKey={'editor.modal.permissions.title'}>
|
||||||
show={ show }
|
|
||||||
onHide={ onHide }
|
|
||||||
closeButton={ true }
|
|
||||||
titleI18nKey={ 'editor.modal.permissions.title' }>
|
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<h5 className={ 'mb-3' }><Trans i18nKey={ 'editor.modal.permissions.owner' }/></h5>
|
<h5 className={'mb-3'}>
|
||||||
<ShowIf condition={ error }>
|
<Trans i18nKey={'editor.modal.permissions.owner'} />
|
||||||
|
</h5>
|
||||||
|
<ShowIf condition={error}>
|
||||||
<Alert variant='danger'>
|
<Alert variant='danger'>
|
||||||
<Trans i18nKey='editor.modal.permissions.error'/>
|
<Trans i18nKey='editor.modal.permissions.error' />
|
||||||
</Alert>
|
</Alert>
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
<ul className={ 'list-group' }>
|
<ul className={'list-group'}>
|
||||||
<li className={ 'list-group-item d-flex flex-row align-items-center' }>
|
<li className={'list-group-item d-flex flex-row align-items-center'}>
|
||||||
<UserAvatar name={ owner?.name ?? '' } photo={ owner?.photo ?? '' }/>
|
<UserAvatar name={owner?.name ?? ''} photo={owner?.photo ?? ''} />
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h5 className={ 'my-3' }><Trans i18nKey={ 'editor.modal.permissions.sharedWithUsers' }/></h5>
|
<h5 className={'my-3'}>
|
||||||
|
<Trans i18nKey={'editor.modal.permissions.sharedWithUsers'} />
|
||||||
|
</h5>
|
||||||
<PermissionList
|
<PermissionList
|
||||||
list={ userList }
|
list={userList}
|
||||||
identifier={ entry => (<UserAvatar name={ entry.name } photo={ entry.photo }/>) }
|
identifier={(entry) => <UserAvatar name={entry.name} photo={entry.photo} />}
|
||||||
changeEditMode={ changeUserMode }
|
changeEditMode={changeUserMode}
|
||||||
removeEntry={ removeUser }
|
removeEntry={removeUser}
|
||||||
createEntry={ addUser }
|
createEntry={addUser}
|
||||||
editI18nKey={ 'editor.modal.permissions.editUser' }
|
editI18nKey={'editor.modal.permissions.editUser'}
|
||||||
viewI18nKey={ 'editor.modal.permissions.viewOnlyUser' }
|
viewI18nKey={'editor.modal.permissions.viewOnlyUser'}
|
||||||
removeI18nKey={ 'editor.modal.permissions.removeUser' }
|
removeI18nKey={'editor.modal.permissions.removeUser'}
|
||||||
addI18nKey={ 'editor.modal.permissions.addUser' }
|
addI18nKey={'editor.modal.permissions.addUser'}
|
||||||
/>
|
/>
|
||||||
<h5 className={ 'my-3' }><Trans i18nKey={ 'editor.modal.permissions.sharedWithGroups' }/></h5>
|
<h5 className={'my-3'}>
|
||||||
<ul className={ 'list-group' }>
|
<Trans i18nKey={'editor.modal.permissions.sharedWithGroups'} />
|
||||||
|
</h5>
|
||||||
|
<ul className={'list-group'}>
|
||||||
<PermissionGroupEntry
|
<PermissionGroupEntry
|
||||||
title={ 'editor.modal.permissions.allUser' }
|
title={'editor.modal.permissions.allUser'}
|
||||||
editMode={ allUserPermissions }
|
editMode={allUserPermissions}
|
||||||
onChangeEditMode={ setAllUserPermissions }
|
onChangeEditMode={setAllUserPermissions}
|
||||||
/>
|
/>
|
||||||
<PermissionGroupEntry
|
<PermissionGroupEntry
|
||||||
title={ 'editor.modal.permissions.allLoggedInUser' }
|
title={'editor.modal.permissions.allLoggedInUser'}
|
||||||
editMode={ allLoggedInUserPermissions }
|
editMode={allLoggedInUserPermissions}
|
||||||
onChangeEditMode={ setAllLoggedInUserPermissions }
|
onChangeEditMode={setAllLoggedInUserPermissions}
|
||||||
/>
|
/>
|
||||||
</ul>
|
</ul>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
|
|
|
@ -20,33 +20,32 @@ export interface RevisionModalListEntryProps {
|
||||||
revisionAuthorListMap: Map<number, UserResponse[]>
|
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
|
<ListGroup.Item
|
||||||
as='li'
|
as='li'
|
||||||
active={ active }
|
active={active}
|
||||||
onClick={ onClick }
|
onClick={onClick}
|
||||||
className='user-select-none revision-item d-flex flex-column'
|
className='user-select-none revision-item d-flex flex-column'>
|
||||||
>
|
|
||||||
<span>
|
<span>
|
||||||
<ForkAwesomeIcon icon={ 'clock-o' } className='mx-2'/>
|
<ForkAwesomeIcon icon={'clock-o'} className='mx-2' />
|
||||||
{ DateTime.fromMillis(revision.timestamp * 1000)
|
{DateTime.fromMillis(revision.timestamp * 1000).toFormat('DDDD T')}
|
||||||
.toFormat('DDDD T') }
|
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<ForkAwesomeIcon icon={ 'file-text-o' } className='mx-2'/>
|
<ForkAwesomeIcon icon={'file-text-o'} className='mx-2' />
|
||||||
<Trans i18nKey={ 'editor.modal.revision.length' }/>: { revision.length }
|
<Trans i18nKey={'editor.modal.revision.length'} />: {revision.length}
|
||||||
</span>
|
</span>
|
||||||
<span className={ 'd-flex flex-row my-1 align-items-center' }>
|
<span className={'d-flex flex-row my-1 align-items-center'}>
|
||||||
<ForkAwesomeIcon icon={ 'user-o' } className={ 'mx-2' }/>
|
<ForkAwesomeIcon icon={'user-o'} className={'mx-2'} />
|
||||||
{
|
{revisionAuthorListMap.get(revision.timestamp)?.map((user, index) => {
|
||||||
revisionAuthorListMap.get(revision.timestamp)
|
return (
|
||||||
?.map((user, index) => {
|
<UserAvatar name={user.name} photo={user.photo} showName={false} additionalClasses={'mx-1'} key={index} />
|
||||||
return (
|
)
|
||||||
<UserAvatar name={ user.name } photo={ user.photo } showName={ false }
|
})}
|
||||||
additionalClasses={ 'mx-1' } key={ index }/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</span>
|
</span>
|
||||||
</ListGroup.Item>
|
</ListGroup.Item>
|
||||||
)
|
)
|
||||||
|
|
|
@ -21,7 +21,7 @@ import './revision-modal.scss'
|
||||||
import { downloadRevision, getUserDataForRevision } from './utils'
|
import { downloadRevision, getUserDataForRevision } from './utils'
|
||||||
|
|
||||||
export interface PermissionsModalProps {
|
export interface PermissionsModalProps {
|
||||||
show: boolean,
|
show: boolean
|
||||||
onHide: () => void
|
onHide: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,8 +37,8 @@ export const RevisionModal: React.FC<PermissionsModalProps> = ({ show, onHide })
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getAllRevisions(id)
|
getAllRevisions(id)
|
||||||
.then(fetchedRevisions => {
|
.then((fetchedRevisions) => {
|
||||||
fetchedRevisions.forEach(revision => {
|
fetchedRevisions.forEach((revision) => {
|
||||||
const authorData = getUserDataForRevision(revision.authors)
|
const authorData = getUserDataForRevision(revision.authors)
|
||||||
revisionAuthorListMap.current.set(revision.timestamp, authorData)
|
revisionAuthorListMap.current.set(revision.timestamp, authorData)
|
||||||
})
|
})
|
||||||
|
@ -55,7 +55,7 @@ export const RevisionModal: React.FC<PermissionsModalProps> = ({ show, onHide })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
getRevision(id, selectedRevisionTimestamp)
|
getRevision(id, selectedRevisionTimestamp)
|
||||||
.then(fetchedRevision => {
|
.then((fetchedRevision) => {
|
||||||
setSelectedRevision(fetchedRevision)
|
setSelectedRevision(fetchedRevision)
|
||||||
})
|
})
|
||||||
.catch(() => setError(true))
|
.catch(() => setError(true))
|
||||||
|
@ -64,60 +64,62 @@ export const RevisionModal: React.FC<PermissionsModalProps> = ({ show, onHide })
|
||||||
const markdownContent = useNoteMarkdownContent()
|
const markdownContent = useNoteMarkdownContent()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommonModal show={ show } onHide={ onHide } titleI18nKey={ 'editor.modal.revision.title' } icon={ 'history' }
|
<CommonModal
|
||||||
closeButton={ true } size={ 'xl' } additionalClasses='revision-modal'>
|
show={show}
|
||||||
|
onHide={onHide}
|
||||||
|
titleI18nKey={'editor.modal.revision.title'}
|
||||||
|
icon={'history'}
|
||||||
|
closeButton={true}
|
||||||
|
size={'xl'}
|
||||||
|
additionalClasses='revision-modal'>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<Row>
|
<Row>
|
||||||
<Col lg={ 4 } className={ 'scroll-col' }>
|
<Col lg={4} className={'scroll-col'}>
|
||||||
<ListGroup as='ul'>
|
<ListGroup as='ul'>
|
||||||
{
|
{revisions.map((revision, revisionIndex) => (
|
||||||
revisions.map((revision, revisionIndex) => (
|
<RevisionModalListEntry
|
||||||
<RevisionModalListEntry
|
key={revisionIndex}
|
||||||
key={ revisionIndex }
|
active={selectedRevisionTimestamp === revision.timestamp}
|
||||||
active={ selectedRevisionTimestamp === revision.timestamp }
|
revision={revision}
|
||||||
revision={ revision }
|
revisionAuthorListMap={revisionAuthorListMap.current}
|
||||||
revisionAuthorListMap={ revisionAuthorListMap.current }
|
onClick={() => setSelectedRevisionTimestamp(revision.timestamp)}
|
||||||
onClick={ () => setSelectedRevisionTimestamp(revision.timestamp) }
|
/>
|
||||||
/>
|
))}
|
||||||
))
|
|
||||||
}
|
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
</Col>
|
</Col>
|
||||||
<Col lg={ 8 } className={ 'scroll-col' }>
|
<Col lg={8} className={'scroll-col'}>
|
||||||
<ShowIf condition={ error }>
|
<ShowIf condition={error}>
|
||||||
<Alert variant='danger'>
|
<Alert variant='danger'>
|
||||||
<Trans i18nKey='editor.modal.revision.error'/>
|
<Trans i18nKey='editor.modal.revision.error' />
|
||||||
</Alert>
|
</Alert>
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
<ShowIf condition={ !error && !!selectedRevision }>
|
<ShowIf condition={!error && !!selectedRevision}>
|
||||||
<ReactDiffViewer
|
<ReactDiffViewer
|
||||||
oldValue={ selectedRevision?.content }
|
oldValue={selectedRevision?.content}
|
||||||
newValue={ markdownContent }
|
newValue={markdownContent}
|
||||||
splitView={ false }
|
splitView={false}
|
||||||
compareMethod={ DiffMethod.WORDS }
|
compareMethod={DiffMethod.WORDS}
|
||||||
useDarkTheme={ darkModeEnabled }
|
useDarkTheme={darkModeEnabled}
|
||||||
/>
|
/>
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
<Button
|
<Button variant='secondary' onClick={onHide}>
|
||||||
variant='secondary'
|
<Trans i18nKey={'common.close'} />
|
||||||
onClick={ onHide }>
|
|
||||||
<Trans i18nKey={ 'common.close' }/>
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant='danger'
|
variant='danger'
|
||||||
disabled={ !selectedRevisionTimestamp }
|
disabled={!selectedRevisionTimestamp}
|
||||||
onClick={ () => window.alert('Not yet implemented. Requires websocket.') }>
|
onClick={() => window.alert('Not yet implemented. Requires websocket.')}>
|
||||||
<Trans i18nKey={ 'editor.modal.revision.revertButton' }/>
|
<Trans i18nKey={'editor.modal.revision.revertButton'} />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant='primary'
|
variant='primary'
|
||||||
disabled={ !selectedRevisionTimestamp }
|
disabled={!selectedRevisionTimestamp}
|
||||||
onClick={ () => downloadRevision(id, selectedRevision) }>
|
onClick={() => downloadRevision(id, selectedRevision)}>
|
||||||
<Trans i18nKey={ 'editor.modal.revision.download' }/>
|
<Trans i18nKey={'editor.modal.revision.download'} />
|
||||||
</Button>
|
</Button>
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</CommonModal>
|
</CommonModal>
|
||||||
|
|
|
@ -13,7 +13,7 @@ export const downloadRevision = (noteId: string, revision: Revision | null): voi
|
||||||
if (!revision) {
|
if (!revision) {
|
||||||
return
|
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[] => {
|
export const getUserDataForRevision = (authors: string[]): UserResponse[] => {
|
||||||
|
@ -23,7 +23,7 @@ export const getUserDataForRevision = (authors: string[]): UserResponse[] => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
getUserById(author)
|
getUserById(author)
|
||||||
.then(userData => {
|
.then((userData) => {
|
||||||
users.push(userData)
|
users.push(userData)
|
||||||
})
|
})
|
||||||
.catch((error) => console.error(error))
|
.catch((error) => console.error(error))
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { EditorPagePathParams } from '../../editor-page'
|
||||||
import { NoteType } from '../../note-frontmatter/note-frontmatter'
|
import { NoteType } from '../../note-frontmatter/note-frontmatter'
|
||||||
|
|
||||||
export interface ShareModalProps {
|
export interface ShareModalProps {
|
||||||
show: boolean,
|
show: boolean
|
||||||
onHide: () => void
|
onHide: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,24 +31,21 @@ export const ShareModal: React.FC<ShareModalProps> = ({ show, onHide }) => {
|
||||||
const { id } = useParams<EditorPagePathParams>()
|
const { id } = useParams<EditorPagePathParams>()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommonModal
|
<CommonModal show={show} onHide={onHide} closeButton={true} titleI18nKey={'editor.modal.shareLink.title'}>
|
||||||
show={ show }
|
|
||||||
onHide={ onHide }
|
|
||||||
closeButton={ true }
|
|
||||||
titleI18nKey={ 'editor.modal.shareLink.title' }>
|
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<Trans i18nKey={ 'editor.modal.shareLink.editorDescription' }/>
|
<Trans i18nKey={'editor.modal.shareLink.editorDescription'} />
|
||||||
<CopyableField content={ `${ baseUrl }n/${ id }?${ editorMode }` } nativeShareButton={ true }
|
<CopyableField
|
||||||
url={ `${ baseUrl }n/${ id }?${ editorMode }` }/>
|
content={`${baseUrl}n/${id}?${editorMode}`}
|
||||||
<ShowIf condition={ noteFrontmatter.type === NoteType.SLIDE }>
|
nativeShareButton={true}
|
||||||
<Trans i18nKey={ 'editor.modal.shareLink.slidesDescription' }/>
|
url={`${baseUrl}n/${id}?${editorMode}`}
|
||||||
<CopyableField content={ `${ baseUrl }p/${ id }` } nativeShareButton={ true }
|
/>
|
||||||
url={ `${ baseUrl }p/${ id }` }/>
|
<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>
|
||||||
<ShowIf condition={ noteFrontmatter.type === '' }>
|
<ShowIf condition={noteFrontmatter.type === ''}>
|
||||||
<Trans i18nKey={ 'editor.modal.shareLink.viewOnlyDescription' }/>
|
<Trans i18nKey={'editor.modal.shareLink.viewOnlyDescription'} />
|
||||||
<CopyableField content={ `${ baseUrl }s/${ id }` } nativeShareButton={ true }
|
<CopyableField content={`${baseUrl}s/${id}`} nativeShareButton={true} url={`${baseUrl}s/${id}`} />
|
||||||
url={ `${ baseUrl }s/${ id }` }/>
|
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
</CommonModal>
|
</CommonModal>
|
||||||
|
|
|
@ -7,19 +7,31 @@
|
||||||
import { RefObject, useCallback } from 'react'
|
import { RefObject, useCallback } from 'react'
|
||||||
import { LineMarkerPosition } from '../../../markdown-renderer/types'
|
import { LineMarkerPosition } from '../../../markdown-renderer/types'
|
||||||
|
|
||||||
export const useAdaptedLineMarkerCallback = (documentRenderPaneRef: RefObject<HTMLDivElement> | undefined,
|
export const useAdaptedLineMarkerCallback = (
|
||||||
|
documentRenderPaneRef: RefObject<HTMLDivElement> | undefined,
|
||||||
rendererRef: RefObject<HTMLDivElement>,
|
rendererRef: RefObject<HTMLDivElement>,
|
||||||
onLineMarkerPositionChanged: ((lineMarkerPosition: LineMarkerPosition[]) => void) | undefined): ((lineMarkerPosition: LineMarkerPosition[]) => void) => {
|
onLineMarkerPositionChanged: ((lineMarkerPosition: LineMarkerPosition[]) => void) | undefined
|
||||||
return useCallback((linkMarkerPositions) => {
|
): ((lineMarkerPosition: LineMarkerPosition[]) => void) => {
|
||||||
if (!onLineMarkerPositionChanged || !documentRenderPaneRef || !documentRenderPaneRef.current || !rendererRef.current) {
|
return useCallback(
|
||||||
return
|
(linkMarkerPositions) => {
|
||||||
}
|
if (
|
||||||
const documentRenderPaneTop = (documentRenderPaneRef.current.offsetTop ?? 0)
|
!onLineMarkerPositionChanged ||
|
||||||
const rendererTop = (rendererRef.current.offsetTop ?? 0)
|
!documentRenderPaneRef ||
|
||||||
const offset = rendererTop - documentRenderPaneTop
|
!documentRenderPaneRef.current ||
|
||||||
onLineMarkerPositionChanged(linkMarkerPositions.map(oldMarker => ({
|
!rendererRef.current
|
||||||
line: oldMarker.line,
|
) {
|
||||||
position: oldMarker.position + offset
|
return
|
||||||
})))
|
}
|
||||||
}, [documentRenderPaneRef, onLineMarkerPositionChanged, rendererRef])
|
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 { RefObject, useCallback, useRef } from 'react'
|
||||||
import { IframeEditorToRendererCommunicator } from '../../../render-page/iframe-editor-to-renderer-communicator'
|
import { IframeEditorToRendererCommunicator } from '../../../render-page/iframe-editor-to-renderer-communicator'
|
||||||
|
|
||||||
export const useOnIframeLoad = (frameReference: RefObject<HTMLIFrameElement>, iframeCommunicator: IframeEditorToRendererCommunicator,
|
export const useOnIframeLoad = (
|
||||||
rendererOrigin: string, renderPageUrl: string, onNavigateAway: () => void): () => void => {
|
frameReference: RefObject<HTMLIFrameElement>,
|
||||||
|
iframeCommunicator: IframeEditorToRendererCommunicator,
|
||||||
|
rendererOrigin: string,
|
||||||
|
renderPageUrl: string,
|
||||||
|
onNavigateAway: () => void
|
||||||
|
): (() => void) => {
|
||||||
const sendToRenderPage = useRef<boolean>(true)
|
const sendToRenderPage = useRef<boolean>(true)
|
||||||
|
|
||||||
return useCallback(() => {
|
return useCallback(() => {
|
||||||
|
|
|
@ -26,7 +26,12 @@ export const ShowOnPropChangeImageLightbox: React.FC<ShowOnPropChangeImageLightb
|
||||||
}, [details])
|
}, [details])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ImageLightboxModal show={ show } onHide={ hideLightbox } src={ details?.src }
|
<ImageLightboxModal
|
||||||
alt={ details?.alt } title={ details?.title }/>
|
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()
|
useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommonModal data-cy={ 'limitReachedModal' } show={ show } onHide={ onHide }
|
<CommonModal
|
||||||
titleI18nKey={ 'editor.error.limitReached.title' } closeButton={ true }>
|
data-cy={'limitReachedModal'}
|
||||||
|
show={show}
|
||||||
|
onHide={onHide}
|
||||||
|
titleI18nKey={'editor.error.limitReached.title'}
|
||||||
|
closeButton={true}>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<Trans i18nKey={ 'editor.error.limitReached.description' } values={ { maxLength } }/>
|
<Trans i18nKey={'editor.error.limitReached.description'} values={{ maxLength }} />
|
||||||
<strong className='mt-2 d-block'><Trans i18nKey={ 'editor.error.limitReached.advice' }/></strong>
|
<strong className='mt-2 d-block'>
|
||||||
|
<Trans i18nKey={'editor.error.limitReached.advice'} />
|
||||||
|
</strong>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
<Button onClick={ onHide }><Trans i18nKey={ 'common.close' }/></Button>
|
<Button onClick={onHide}>
|
||||||
|
<Trans i18nKey={'common.close'} />
|
||||||
|
</Button>
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</CommonModal>
|
</CommonModal>
|
||||||
)
|
)
|
||||||
|
|
|
@ -59,17 +59,23 @@ export const EditorPage: React.FC = () => {
|
||||||
rendererScrollState: { firstLineInView: 1, scrolledPercentage: 0 }
|
rendererScrollState: { firstLineInView: 1, scrolledPercentage: 0 }
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const onMarkdownRendererScroll = useCallback((newScrollState: ScrollState) => {
|
const onMarkdownRendererScroll = useCallback(
|
||||||
if (scrollSource.current === ScrollSource.RENDERER && editorSyncScroll) {
|
(newScrollState: ScrollState) => {
|
||||||
setScrollState((old) => ({ editorScrollState: newScrollState, rendererScrollState: old.rendererScrollState }))
|
if (scrollSource.current === ScrollSource.RENDERER && editorSyncScroll) {
|
||||||
}
|
setScrollState((old) => ({ editorScrollState: newScrollState, rendererScrollState: old.rendererScrollState }))
|
||||||
}, [editorSyncScroll])
|
}
|
||||||
|
},
|
||||||
|
[editorSyncScroll]
|
||||||
|
)
|
||||||
|
|
||||||
const onEditorScroll = useCallback((newScrollState: ScrollState) => {
|
const onEditorScroll = useCallback(
|
||||||
if (scrollSource.current === ScrollSource.EDITOR && editorSyncScroll) {
|
(newScrollState: ScrollState) => {
|
||||||
setScrollState((old) => ({ rendererScrollState: newScrollState, editorScrollState: old.editorScrollState }))
|
if (scrollSource.current === ScrollSource.EDITOR && editorSyncScroll) {
|
||||||
}
|
setScrollState((old) => ({ rendererScrollState: newScrollState, editorScrollState: old.editorScrollState }))
|
||||||
}, [editorSyncScroll])
|
}
|
||||||
|
},
|
||||||
|
[editorSyncScroll]
|
||||||
|
)
|
||||||
|
|
||||||
useViewModeShortcuts()
|
useViewModeShortcuts()
|
||||||
useApplyDarkMode()
|
useApplyDarkMode()
|
||||||
|
@ -90,48 +96,56 @@ export const EditorPage: React.FC = () => {
|
||||||
|
|
||||||
useNotificationTest()
|
useNotificationTest()
|
||||||
|
|
||||||
const leftPane = useMemo(() =>
|
const leftPane = useMemo(
|
||||||
|
() => (
|
||||||
<EditorPane
|
<EditorPane
|
||||||
onContentChange={ setNoteMarkdownContent }
|
onContentChange={setNoteMarkdownContent}
|
||||||
content={ markdownContent }
|
content={markdownContent}
|
||||||
scrollState={ scrollState.editorScrollState }
|
scrollState={scrollState.editorScrollState}
|
||||||
onScroll={ onEditorScroll }
|
onScroll={onEditorScroll}
|
||||||
onMakeScrollSource={ setEditorToScrollSource }/>
|
onMakeScrollSource={setEditorToScrollSource}
|
||||||
, [markdownContent, onEditorScroll, scrollState.editorScrollState, setEditorToScrollSource])
|
/>
|
||||||
|
),
|
||||||
|
[markdownContent, onEditorScroll, scrollState.editorScrollState, setEditorToScrollSource]
|
||||||
|
)
|
||||||
|
|
||||||
const rightPane = useMemo(() =>
|
const rightPane = useMemo(
|
||||||
|
() => (
|
||||||
<RenderIframe
|
<RenderIframe
|
||||||
frameClasses={ 'h-100 w-100' }
|
frameClasses={'h-100 w-100'}
|
||||||
markdownContent={ markdownContent }
|
markdownContent={markdownContent}
|
||||||
onMakeScrollSource={ setRendererToScrollSource }
|
onMakeScrollSource={setRendererToScrollSource}
|
||||||
onFirstHeadingChange={ updateNoteTitleByFirstHeading }
|
onFirstHeadingChange={updateNoteTitleByFirstHeading}
|
||||||
onTaskCheckedChange={ SetCheckboxInMarkdownContent }
|
onTaskCheckedChange={SetCheckboxInMarkdownContent}
|
||||||
onFrontmatterChange={ setNoteFrontmatter }
|
onFrontmatterChange={setNoteFrontmatter}
|
||||||
onScroll={ onMarkdownRendererScroll }
|
onScroll={onMarkdownRendererScroll}
|
||||||
scrollState={ scrollState.rendererScrollState }
|
scrollState={scrollState.rendererScrollState}
|
||||||
rendererType={ RendererType.DOCUMENT }/>
|
rendererType={RendererType.DOCUMENT}
|
||||||
, [markdownContent, onMarkdownRendererScroll, scrollState.rendererScrollState,
|
/>
|
||||||
setRendererToScrollSource])
|
),
|
||||||
|
[markdownContent, onMarkdownRendererScroll, scrollState.rendererScrollState, setRendererToScrollSource]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IframeCommunicatorContextProvider>
|
<IframeCommunicatorContextProvider>
|
||||||
<UiNotifications/>
|
<UiNotifications />
|
||||||
<MotdBanner/>
|
<MotdBanner />
|
||||||
<div className={ 'd-flex flex-column vh-100' }>
|
<div className={'d-flex flex-column vh-100'}>
|
||||||
<AppBar mode={ AppBarMode.EDITOR }/>
|
<AppBar mode={AppBarMode.EDITOR} />
|
||||||
<div className={ 'container' }>
|
<div className={'container'}>
|
||||||
<ErrorWhileLoadingNoteAlert show={ error }/>
|
<ErrorWhileLoadingNoteAlert show={error} />
|
||||||
<LoadingNoteAlert show={ loading }/>
|
<LoadingNoteAlert show={loading} />
|
||||||
</div>
|
</div>
|
||||||
<ShowIf condition={ !error && !loading }>
|
<ShowIf condition={!error && !loading}>
|
||||||
<div className={ 'flex-fill d-flex h-100 w-100 overflow-hidden flex-row' }>
|
<div className={'flex-fill d-flex h-100 w-100 overflow-hidden flex-row'}>
|
||||||
<Splitter
|
<Splitter
|
||||||
showLeft={ editorMode === EditorMode.EDITOR || editorMode === EditorMode.BOTH }
|
showLeft={editorMode === EditorMode.EDITOR || editorMode === EditorMode.BOTH}
|
||||||
left={ leftPane }
|
left={leftPane}
|
||||||
showRight={ editorMode === EditorMode.PREVIEW || editorMode === EditorMode.BOTH }
|
showRight={editorMode === EditorMode.PREVIEW || editorMode === EditorMode.BOTH}
|
||||||
right={ rightPane }
|
right={rightPane}
|
||||||
containerClassName={ 'overflow-hidden' }/>
|
containerClassName={'overflow-hidden'}
|
||||||
<Sidebar/>
|
/>
|
||||||
|
<Sidebar />
|
||||||
</div>
|
</div>
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -11,34 +11,39 @@ const wordRegExp = /^```((\w|-|_|\+)*)$/
|
||||||
let allSupportedLanguages: string[] = []
|
let allSupportedLanguages: string[] = []
|
||||||
|
|
||||||
const codeBlockHint = (editor: Editor): Promise<Hints | null> => {
|
const codeBlockHint = (editor: Editor): Promise<Hints | null> => {
|
||||||
return import(/* webpackChunkName: "highlight.js" */ '../../../common/hljs/hljs').then((hljs) =>
|
return import(/* webpackChunkName: "highlight.js" */ '../../../common/hljs/hljs').then(
|
||||||
new Promise((resolve) => {
|
(hljs) =>
|
||||||
const searchTerm = findWordAtCursor(editor)
|
new Promise((resolve) => {
|
||||||
const searchResult = wordRegExp.exec(searchTerm.text)
|
const searchTerm = findWordAtCursor(editor)
|
||||||
if (searchResult === null) {
|
const searchResult = wordRegExp.exec(searchTerm.text)
|
||||||
resolve(null)
|
if (searchResult === null) {
|
||||||
return
|
resolve(null)
|
||||||
}
|
return
|
||||||
const term = searchResult[1]
|
}
|
||||||
if (allSupportedLanguages.length === 0) {
|
const term = searchResult[1]
|
||||||
allSupportedLanguages = hljs.default.listLanguages()
|
if (allSupportedLanguages.length === 0) {
|
||||||
.concat('csv', 'flow', 'html', 'js', 'markmap', 'abc', 'graphviz', 'mermaid', 'vega-lite')
|
allSupportedLanguages = hljs.default
|
||||||
}
|
.listLanguages()
|
||||||
const suggestions = search(term, allSupportedLanguages)
|
.concat('csv', 'flow', 'html', 'js', 'markmap', 'abc', 'graphviz', 'mermaid', 'vega-lite')
|
||||||
const cursor = editor.getCursor()
|
}
|
||||||
if (!suggestions) {
|
const suggestions = search(term, allSupportedLanguages)
|
||||||
resolve(null)
|
const cursor = editor.getCursor()
|
||||||
} else {
|
if (!suggestions) {
|
||||||
resolve({
|
resolve(null)
|
||||||
list: suggestions.map((suggestion: string): Hint => ({
|
} else {
|
||||||
text: '```' + suggestion + '\n\n```\n',
|
resolve({
|
||||||
displayText: suggestion
|
list: suggestions.map(
|
||||||
})),
|
(suggestion: string): Hint => ({
|
||||||
from: Pos(cursor.line, searchTerm.start),
|
text: '```' + suggestion + '\n\n```\n',
|
||||||
to: Pos(cursor.line, searchTerm.end)
|
displayText: suggestion
|
||||||
})
|
})
|
||||||
}
|
),
|
||||||
}))
|
from: Pos(cursor.line, searchTerm.start),
|
||||||
|
to: Pos(cursor.line, searchTerm.end)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CodeBlockHinter: Hinter = {
|
export const CodeBlockHinter: Hinter = {
|
||||||
|
|
|
@ -23,9 +23,11 @@ const collapsableBlockHint = (editor: Editor): Promise<Hints | null> => {
|
||||||
resolve(null)
|
resolve(null)
|
||||||
} else {
|
} else {
|
||||||
resolve({
|
resolve({
|
||||||
list: suggestions.map((suggestion: string): Hint => ({
|
list: suggestions.map(
|
||||||
text: suggestion
|
(suggestion: string): Hint => ({
|
||||||
})),
|
text: suggestion
|
||||||
|
})
|
||||||
|
),
|
||||||
from: Pos(cursor.line, searchTerm.start),
|
from: Pos(cursor.line, searchTerm.start),
|
||||||
to: Pos(cursor.line, searchTerm.end + 1)
|
to: Pos(cursor.line, searchTerm.end + 1)
|
||||||
})
|
})
|
||||||
|
|
|
@ -13,11 +13,14 @@ const spoilerSuggestion: Hint = {
|
||||||
text: ':::spoiler Toggle label\nToggled content\n::: \n',
|
text: ':::spoiler Toggle label\nToggled content\n::: \n',
|
||||||
displayText: 'spoiler'
|
displayText: 'spoiler'
|
||||||
}
|
}
|
||||||
const suggestions = validAlertLevels.map((suggestion: string): Hint => ({
|
const suggestions = validAlertLevels
|
||||||
text: ':::' + suggestion + '\n\n::: \n',
|
.map(
|
||||||
displayText: suggestion
|
(suggestion: string): Hint => ({
|
||||||
}))
|
text: ':::' + suggestion + '\n\n::: \n',
|
||||||
.concat(spoilerSuggestion)
|
displayText: suggestion
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.concat(spoilerSuggestion)
|
||||||
|
|
||||||
const containerHint = (editor: Editor): Promise<Hints | null> => {
|
const containerHint = (editor: Editor): Promise<Hints | null> => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
|
|
@ -40,7 +40,7 @@ const convertEmojiEventToHint = (emojiData: EmojiClickEventDetail): Hint | undef
|
||||||
text: shortCode,
|
text: shortCode,
|
||||||
render: (parent: HTMLLIElement) => {
|
render: (parent: HTMLLIElement) => {
|
||||||
const wrapper = document.createElement('div')
|
const wrapper = document.createElement('div')
|
||||||
wrapper.innerHTML = `${ getEmojiIcon(emojiData) } ${ shortCode }`
|
wrapper.innerHTML = `${getEmojiIcon(emojiData)} ${shortCode}`
|
||||||
parent.appendChild(wrapper)
|
parent.appendChild(wrapper)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,17 +56,15 @@ const generateEmojiHints = async (editor: Editor): Promise<Hints | null> => {
|
||||||
const cursor = editor.getCursor()
|
const cursor = editor.getCursor()
|
||||||
const skinTone = await emojiIndex.getPreferredSkinTone()
|
const skinTone = await emojiIndex.getPreferredSkinTone()
|
||||||
const emojiEventDetails: EmojiClickEventDetail[] = suggestionList
|
const emojiEventDetails: EmojiClickEventDetail[] = suggestionList
|
||||||
.filter(emoji => !!emoji.shortcodes)
|
.filter((emoji) => !!emoji.shortcodes)
|
||||||
.map((emoji) => ({
|
.map((emoji) => ({
|
||||||
emoji,
|
emoji,
|
||||||
skinTone: skinTone,
|
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
|
name: emoji.name
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const hints = emojiEventDetails
|
const hints = emojiEventDetails.map(convertEmojiEventToHint).filter((o) => !!o) as Hint[]
|
||||||
.map(convertEmojiEventToHint)
|
|
||||||
.filter(o => !!o) as Hint[]
|
|
||||||
return {
|
return {
|
||||||
list: hints,
|
list: hints,
|
||||||
from: Pos(cursor.line, searchTerm.start),
|
from: Pos(cursor.line, searchTerm.start),
|
||||||
|
|
|
@ -30,10 +30,12 @@ const headerHint = (editor: Editor): Promise<Hints | null> => {
|
||||||
resolve(null)
|
resolve(null)
|
||||||
} else {
|
} else {
|
||||||
resolve({
|
resolve({
|
||||||
list: suggestions.map((suggestion): Hint => ({
|
list: suggestions.map(
|
||||||
text: allSupportedHeadersTextToInsert[allSupportedHeaders.indexOf(suggestion)],
|
(suggestion): Hint => ({
|
||||||
displayText: suggestion
|
text: allSupportedHeadersTextToInsert[allSupportedHeaders.indexOf(suggestion)],
|
||||||
})),
|
displayText: suggestion
|
||||||
|
})
|
||||||
|
),
|
||||||
from: Pos(cursor.line, searchTerm.start),
|
from: Pos(cursor.line, searchTerm.start),
|
||||||
to: Pos(cursor.line, searchTerm.end)
|
to: Pos(cursor.line, searchTerm.end)
|
||||||
})
|
})
|
||||||
|
|
|
@ -28,9 +28,11 @@ const imageHint = (editor: Editor): Promise<Hints | null> => {
|
||||||
resolve(null)
|
resolve(null)
|
||||||
} else {
|
} else {
|
||||||
resolve({
|
resolve({
|
||||||
list: suggestions.map((suggestion: string): Hint => ({
|
list: suggestions.map(
|
||||||
text: suggestion
|
(suggestion: string): Hint => ({
|
||||||
})),
|
text: suggestion
|
||||||
|
})
|
||||||
|
),
|
||||||
from: Pos(cursor.line, searchTerm.start),
|
from: Pos(cursor.line, searchTerm.start),
|
||||||
to: Pos(cursor.line, searchTerm.end + 1)
|
to: Pos(cursor.line, searchTerm.end + 1)
|
||||||
})
|
})
|
||||||
|
|
|
@ -15,13 +15,13 @@ import { LinkAndExtraTagHinter } from './link-and-extra-tag'
|
||||||
import { PDFHinter } from './pdf'
|
import { PDFHinter } from './pdf'
|
||||||
|
|
||||||
interface findWordAtCursorResponse {
|
interface findWordAtCursorResponse {
|
||||||
start: number,
|
start: number
|
||||||
end: number,
|
end: number
|
||||||
text: string
|
text: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Hinter {
|
export interface Hinter {
|
||||||
wordRegExp: RegExp,
|
wordRegExp: RegExp
|
||||||
hint: (editor: Editor) => Promise<Hints | null>
|
hint: (editor: Editor) => Promise<Hints | null>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,8 +40,7 @@ export const findWordAtCursor = (editor: Editor): findWordAtCursorResponse => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
text: line.slice(start, end)
|
text: line.slice(start, end).toLowerCase(),
|
||||||
.toLowerCase(),
|
|
||||||
start: start,
|
start: start,
|
||||||
end: end
|
end: end
|
||||||
}
|
}
|
||||||
|
@ -49,9 +48,8 @@ export const findWordAtCursor = (editor: Editor): findWordAtCursorResponse => {
|
||||||
|
|
||||||
export const search = (term: string, list: string[]): string[] => {
|
export const search = (term: string, list: string[]): string[] => {
|
||||||
const suggestions: string[] = []
|
const suggestions: string[] = []
|
||||||
list.forEach(item => {
|
list.forEach((item) => {
|
||||||
if (item.toLowerCase()
|
if (item.toLowerCase().startsWith(term.toLowerCase())) {
|
||||||
.startsWith(term.toLowerCase())) {
|
|
||||||
suggestions.push(item)
|
suggestions.push(item)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -22,7 +22,6 @@ const allSupportedLinks = [
|
||||||
'name',
|
'name',
|
||||||
'time',
|
'time',
|
||||||
'[color=#FFFFFF]'
|
'[color=#FFFFFF]'
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const linkAndExtraTagHint = (editor: Editor): Promise<Hints | null> => {
|
const linkAndExtraTagHint = (editor: Editor): Promise<Hints | null> => {
|
||||||
|
@ -46,13 +45,12 @@ const linkAndExtraTagHint = (editor: Editor): Promise<Hints | null> => {
|
||||||
case 'name':
|
case 'name':
|
||||||
// Get the user when a completion happens, this prevents to early calls resulting in 'Anonymous'
|
// Get the user when a completion happens, this prevents to early calls resulting in 'Anonymous'
|
||||||
return {
|
return {
|
||||||
text: `[name=${ userName }]`
|
text: `[name=${userName}]`
|
||||||
}
|
}
|
||||||
case 'time':
|
case 'time':
|
||||||
// show the current time when the autocompletion is opened and not when the function is loaded
|
// show the current time when the autocompletion is opened and not when the function is loaded
|
||||||
return {
|
return {
|
||||||
text: `[time=${ DateTime.local()
|
text: `[time=${DateTime.local().toFormat('DDDD T')}]`
|
||||||
.toFormat('DDDD T') }]`
|
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -23,9 +23,11 @@ const pdfHint = (editor: Editor): Promise<Hints | null> => {
|
||||||
resolve(null)
|
resolve(null)
|
||||||
} else {
|
} else {
|
||||||
resolve({
|
resolve({
|
||||||
list: suggestions.map((suggestion: string): Hint => ({
|
list: suggestions.map(
|
||||||
text: suggestion
|
(suggestion: string): Hint => ({
|
||||||
})),
|
text: suggestion
|
||||||
|
})
|
||||||
|
),
|
||||||
from: Pos(cursor.line, searchTerm.start),
|
from: Pos(cursor.line, searchTerm.start),
|
||||||
to: Pos(cursor.line, searchTerm.end + 1)
|
to: Pos(cursor.line, searchTerm.end + 1)
|
||||||
})
|
})
|
||||||
|
|
|
@ -64,16 +64,22 @@ const onChange = (editor: Editor) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DropEvent {
|
interface DropEvent {
|
||||||
pageX: number,
|
pageX: number
|
||||||
pageY: number,
|
pageY: number
|
||||||
dataTransfer: {
|
dataTransfer: {
|
||||||
files: FileList
|
files: FileList
|
||||||
effectAllowed: string
|
effectAllowed: string
|
||||||
} | null
|
} | null
|
||||||
preventDefault: () => void
|
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 { t } = useTranslation()
|
||||||
const maxLength = useSelector((state: ApplicationState) => state.config.maxDocumentLength)
|
const maxLength = useSelector((state: ApplicationState) => state.config.maxDocumentLength)
|
||||||
const smartPasteEnabled = useSelector((state: ApplicationState) => state.editorConfig.smartPaste)
|
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 [editorScroll, setEditorScroll] = useState<ScrollInfo>()
|
||||||
const onEditorScroll = useCallback((editor: Editor, data: ScrollInfo) => setEditorScroll(data), [])
|
const onEditorScroll = useCallback((editor: Editor, data: ScrollInfo) => setEditorScroll(data), [])
|
||||||
|
|
||||||
const onPaste = useCallback((pasteEditor: Editor, event: PasteEvent) => {
|
const onPaste = useCallback(
|
||||||
if (!event || !event.clipboardData) {
|
(pasteEditor: Editor, event: PasteEvent) => {
|
||||||
return
|
if (!event || !event.clipboardData) {
|
||||||
}
|
|
||||||
if (smartPasteEnabled) {
|
|
||||||
const tableInserted = handleTablePaste(event, pasteEditor)
|
|
||||||
if (tableInserted) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
if (smartPasteEnabled) {
|
||||||
handleFilePaste(event, pasteEditor)
|
const tableInserted = handleTablePaste(event, pasteEditor)
|
||||||
}, [smartPasteEnabled])
|
if (tableInserted) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleFilePaste(event, pasteEditor)
|
||||||
|
},
|
||||||
|
[smartPasteEnabled]
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editor || !onScroll || !editorScroll) {
|
if (!editor || !onScroll || !editorScroll) {
|
||||||
|
@ -112,7 +121,7 @@ export const EditorPane: React.FC<EditorPaneProps & ScrollProps> = ({ onContentC
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const heightOfLine = (lineInfo.handle as { height: number }).height
|
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 percentage = Math.floor(percentageRaw * 100)
|
||||||
|
|
||||||
const newScrollState: ScrollState = { firstLineInView: line + 1, scrolledPercentage: percentage }
|
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 startYOfLine = editor.heightAtLine(scrollState.firstLineInView - 1, 'local')
|
||||||
const heightOfLine = (editor.lineInfo(scrollState.firstLineInView - 1).handle as { height: number }).height
|
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)
|
const newPosition = Math.floor(newPositionRaw)
|
||||||
if (newPosition !== lastScrollPosition.current) {
|
if (newPosition !== lastScrollPosition.current) {
|
||||||
lastScrollPosition.current = newPosition
|
lastScrollPosition.current = newPosition
|
||||||
|
@ -133,28 +142,44 @@ export const EditorPane: React.FC<EditorPaneProps & ScrollProps> = ({ onContentC
|
||||||
}
|
}
|
||||||
}, [editor, scrollState])
|
}, [editor, scrollState])
|
||||||
|
|
||||||
const onBeforeChange = useCallback((editor: Editor, data: EditorChange, value: string) => {
|
const onBeforeChange = useCallback(
|
||||||
if (value.length > maxLength && !maxLengthWarningAlreadyShown.current) {
|
(editor: Editor, data: EditorChange, value: string) => {
|
||||||
setShowMaxLengthWarning(true)
|
if (value.length > maxLength && !maxLengthWarningAlreadyShown.current) {
|
||||||
maxLengthWarningAlreadyShown.current = true
|
setShowMaxLengthWarning(true)
|
||||||
}
|
maxLengthWarningAlreadyShown.current = true
|
||||||
if (value.length <= maxLength) {
|
}
|
||||||
maxLengthWarningAlreadyShown.current = false
|
if (value.length <= maxLength) {
|
||||||
}
|
maxLengthWarningAlreadyShown.current = false
|
||||||
onContentChange(value)
|
}
|
||||||
}, [onContentChange, maxLength, maxLengthWarningAlreadyShown])
|
onContentChange(value)
|
||||||
const onEditorDidMount = useCallback(mountedEditor => {
|
},
|
||||||
setStatusBarInfo(createStatusInfo(mountedEditor, maxLength))
|
[onContentChange, maxLength, maxLengthWarningAlreadyShown]
|
||||||
setEditor(mountedEditor)
|
)
|
||||||
}, [maxLength])
|
const onEditorDidMount = useCallback(
|
||||||
|
(mountedEditor) => {
|
||||||
|
setStatusBarInfo(createStatusInfo(mountedEditor, maxLength))
|
||||||
|
setEditor(mountedEditor)
|
||||||
|
},
|
||||||
|
[maxLength]
|
||||||
|
)
|
||||||
|
|
||||||
const onCursorActivity = useCallback((editorWithActivity) => {
|
const onCursorActivity = useCallback(
|
||||||
setStatusBarInfo(createStatusInfo(editorWithActivity, maxLength))
|
(editorWithActivity) => {
|
||||||
}, [maxLength])
|
setStatusBarInfo(createStatusInfo(editorWithActivity, maxLength))
|
||||||
|
},
|
||||||
|
[maxLength]
|
||||||
|
)
|
||||||
|
|
||||||
const onDrop = useCallback((dropEditor: Editor, event: DropEvent) => {
|
const onDrop = useCallback((dropEditor: Editor, event: DropEvent) => {
|
||||||
if (event && dropEditor && event.pageX && event.pageY && event.dataTransfer &&
|
if (
|
||||||
event.dataTransfer.files && event.dataTransfer.files.length >= 1) {
|
event &&
|
||||||
|
dropEditor &&
|
||||||
|
event.pageX &&
|
||||||
|
event.pageY &&
|
||||||
|
event.dataTransfer &&
|
||||||
|
event.dataTransfer.files &&
|
||||||
|
event.dataTransfer.files.length >= 1
|
||||||
|
) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
const top: number = event.pageY
|
const top: number = event.pageY
|
||||||
const left: number = event.pageX
|
const left: number = event.pageX
|
||||||
|
@ -167,53 +192,52 @@ export const EditorPane: React.FC<EditorPaneProps & ScrollProps> = ({ onContentC
|
||||||
|
|
||||||
const onMaxLengthHide = useCallback(() => setShowMaxLengthWarning(false), [])
|
const onMaxLengthHide = useCallback(() => setShowMaxLengthWarning(false), [])
|
||||||
|
|
||||||
const codeMirrorOptions: EditorConfiguration = useMemo<EditorConfiguration>(() => ({
|
const codeMirrorOptions: EditorConfiguration = useMemo<EditorConfiguration>(
|
||||||
...editorPreferences,
|
() => ({
|
||||||
mode: 'gfm',
|
...editorPreferences,
|
||||||
viewportMargin: 20,
|
mode: 'gfm',
|
||||||
styleActiveLine: true,
|
viewportMargin: 20,
|
||||||
lineNumbers: true,
|
styleActiveLine: true,
|
||||||
lineWrapping: true,
|
lineNumbers: true,
|
||||||
showCursorWhenSelecting: true,
|
lineWrapping: true,
|
||||||
highlightSelectionMatches: true,
|
showCursorWhenSelecting: true,
|
||||||
inputStyle: 'textarea',
|
highlightSelectionMatches: true,
|
||||||
matchBrackets: true,
|
inputStyle: 'textarea',
|
||||||
autoCloseBrackets: true,
|
matchBrackets: true,
|
||||||
matchTags: {
|
autoCloseBrackets: true,
|
||||||
bothTags: true
|
matchTags: {
|
||||||
},
|
bothTags: true
|
||||||
autoCloseTags: true,
|
},
|
||||||
foldGutter: true,
|
autoCloseTags: true,
|
||||||
gutters: [
|
foldGutter: true,
|
||||||
'CodeMirror-linenumbers',
|
gutters: ['CodeMirror-linenumbers', 'authorship-gutters', 'CodeMirror-foldgutter'],
|
||||||
'authorship-gutters',
|
extraKeys: defaultKeyMap,
|
||||||
'CodeMirror-foldgutter'
|
flattenSpans: true,
|
||||||
],
|
addModeClass: true,
|
||||||
extraKeys: defaultKeyMap,
|
autoRefresh: true,
|
||||||
flattenSpans: true,
|
// otherCursors: true,
|
||||||
addModeClass: true,
|
placeholder: t('editor.placeholder')
|
||||||
autoRefresh: true,
|
}),
|
||||||
// otherCursors: true,
|
[t, editorPreferences]
|
||||||
placeholder: t('editor.placeholder')
|
)
|
||||||
}), [t, editorPreferences])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ 'd-flex flex-column h-100 position-relative' } onMouseEnter={ onMakeScrollSource }>
|
<div className={'d-flex flex-column h-100 position-relative'} onMouseEnter={onMakeScrollSource}>
|
||||||
<MaxLengthWarningModal show={ showMaxLengthWarning } onHide={ onMaxLengthHide } maxLength={ maxLength }/>
|
<MaxLengthWarningModal show={showMaxLengthWarning} onHide={onMaxLengthHide} maxLength={maxLength} />
|
||||||
<ToolBar editor={ editor }/>
|
<ToolBar editor={editor} />
|
||||||
<ControlledCodeMirror
|
<ControlledCodeMirror
|
||||||
className={ `overflow-hidden w-100 flex-fill ${ ligaturesEnabled ? '' : 'no-ligatures' }` }
|
className={`overflow-hidden w-100 flex-fill ${ligaturesEnabled ? '' : 'no-ligatures'}`}
|
||||||
value={ content }
|
value={content}
|
||||||
options={ codeMirrorOptions }
|
options={codeMirrorOptions}
|
||||||
onChange={ onChange }
|
onChange={onChange}
|
||||||
onPaste={ onPaste }
|
onPaste={onPaste}
|
||||||
onDrop={ onDrop }
|
onDrop={onDrop}
|
||||||
onCursorActivity={ onCursorActivity }
|
onCursorActivity={onCursorActivity}
|
||||||
editorDidMount={ onEditorDidMount }
|
editorDidMount={onEditorDidMount}
|
||||||
onBeforeChange={ onBeforeChange }
|
onBeforeChange={onBeforeChange}
|
||||||
onScroll={ onEditorScroll }
|
onScroll={onEditorScroll}
|
||||||
/>
|
/>
|
||||||
<StatusBar { ...statusBarInfo } />
|
<StatusBar {...statusBarInfo} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ import {
|
||||||
underlineSelection
|
underlineSelection
|
||||||
} from './tool-bar/utils/toolbarButtonUtils'
|
} 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 f10 = (editor: Editor): void | typeof Pass => editor.setOption('fullScreen', !editor.getOption('fullScreen'))
|
||||||
const esc = (editor: Editor): void | typeof Pass => {
|
const esc = (editor: Editor): void | typeof Pass => {
|
||||||
|
@ -30,8 +30,7 @@ const tab = (editor: Editor) => {
|
||||||
const tab = '\t'
|
const tab = '\t'
|
||||||
|
|
||||||
// contruct x length spaces
|
// contruct x length spaces
|
||||||
const spaces = Array((editor.getOption('indentUnit') ?? 0) + 1)
|
const spaces = Array((editor.getOption('indentUnit') ?? 0) + 1).join(' ')
|
||||||
.join(' ')
|
|
||||||
|
|
||||||
// auto indent whole line when in list or blockquote
|
// auto indent whole line when in list or blockquote
|
||||||
const cursor = editor.getCursor()
|
const cursor = editor.getCursor()
|
||||||
|
@ -44,9 +43,7 @@ const tab = (editor: Editor) => {
|
||||||
const regex = /^(\s*)(>[> ]*|[*+-]\s|(\d+)([.)]))/
|
const regex = /^(\s*)(>[> ]*|[*+-]\s|(\d+)([.)]))/
|
||||||
|
|
||||||
let match
|
let match
|
||||||
const multiple = editor.getSelection()
|
const multiple = editor.getSelection().split('\n').length > 1 || editor.getSelections().length > 1
|
||||||
.split('\n').length > 1 ||
|
|
||||||
editor.getSelections().length > 1
|
|
||||||
|
|
||||||
if (multiple) {
|
if (multiple) {
|
||||||
editor.execCommand('defaultTab')
|
editor.execCommand('defaultTab')
|
||||||
|
@ -72,35 +69,35 @@ const tab = (editor: Editor) => {
|
||||||
|
|
||||||
export const defaultKeyMap: KeyMap = !isMac
|
export const defaultKeyMap: KeyMap = !isMac
|
||||||
? {
|
? {
|
||||||
F9: suppressKey,
|
F9: suppressKey,
|
||||||
F10: f10,
|
F10: f10,
|
||||||
Esc: esc,
|
Esc: esc,
|
||||||
'Ctrl-S': suppressKey,
|
'Ctrl-S': suppressKey,
|
||||||
Enter: 'newlineAndIndentContinueMarkdownList',
|
Enter: 'newlineAndIndentContinueMarkdownList',
|
||||||
Tab: tab,
|
Tab: tab,
|
||||||
Home: 'goLineLeftSmart',
|
Home: 'goLineLeftSmart',
|
||||||
End: 'goLineRight',
|
End: 'goLineRight',
|
||||||
'Ctrl-I': makeSelectionItalic,
|
'Ctrl-I': makeSelectionItalic,
|
||||||
'Ctrl-B': makeSelectionBold,
|
'Ctrl-B': makeSelectionBold,
|
||||||
'Ctrl-U': underlineSelection,
|
'Ctrl-U': underlineSelection,
|
||||||
'Ctrl-D': strikeThroughSelection,
|
'Ctrl-D': strikeThroughSelection,
|
||||||
'Ctrl-M': markSelection,
|
'Ctrl-M': markSelection,
|
||||||
'Ctrl-K': addLink
|
'Ctrl-K': addLink
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
F9: suppressKey,
|
F9: suppressKey,
|
||||||
F10: f10,
|
F10: f10,
|
||||||
Esc: esc,
|
Esc: esc,
|
||||||
'Cmd-S': suppressKey,
|
'Cmd-S': suppressKey,
|
||||||
Enter: 'newlineAndIndentContinueMarkdownList',
|
Enter: 'newlineAndIndentContinueMarkdownList',
|
||||||
Tab: tab,
|
Tab: tab,
|
||||||
'Cmd-Left': 'goLineLeftSmart',
|
'Cmd-Left': 'goLineLeftSmart',
|
||||||
'Cmd-Right': 'goLineRight',
|
'Cmd-Right': 'goLineRight',
|
||||||
Home: 'goLineLeftSmart',
|
Home: 'goLineLeftSmart',
|
||||||
End: 'goLineRight',
|
End: 'goLineRight',
|
||||||
'Cmd-I': makeSelectionItalic,
|
'Cmd-I': makeSelectionItalic,
|
||||||
'Cmd-B': makeSelectionBold,
|
'Cmd-B': makeSelectionBold,
|
||||||
'Cmd-U': underlineSelection,
|
'Cmd-U': underlineSelection,
|
||||||
'Cmd-D': strikeThroughSelection,
|
'Cmd-D': strikeThroughSelection,
|
||||||
'Cmd-M': markSelection
|
'Cmd-M': markSelection
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,11 +34,17 @@ export const createStatusInfo = (editor: Editor, maxDocumentLength: number): Sta
|
||||||
remainingCharacters: maxDocumentLength - editor.getValue().length,
|
remainingCharacters: maxDocumentLength - editor.getValue().length,
|
||||||
linesInDocument: editor.lineCount(),
|
linesInDocument: editor.lineCount(),
|
||||||
selectedColumns: editor.getSelection().length,
|
selectedColumns: editor.getSelection().length,
|
||||||
selectedLines: editor.getSelection()
|
selectedLines: editor.getSelection().split('\n').length
|
||||||
.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 { t } = useTranslation()
|
||||||
|
|
||||||
const getLengthTooltip = useMemo(() => {
|
const getLengthTooltip = useMemo(() => {
|
||||||
|
@ -52,27 +58,26 @@ export const StatusBar: React.FC<StatusBarInfo> = ({ position, selectedColumns,
|
||||||
}, [remainingCharacters, t])
|
}, [remainingCharacters, t])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="d-flex flex-row status-bar px-2">
|
<div className='d-flex flex-row status-bar px-2'>
|
||||||
<div>
|
<div>
|
||||||
<span>{ t('editor.statusBar.cursor', { line: position.line + 1, columns: position.ch + 1 }) }</span>
|
<span>{t('editor.statusBar.cursor', { line: position.line + 1, columns: position.ch + 1 })}</span>
|
||||||
<ShowIf condition={ selectedColumns !== 0 && selectedLines !== 0 }>
|
<ShowIf condition={selectedColumns !== 0 && selectedLines !== 0}>
|
||||||
<ShowIf condition={ selectedLines === 1 }>
|
<ShowIf condition={selectedLines === 1}>
|
||||||
<span> – { t('editor.statusBar.selection.column', { count: selectedColumns }) }</span>
|
<span> – {t('editor.statusBar.selection.column', { count: selectedColumns })}</span>
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
<ShowIf condition={ selectedLines > 1 }>
|
<ShowIf condition={selectedLines > 1}>
|
||||||
<span> – { t('editor.statusBar.selection.line', { count: selectedLines }) }</span>
|
<span> – {t('editor.statusBar.selection.line', { count: selectedLines })}</span>
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-auto">
|
<div className='ml-auto'>
|
||||||
<span>{ t('editor.statusBar.lines', { lines: linesInDocument }) }</span>
|
<span>{t('editor.statusBar.lines', { lines: linesInDocument })}</span>
|
||||||
–
|
–
|
||||||
<span
|
<span
|
||||||
data-cy={ 'remainingCharacters' }
|
data-cy={'remainingCharacters'}
|
||||||
title={ getLengthTooltip }
|
title={getLengthTooltip}
|
||||||
className={ remainingCharacters <= 0 ? 'text-danger' : remainingCharacters <= 100 ? 'text-warning' : '' }
|
className={remainingCharacters <= 0 ? 'text-danger' : remainingCharacters <= 100 ? 'text-warning' : ''}>
|
||||||
>
|
{t('editor.statusBar.length', { length: charactersInDocument })}
|
||||||
{ t('editor.statusBar.length', { length: charactersInDocument }) }
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,90 +7,83 @@
|
||||||
import { convertClipboardTableToMarkdown, isTable } from './table-extractor'
|
import { convertClipboardTableToMarkdown, isTable } from './table-extractor'
|
||||||
|
|
||||||
describe('isTable detection: ', () => {
|
describe('isTable detection: ', () => {
|
||||||
|
|
||||||
it('empty string is no table', () => {
|
it('empty string is no table', () => {
|
||||||
expect(isTable(''))
|
expect(isTable('')).toBe(false)
|
||||||
.toBe(false)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('single line is no table', () => {
|
it('single line is no table', () => {
|
||||||
const input = 'some none table'
|
const input = 'some none table'
|
||||||
expect(isTable(input))
|
expect(isTable(input)).toBe(false)
|
||||||
.toBe(false)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('multiple lines without tabs are no table', () => {
|
it('multiple lines without tabs are no table', () => {
|
||||||
const input = 'some none table\nanother line'
|
const input = 'some none table\nanother line'
|
||||||
expect(isTable(input))
|
expect(isTable(input)).toBe(false)
|
||||||
.toBe(false)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('code blocks are no table', () => {
|
it('code blocks are no table', () => {
|
||||||
const input = '```python\ndef a:\n\tprint("a")\n\tprint("b")```'
|
const input = '```python\ndef a:\n\tprint("a")\n\tprint("b")```'
|
||||||
expect(isTable(input))
|
expect(isTable(input)).toBe(false)
|
||||||
.toBe(false)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('tab-indented text is no table', () => {
|
it('tab-indented text is no table', () => {
|
||||||
const input = '\tsome tab indented text\n\tabc\n\tdef'
|
const input = '\tsome tab indented text\n\tabc\n\tdef'
|
||||||
expect(isTable(input))
|
expect(isTable(input)).toBe(false)
|
||||||
.toBe(false)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('not equal number of tabs is no table', () => {
|
it('not equal number of tabs is no table', () => {
|
||||||
const input = '1 ...\n2\tabc\n3\td\te\tf\n4\t16'
|
const input = '1 ...\n2\tabc\n3\td\te\tf\n4\t16'
|
||||||
expect(isTable(input))
|
expect(isTable(input)).toBe(false)
|
||||||
.toBe(false)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('table without newline at end is valid', () => {
|
it('table without newline at end is valid', () => {
|
||||||
const input = '1\t1\n2\t4\n3\t9\n4\t16\n5\t25'
|
const input = '1\t1\n2\t4\n3\t9\n4\t16\n5\t25'
|
||||||
expect(isTable(input))
|
expect(isTable(input)).toBe(true)
|
||||||
.toBe(true)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('table with newline at end is valid', () => {
|
it('table with newline at end is valid', () => {
|
||||||
const input = '1\t1\n2\t4\n3\t9\n4\t16\n5\t25\n'
|
const input = '1\t1\n2\t4\n3\t9\n4\t16\n5\t25\n'
|
||||||
expect(isTable(input))
|
expect(isTable(input)).toBe(true)
|
||||||
.toBe(true)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('table with some first cells missing is valid', () => {
|
it('table with some first cells missing is valid', () => {
|
||||||
const input = '1\t1\n\t0\n\t0\n4\t16\n5\t25\n'
|
const input = '1\t1\n\t0\n\t0\n4\t16\n5\t25\n'
|
||||||
expect(isTable(input))
|
expect(isTable(input)).toBe(true)
|
||||||
.toBe(true)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('table with some last cells missing is valid', () => {
|
it('table with some last cells missing is valid', () => {
|
||||||
const input = '1\t1\n2\t\n3\t\n4\t16\n'
|
const input = '1\t1\n2\t\n3\t\n4\t16\n'
|
||||||
expect(isTable(input))
|
expect(isTable(input)).toBe(true)
|
||||||
.toBe(true)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Conversion from clipboard table to markdown format', () => {
|
describe('Conversion from clipboard table to markdown format', () => {
|
||||||
it('normal table without newline at end converts right', () => {
|
it('normal table without newline at end converts right', () => {
|
||||||
const input = '1\t1\ta\n2\t4\tb\n3\t9\tc\n4\t16\td'
|
const input = '1\t1\ta\n2\t4\tb\n3\t9\tc\n4\t16\td'
|
||||||
expect(convertClipboardTableToMarkdown(input))
|
expect(convertClipboardTableToMarkdown(input)).toEqual(
|
||||||
.toEqual('| #1 | #2 | #3 |\n| -- | -- | -- |\n| 1 | 1 | a |\n| 2 | 4 | b |\n| 3 | 9 | c |\n| 4 | 16 | d |')
|
'| #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', () => {
|
it('normal table with newline at end converts right', () => {
|
||||||
const input = '1\t1\n2\t4\n3\t9\n4\t16\n'
|
const input = '1\t1\n2\t4\n3\t9\n4\t16\n'
|
||||||
expect(convertClipboardTableToMarkdown(input))
|
expect(convertClipboardTableToMarkdown(input)).toEqual(
|
||||||
.toEqual('| #1 | #2 |\n| -- | -- |\n| 1 | 1 |\n| 2 | 4 |\n| 3 | 9 |\n| 4 | 16 |')
|
'| #1 | #2 |\n| -- | -- |\n| 1 | 1 |\n| 2 | 4 |\n| 3 | 9 |\n| 4 | 16 |'
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('table with some first cells missing converts right', () => {
|
it('table with some first cells missing converts right', () => {
|
||||||
const input = '1\t1\n\t0\n\t0\n4\t16\n'
|
const input = '1\t1\n\t0\n\t0\n4\t16\n'
|
||||||
expect(convertClipboardTableToMarkdown(input))
|
expect(convertClipboardTableToMarkdown(input)).toEqual(
|
||||||
.toEqual('| #1 | #2 |\n| -- | -- |\n| 1 | 1 |\n| | 0 |\n| | 0 |\n| 4 | 16 |')
|
'| #1 | #2 |\n| -- | -- |\n| 1 | 1 |\n| | 0 |\n| | 0 |\n| 4 | 16 |'
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('table with some last cells missing converts right', () => {
|
it('table with some last cells missing converts right', () => {
|
||||||
const input = '1\t1\n2\t\n3\t\n4\t16\n'
|
const input = '1\t1\n2\t\n3\t\n4\t16\n'
|
||||||
expect(convertClipboardTableToMarkdown(input))
|
expect(convertClipboardTableToMarkdown(input)).toEqual(
|
||||||
.toEqual('| #1 | #2 |\n| -- | -- |\n| 1 | 1 |\n| 2 | |\n| 3 | |\n| 4 | 16 |')
|
'| #1 | #2 |\n| -- | -- |\n| 1 | 1 |\n| 2 | |\n| 3 | |\n| 4 | 16 |'
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('empty input results in empty output', () => {
|
it('empty input results in empty output', () => {
|
||||||
|
|
|
@ -16,43 +16,35 @@ export const isTable = (text: string): boolean => {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines = text.split(/\r?\n/)
|
const lines = text.split(/\r?\n/).filter((line) => line.trim() !== '')
|
||||||
.filter(line => line.trim() !== '')
|
|
||||||
|
|
||||||
// Tab-indented text should not be matched as a table
|
// 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
|
return false
|
||||||
}
|
}
|
||||||
// Every line should have the same amount of tabs (table columns)
|
// Every line should have the same amount of tabs (table columns)
|
||||||
const tabsPerLines = lines.map(line => line.match(/\t/g)?.length ?? 0)
|
const tabsPerLines = lines.map((line) => line.match(/\t/g)?.length ?? 0)
|
||||||
return tabsPerLines.every(line => line === tabsPerLines[0])
|
return tabsPerLines.every((line) => line === tabsPerLines[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
export const convertClipboardTableToMarkdown = (pasteData: string): string => {
|
export const convertClipboardTableToMarkdown = (pasteData: string): string => {
|
||||||
if (pasteData.trim() === '') {
|
if (pasteData.trim() === '') {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
const tableRows = pasteData.split(/\r?\n/)
|
const tableRows = pasteData.split(/\r?\n/).filter((row) => row.trim() !== '')
|
||||||
.filter(row => row.trim() !== '')
|
|
||||||
const tableCells = tableRows.reduce((cellsInRow, row, index) => {
|
const tableCells = tableRows.reduce((cellsInRow, row, index) => {
|
||||||
cellsInRow[index] = row.split('\t')
|
cellsInRow[index] = row.split('\t')
|
||||||
return cellsInRow
|
return cellsInRow
|
||||||
}, [] as string[][])
|
}, [] as string[][])
|
||||||
const arrayMaxRows = createNumberRangeArray(tableCells.length)
|
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
|
const headRow1 = arrayMaxColumns.map((col) => `| #${col + 1} `).join('') + '|'
|
||||||
.map(col => `| #${ col + 1 } `)
|
const headRow2 = arrayMaxColumns.map((col) => `| -${'-'.repeat((col + 1).toString().length)} `).join('') + '|'
|
||||||
.join('') + '|'
|
|
||||||
const headRow2 = arrayMaxColumns
|
|
||||||
.map(col => `| -${ '-'.repeat((col + 1).toString().length) } `)
|
|
||||||
.join('') + '|'
|
|
||||||
const body = arrayMaxRows
|
const body = arrayMaxRows
|
||||||
.map(row => {
|
.map((row) => {
|
||||||
return arrayMaxColumns
|
return arrayMaxColumns.map((col) => '| ' + tableCells[row][col] + ' ').join('') + '|'
|
||||||
.map(col => '| ' + tableCells[row][col] + ' ')
|
|
||||||
.join('') + '|'
|
|
||||||
})
|
})
|
||||||
.join('\n')
|
.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…
Add table
Add a link
Reference in a new issue