mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-25 03:06:31 -05:00
Change motd banner to motd modal
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
328bc917eb
commit
ee7cde0096
26 changed files with 361 additions and 269 deletions
|
@ -1 +0,0 @@
|
||||||
This is the mock banner call
|
|
|
@ -1,3 +0,0 @@
|
||||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
|
|
||||||
SPDX-License-Identifier: CC0-1.0
|
|
|
@ -1,80 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
const BANNER_LOCAL_STORAGE_KEY = 'banner.lastModified'
|
|
||||||
const MOCK_LAST_MODIFIED = 'mockETag'
|
|
||||||
const bannerMockContent = 'This is the mock banner call'
|
|
||||||
|
|
||||||
describe('Banner', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.intercept({
|
|
||||||
method: 'GET',
|
|
||||||
url: '/mock-backend/public/banner.txt'
|
|
||||||
}, {
|
|
||||||
statusCode: 200,
|
|
||||||
headers: { 'Last-Modified': MOCK_LAST_MODIFIED },
|
|
||||||
body: bannerMockContent
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.intercept({
|
|
||||||
method: 'HEAD',
|
|
||||||
url: '/mock-backend/public/banner.txt'
|
|
||||||
}, {
|
|
||||||
statusCode: 200,
|
|
||||||
headers: { 'Last-Modified': MOCK_LAST_MODIFIED }
|
|
||||||
})
|
|
||||||
.as('headBanner')
|
|
||||||
|
|
||||||
cy.visit('/')
|
|
||||||
localStorage.removeItem(BANNER_LOCAL_STORAGE_KEY)
|
|
||||||
expect(localStorage.getItem(BANNER_LOCAL_STORAGE_KEY)).to.be.null
|
|
||||||
})
|
|
||||||
|
|
||||||
it('shows the correct alert banner text', () => {
|
|
||||||
cy.get('[data-cy="motd-banner"]')
|
|
||||||
.contains(bannerMockContent)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can be dismissed', () => {
|
|
||||||
cy.get('[data-cy="motd-banner"]')
|
|
||||||
.contains(bannerMockContent)
|
|
||||||
cy.get('button[data-cy="motd-dismiss"]')
|
|
||||||
.click()
|
|
||||||
.then(() => {
|
|
||||||
expect(localStorage.getItem(BANNER_LOCAL_STORAGE_KEY))
|
|
||||||
.to
|
|
||||||
.equal(MOCK_LAST_MODIFIED)
|
|
||||||
})
|
|
||||||
cy.get('[data-cy="no-motd-banner"]')
|
|
||||||
.should('exist')
|
|
||||||
cy.get('[data-cy="motd-banner"]')
|
|
||||||
.should('not.exist')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('won\'t show again on reload', () => {
|
|
||||||
cy.get('[data-cy="motd-banner"]')
|
|
||||||
.contains(bannerMockContent)
|
|
||||||
cy.get('button[data-cy="motd-dismiss"]')
|
|
||||||
.click()
|
|
||||||
.then(() => {
|
|
||||||
expect(localStorage.getItem(BANNER_LOCAL_STORAGE_KEY))
|
|
||||||
.to
|
|
||||||
.equal(MOCK_LAST_MODIFIED)
|
|
||||||
})
|
|
||||||
cy.get('[data-cy="no-motd-banner"]')
|
|
||||||
.should('exist')
|
|
||||||
cy.get('[data-cy="motd-banner"]')
|
|
||||||
.should('not.exist')
|
|
||||||
cy.reload()
|
|
||||||
cy.get('main')
|
|
||||||
.should('exist')
|
|
||||||
cy.wait('@headBanner')
|
|
||||||
cy.get('[data-cy="no-motd-banner"]')
|
|
||||||
.should('exist')
|
|
||||||
cy.get('[data-cy="motd-banner"]')
|
|
||||||
.should('not.exist')
|
|
||||||
})
|
|
||||||
})
|
|
91
cypress/integration/motd.spec.ts
Normal file
91
cypress/integration/motd.spec.ts
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
const MOTD_LOCAL_STORAGE_KEY = 'motd.lastModified'
|
||||||
|
const MOCK_LAST_MODIFIED = 'mockETag'
|
||||||
|
const motdMockContent = 'This is the mock Motd call'
|
||||||
|
|
||||||
|
describe('Motd', () => {
|
||||||
|
const mockExistingMotd = () => {
|
||||||
|
cy.intercept('GET', '/mock-backend/public/motd.txt', {
|
||||||
|
statusCode: 200,
|
||||||
|
headers: { 'Last-Modified': MOCK_LAST_MODIFIED },
|
||||||
|
body: motdMockContent
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.intercept('HEAD', '/mock-backend/public/motd.txt', {
|
||||||
|
statusCode: 200,
|
||||||
|
headers: { 'Last-Modified': MOCK_LAST_MODIFIED }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorage.removeItem(MOTD_LOCAL_STORAGE_KEY)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows the correct alert Motd text', () => {
|
||||||
|
mockExistingMotd()
|
||||||
|
cy.visit('/')
|
||||||
|
cy.get('[data-cy="motd"]').contains(motdMockContent)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can be dismissed', () => {
|
||||||
|
mockExistingMotd()
|
||||||
|
cy.visit('/')
|
||||||
|
cy.get('[data-cy="motd"]').contains(motdMockContent)
|
||||||
|
cy.get('button[data-cy="motd-dismiss"]')
|
||||||
|
.click()
|
||||||
|
.then(() => {
|
||||||
|
expect(localStorage.getItem(MOTD_LOCAL_STORAGE_KEY)).to.equal(MOCK_LAST_MODIFIED)
|
||||||
|
})
|
||||||
|
cy.get('[data-cy="motd"]').should('not.exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it("won't show again after dismiss and reload", () => {
|
||||||
|
mockExistingMotd()
|
||||||
|
cy.visit('/')
|
||||||
|
cy.get('[data-cy="motd"]').contains(motdMockContent)
|
||||||
|
cy.get('button[data-cy="motd-dismiss"]')
|
||||||
|
.click()
|
||||||
|
.then(() => {
|
||||||
|
expect(localStorage.getItem(MOTD_LOCAL_STORAGE_KEY)).to.equal(MOCK_LAST_MODIFIED)
|
||||||
|
})
|
||||||
|
cy.get('[data-cy="motd"]').should('not.exist')
|
||||||
|
cy.reload()
|
||||||
|
cy.get('main').should('exist')
|
||||||
|
cy.get('[data-cy="motd"]').should('not.exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it("will show again after reload without dismiss", () => {
|
||||||
|
mockExistingMotd()
|
||||||
|
cy.visit('/')
|
||||||
|
cy.get('[data-cy="motd"]').contains(motdMockContent)
|
||||||
|
cy.reload()
|
||||||
|
cy.get('main').should('exist')
|
||||||
|
cy.get('[data-cy="motd"]').contains(motdMockContent)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("won't show again after dismiss and page navigation", () => {
|
||||||
|
mockExistingMotd()
|
||||||
|
cy.visit('/')
|
||||||
|
cy.get('[data-cy="motd"]').contains(motdMockContent)
|
||||||
|
cy.get('button[data-cy="motd-dismiss"]')
|
||||||
|
.click()
|
||||||
|
.then(() => {
|
||||||
|
expect(localStorage.getItem(MOTD_LOCAL_STORAGE_KEY)).to.equal(MOCK_LAST_MODIFIED)
|
||||||
|
})
|
||||||
|
cy.get('[data-cy="motd"]').should('not.exist')
|
||||||
|
cy.get('#navLinkHistory').click()
|
||||||
|
cy.get('main').should('exist')
|
||||||
|
cy.get('[data-cy="motd"]').should('not.exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it("won't show if no file exists", () => {
|
||||||
|
cy.visit('/')
|
||||||
|
cy.get('main').should('exist')
|
||||||
|
cy.get('[data-cy="motd"]').should('not.exist')
|
||||||
|
})
|
||||||
|
})
|
|
@ -68,4 +68,12 @@ Cypress.Commands.add('loadConfig', (additionalConfig?: Partial<typeof config>) =
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.loadConfig()
|
cy.loadConfig()
|
||||||
|
|
||||||
|
cy.intercept('GET', '/mock-backend/public/motd.txt', {
|
||||||
|
body: '404 Not Found!',
|
||||||
|
statusCode: 404
|
||||||
|
})
|
||||||
|
cy.intercept('HEAD', '/mock-backend/public/motd.txt', {
|
||||||
|
statusCode: 404
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -20,8 +20,8 @@ beforeEach(() => {
|
||||||
cy.intercept(`/mock-backend/api/private/notes/${ testNoteId }-get`, {
|
cy.intercept(`/mock-backend/api/private/notes/${ testNoteId }-get`, {
|
||||||
"content": "",
|
"content": "",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"id": "ABC11",
|
"id": "mock_note_id",
|
||||||
"alias": "banner",
|
"alias": "mockNote",
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"viewCount": 0,
|
"viewCount": 0,
|
||||||
"updateTime": "2021-04-24T09:27:51.000Z",
|
"updateTime": "2021-04-24T09:27:51.000Z",
|
||||||
|
|
|
@ -455,6 +455,7 @@
|
||||||
"export": "Export",
|
"export": "Export",
|
||||||
"refresh": "Refresh",
|
"refresh": "Refresh",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
|
"dismiss": "Dismiss",
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
|
@ -495,5 +496,8 @@
|
||||||
"other": "There was an error while registering your account. Just try it again."
|
"other": "There was an error while registering your account. Just try it again."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"motd": {
|
||||||
|
"title": "Information"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,10 +18,6 @@
|
||||||
"name": "DEMO Corp",
|
"name": "DEMO Corp",
|
||||||
"logo": "/mock-backend/public/img/demo.png"
|
"logo": "/mock-backend/public/img/demo.png"
|
||||||
},
|
},
|
||||||
"banner": {
|
|
||||||
"text": "This is the test banner text",
|
|
||||||
"timestamp": "2020-05-22T20:46:08.962Z"
|
|
||||||
},
|
|
||||||
"customAuthNames": {
|
"customAuthNames": {
|
||||||
"ldap": "FooBar",
|
"ldap": "FooBar",
|
||||||
"oauth2": "Olaf2",
|
"oauth2": "Olaf2",
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
This is the test banner text
|
|
1
public/mock-backend/public/motd.txt
Normal file
1
public/mock-backend/public/motd.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
This is the test motd text
|
|
@ -1,54 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { setBanner } from '../../../redux/banner/methods'
|
|
||||||
import { defaultFetchConfig } from '../../../api/utils'
|
|
||||||
import { Logger } from '../../../utils/logger'
|
|
||||||
|
|
||||||
export const BANNER_LOCAL_STORAGE_KEY = 'banner.lastModified'
|
|
||||||
const log = new Logger('Banner')
|
|
||||||
|
|
||||||
export const fetchAndSetBanner = async (customizeAssetsUrl: string): Promise<void> => {
|
|
||||||
const cachedLastModified = window.localStorage.getItem(BANNER_LOCAL_STORAGE_KEY)
|
|
||||||
const bannerUrl = `${customizeAssetsUrl}banner.txt`
|
|
||||||
|
|
||||||
if (cachedLastModified) {
|
|
||||||
const response = await fetch(bannerUrl, {
|
|
||||||
...defaultFetchConfig,
|
|
||||||
method: 'HEAD'
|
|
||||||
})
|
|
||||||
if (response.status !== 200) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (response.headers.get('Last-Modified') === cachedLastModified) {
|
|
||||||
setBanner({
|
|
||||||
lastModified: cachedLastModified,
|
|
||||||
text: ''
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(bannerUrl, {
|
|
||||||
...defaultFetchConfig
|
|
||||||
})
|
|
||||||
|
|
||||||
if (response.status !== 200) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const bannerText = await response.text()
|
|
||||||
|
|
||||||
const lastModified = response.headers.get('Last-Modified')
|
|
||||||
if (!lastModified) {
|
|
||||||
log.warn("'Last-Modified' not found for banner.txt!")
|
|
||||||
}
|
|
||||||
|
|
||||||
setBanner({
|
|
||||||
lastModified: lastModified,
|
|
||||||
text: bannerText
|
|
||||||
})
|
|
||||||
}
|
|
56
src/components/application-loader/initializers/fetch-motd.ts
Normal file
56
src/components/application-loader/initializers/fetch-motd.ts
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { setMotd } from '../../../redux/motd/methods'
|
||||||
|
import { defaultFetchConfig } from '../../../api/utils'
|
||||||
|
import { Logger } from '../../../utils/logger'
|
||||||
|
|
||||||
|
export const MOTD_LOCAL_STORAGE_KEY = 'motd.lastModified'
|
||||||
|
const log = new Logger('Motd')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the current motd from the backend and sets the content in the global application state.
|
||||||
|
* If the motd hasn't changed since the last time then the global application state won't be changed.
|
||||||
|
* To check if the motd has changed the "last modified" header from the request
|
||||||
|
* will be compared to the saved value from the browser's local storage.
|
||||||
|
*
|
||||||
|
* @param customizeAssetsUrl the URL where the motd.txt can be found.
|
||||||
|
* @return A promise that gets resolved if the motd was fetched successfully.
|
||||||
|
*/
|
||||||
|
export const fetchMotd = async (customizeAssetsUrl: string): Promise<void> => {
|
||||||
|
const cachedLastModified = window.localStorage.getItem(MOTD_LOCAL_STORAGE_KEY)
|
||||||
|
const motdUrl = `${customizeAssetsUrl}motd.txt`
|
||||||
|
|
||||||
|
if (cachedLastModified) {
|
||||||
|
const response = await fetch(motdUrl, {
|
||||||
|
...defaultFetchConfig,
|
||||||
|
method: 'HEAD'
|
||||||
|
})
|
||||||
|
if (response.status !== 200) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (response.headers.get('Last-Modified') === cachedLastModified) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(motdUrl, {
|
||||||
|
...defaultFetchConfig
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const motdText = await response.text()
|
||||||
|
|
||||||
|
const lastModified = response.headers.get('Last-Modified')
|
||||||
|
if (!lastModified) {
|
||||||
|
log.warn("'Last-Modified' not found for motd.txt!")
|
||||||
|
}
|
||||||
|
|
||||||
|
setMotd(motdText, lastModified)
|
||||||
|
}
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
import { setUpI18n } from './i18n/i18n'
|
import { setUpI18n } from './i18n/i18n'
|
||||||
import { refreshHistoryState } from '../../../redux/history/methods'
|
import { refreshHistoryState } from '../../../redux/history/methods'
|
||||||
import { fetchAndSetBanner } from './fetch-and-set-banner'
|
import { fetchMotd } from './fetch-motd'
|
||||||
import { setApiUrl } from '../../../redux/api-url/methods'
|
import { setApiUrl } from '../../../redux/api-url/methods'
|
||||||
import { fetchAndSetUser } from '../../login-page/auth/utils'
|
import { fetchAndSetUser } from '../../login-page/auth/utils'
|
||||||
import { fetchFrontendConfig } from './fetch-frontend-config'
|
import { fetchFrontendConfig } from './fetch-frontend-config'
|
||||||
|
@ -47,8 +47,8 @@ export const createSetUpTaskList = (
|
||||||
task: fetchAndSetUser()
|
task: fetchAndSetUser()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Banner',
|
name: 'Motd',
|
||||||
task: fetchAndSetBanner(customizeAssetsUrl)
|
task: fetchMotd(customizeAssetsUrl)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Load history state',
|
name: 'Load history state',
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { ShowIf } from '../show-if/show-if'
|
||||||
|
|
||||||
export interface CommonModalProps {
|
export interface CommonModalProps {
|
||||||
show: boolean
|
show: boolean
|
||||||
onHide: () => void
|
onHide?: () => void
|
||||||
titleI18nKey?: string
|
titleI18nKey?: string
|
||||||
title?: string
|
title?: string
|
||||||
closeButton?: boolean
|
closeButton?: boolean
|
||||||
|
|
|
@ -1,47 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { useCallback } from 'react'
|
|
||||||
import { Alert, Button } from 'react-bootstrap'
|
|
||||||
import { setBanner } from '../../../redux/banner/methods'
|
|
||||||
import { ForkAwesomeIcon } from '../fork-awesome/fork-awesome-icon'
|
|
||||||
import { BANNER_LOCAL_STORAGE_KEY } from '../../application-loader/initializers/fetch-and-set-banner'
|
|
||||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
|
||||||
|
|
||||||
export const MotdBanner: React.FC = () => {
|
|
||||||
const bannerState = useApplicationState((state) => state.banner)
|
|
||||||
|
|
||||||
const dismissBanner = useCallback(() => {
|
|
||||||
if (bannerState.lastModified) {
|
|
||||||
window.localStorage.setItem(BANNER_LOCAL_STORAGE_KEY, bannerState.lastModified)
|
|
||||||
}
|
|
||||||
setBanner({
|
|
||||||
text: '',
|
|
||||||
lastModified: null
|
|
||||||
})
|
|
||||||
}, [bannerState])
|
|
||||||
|
|
||||||
if (bannerState.text === undefined) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!bannerState.text) {
|
|
||||||
return <span data-cy={'no-motd-banner'} />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Alert
|
|
||||||
data-cy={'motd-banner'}
|
|
||||||
variant='primary'
|
|
||||||
dir='auto'
|
|
||||||
className='mb-0 text-center d-flex flex-row justify-content-center'>
|
|
||||||
<span className='flex-grow-1 align-self-center text-black'>{bannerState.text}</span>
|
|
||||||
<Button data-cy={'motd-dismiss'} variant='outline-primary' size='sm' className='mx-2' onClick={dismissBanner}>
|
|
||||||
<ForkAwesomeIcon icon='times' />
|
|
||||||
</Button>
|
|
||||||
</Alert>
|
|
||||||
)
|
|
||||||
}
|
|
60
src/components/common/motd-modal/motd-modal.tsx
Normal file
60
src/components/common/motd-modal/motd-modal.tsx
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { Fragment, useCallback, useMemo } from 'react'
|
||||||
|
import { Button, Modal } from 'react-bootstrap'
|
||||||
|
import { CommonModal } from '../modals/common-modal'
|
||||||
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
||||||
|
import { dismissMotd } from '../../../redux/motd/methods'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the motd from the global application state and shows it in a modal.
|
||||||
|
* If the modal gets dismissed by the user then the "last modified" identifier will be written into the local storage
|
||||||
|
* to prevent that the motd will be shown again until it gets changed.
|
||||||
|
*/
|
||||||
|
export const MotdModal: React.FC = () => {
|
||||||
|
useTranslation()
|
||||||
|
const motdState = useApplicationState((state) => state.motd)
|
||||||
|
|
||||||
|
const domContent = useMemo(() => {
|
||||||
|
if (!motdState) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return motdState.text
|
||||||
|
?.split('\n')
|
||||||
|
.map((line) => <span>{line}</span>)
|
||||||
|
.reduce((previousLine, currentLine, currentLineIndex) => (
|
||||||
|
<Fragment key={currentLineIndex}>
|
||||||
|
{previousLine}
|
||||||
|
<br />
|
||||||
|
{currentLine}
|
||||||
|
</Fragment>
|
||||||
|
))
|
||||||
|
}, [motdState])
|
||||||
|
|
||||||
|
const dismiss = useCallback(() => {
|
||||||
|
if (!motdState) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dismissMotd()
|
||||||
|
}, [motdState])
|
||||||
|
|
||||||
|
if (motdState === null || motdState.dismissed) {
|
||||||
|
return null
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<CommonModal data-cy={'motd'} show={!!motdState} titleI18nKey={'motd.title'}>
|
||||||
|
<Modal.Body>{domContent}</Modal.Body>
|
||||||
|
<Modal.Footer>
|
||||||
|
<Button variant={'success'} onClick={dismiss} data-cy={'motd-dismiss'}>
|
||||||
|
<Trans i18nKey={'common.dismiss'} />
|
||||||
|
</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</CommonModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,7 +10,7 @@ import { useParams } from 'react-router'
|
||||||
import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode'
|
import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode'
|
||||||
import { useDocumentTitleWithNoteTitle } from '../../hooks/common/use-document-title-with-note-title'
|
import { useDocumentTitleWithNoteTitle } from '../../hooks/common/use-document-title-with-note-title'
|
||||||
import { updateNoteTitleByFirstHeading } from '../../redux/note-details/methods'
|
import { updateNoteTitleByFirstHeading } from '../../redux/note-details/methods'
|
||||||
import { MotdBanner } from '../common/motd-banner/motd-banner'
|
import { MotdModal } from '../common/motd-modal/motd-modal'
|
||||||
import { ShowIf } from '../common/show-if/show-if'
|
import { ShowIf } from '../common/show-if/show-if'
|
||||||
import { AppBar, AppBarMode } from '../editor-page/app-bar/app-bar'
|
import { AppBar, AppBarMode } from '../editor-page/app-bar/app-bar'
|
||||||
import { EditorPagePathParams } from '../editor-page/editor-page'
|
import { EditorPagePathParams } from '../editor-page/editor-page'
|
||||||
|
@ -24,6 +24,7 @@ import { useApplicationState } from '../../hooks/common/use-application-state'
|
||||||
import { useNoteMarkdownContentWithoutFrontmatter } from '../../hooks/common/use-note-markdown-content-without-frontmatter'
|
import { useNoteMarkdownContentWithoutFrontmatter } from '../../hooks/common/use-note-markdown-content-without-frontmatter'
|
||||||
import { EditorToRendererCommunicatorContextProvider } from '../editor-page/render-context/editor-to-renderer-communicator-context-provider'
|
import { EditorToRendererCommunicatorContextProvider } from '../editor-page/render-context/editor-to-renderer-communicator-context-provider'
|
||||||
import { useSendFrontmatterInfoFromReduxToRenderer } from '../editor-page/renderer-pane/hooks/use-send-frontmatter-info-from-redux-to-renderer'
|
import { useSendFrontmatterInfoFromReduxToRenderer } from '../editor-page/renderer-pane/hooks/use-send-frontmatter-info-from-redux-to-renderer'
|
||||||
|
import { UiNotifications } from '../notifications/ui-notifications'
|
||||||
|
|
||||||
export const DocumentReadOnlyPage: React.FC = () => {
|
export const DocumentReadOnlyPage: React.FC = () => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
|
@ -40,8 +41,9 @@ export const DocumentReadOnlyPage: React.FC = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditorToRendererCommunicatorContextProvider>
|
<EditorToRendererCommunicatorContextProvider>
|
||||||
|
<UiNotifications />
|
||||||
|
<MotdModal />
|
||||||
<div className={'d-flex flex-column mvh-100 bg-light'}>
|
<div className={'d-flex flex-column mvh-100 bg-light'}>
|
||||||
<MotdBanner />
|
|
||||||
<AppBar mode={AppBarMode.BASIC} />
|
<AppBar mode={AppBarMode.BASIC} />
|
||||||
<div className={'container'}>
|
<div className={'container'}>
|
||||||
<ErrorWhileLoadingNoteAlert show={error} />
|
<ErrorWhileLoadingNoteAlert show={error} />
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next'
|
||||||
import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode'
|
import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode'
|
||||||
import { useDocumentTitleWithNoteTitle } from '../../hooks/common/use-document-title-with-note-title'
|
import { useDocumentTitleWithNoteTitle } from '../../hooks/common/use-document-title-with-note-title'
|
||||||
import { setCheckboxInMarkdownContent, updateNoteTitleByFirstHeading } from '../../redux/note-details/methods'
|
import { setCheckboxInMarkdownContent, updateNoteTitleByFirstHeading } from '../../redux/note-details/methods'
|
||||||
import { MotdBanner } from '../common/motd-banner/motd-banner'
|
import { MotdModal } from '../common/motd-modal/motd-modal'
|
||||||
import { ShowIf } from '../common/show-if/show-if'
|
import { ShowIf } from '../common/show-if/show-if'
|
||||||
import { ErrorWhileLoadingNoteAlert } from '../document-read-only-page/ErrorWhileLoadingNoteAlert'
|
import { ErrorWhileLoadingNoteAlert } from '../document-read-only-page/ErrorWhileLoadingNoteAlert'
|
||||||
import { LoadingNoteAlert } from '../document-read-only-page/LoadingNoteAlert'
|
import { LoadingNoteAlert } from '../document-read-only-page/LoadingNoteAlert'
|
||||||
|
@ -126,7 +126,7 @@ export const EditorPage: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<EditorToRendererCommunicatorContextProvider>
|
<EditorToRendererCommunicatorContextProvider>
|
||||||
<UiNotifications />
|
<UiNotifications />
|
||||||
<MotdBanner />
|
<MotdModal />
|
||||||
<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'}>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
import React, { Fragment } from 'react'
|
import React, { Fragment } from 'react'
|
||||||
import { Container } from 'react-bootstrap'
|
import { Container } from 'react-bootstrap'
|
||||||
import { useDocumentTitle } from '../../hooks/common/use-document-title'
|
import { useDocumentTitle } from '../../hooks/common/use-document-title'
|
||||||
import { MotdBanner } from '../common/motd-banner/motd-banner'
|
import { MotdModal } from '../common/motd-modal/motd-modal'
|
||||||
import { Footer } from './footer/footer'
|
import { Footer } from './footer/footer'
|
||||||
import { HeaderBar } from './navigation/header-bar/header-bar'
|
import { HeaderBar } from './navigation/header-bar/header-bar'
|
||||||
import { UiNotifications } from '../notifications/ui-notifications'
|
import { UiNotifications } from '../notifications/ui-notifications'
|
||||||
|
@ -18,8 +18,8 @@ export const LandingLayout: React.FC = ({ children }) => {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<UiNotifications />
|
<UiNotifications />
|
||||||
|
<MotdModal />
|
||||||
<Container className='text-light d-flex flex-column mvh-100'>
|
<Container className='text-light d-flex flex-column mvh-100'>
|
||||||
<MotdBanner />
|
|
||||||
<HeaderBar />
|
<HeaderBar />
|
||||||
<div className={'d-flex flex-column justify-content-between flex-fill text-center'}>
|
<div className={'d-flex flex-column justify-content-between flex-fill text-center'}>
|
||||||
<main>{children}</main>
|
<main>{children}</main>
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { store } from '..'
|
|
||||||
import { BannerActionType, BannerState, SetBannerAction } from './types'
|
|
||||||
|
|
||||||
export const setBanner = (state: BannerState): void => {
|
|
||||||
store.dispatch({
|
|
||||||
type: BannerActionType.SET_BANNER,
|
|
||||||
state
|
|
||||||
} as SetBannerAction)
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Reducer } from 'redux'
|
|
||||||
import { BannerActions, BannerActionType, BannerState } from './types'
|
|
||||||
|
|
||||||
export const initialState: BannerState = {
|
|
||||||
text: undefined,
|
|
||||||
lastModified: null
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BannerReducer: Reducer<BannerState, BannerActions> = (
|
|
||||||
state: BannerState = initialState,
|
|
||||||
action: BannerActions
|
|
||||||
) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case BannerActionType.SET_BANNER:
|
|
||||||
return action.state
|
|
||||||
default:
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Action } from 'redux'
|
|
||||||
|
|
||||||
export enum BannerActionType {
|
|
||||||
SET_BANNER = 'banner/set'
|
|
||||||
}
|
|
||||||
|
|
||||||
export type BannerActions = SetBannerAction
|
|
||||||
|
|
||||||
export interface SetBannerAction extends Action<BannerActionType> {
|
|
||||||
type: BannerActionType.SET_BANNER
|
|
||||||
state: BannerState
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BannerState {
|
|
||||||
text: string | undefined
|
|
||||||
lastModified: string | null
|
|
||||||
}
|
|
|
@ -8,8 +8,8 @@ import { combineReducers, createStore, Reducer } from 'redux'
|
||||||
import { Config } from '../api/config/types'
|
import { Config } from '../api/config/types'
|
||||||
import { ApiUrlReducer } from './api-url/reducers'
|
import { ApiUrlReducer } from './api-url/reducers'
|
||||||
import { ApiUrlObject } from './api-url/types'
|
import { ApiUrlObject } from './api-url/types'
|
||||||
import { BannerReducer } from './banner/reducers'
|
import { MotdReducer } from './motd/reducers'
|
||||||
import { BannerState } from './banner/types'
|
import { OptionalMotdState } from './motd/types'
|
||||||
import { ConfigReducer } from './config/reducers'
|
import { ConfigReducer } from './config/reducers'
|
||||||
import { DarkModeConfigReducer } from './dark-mode/reducers'
|
import { DarkModeConfigReducer } from './dark-mode/reducers'
|
||||||
import { DarkModeConfig } from './dark-mode/types'
|
import { DarkModeConfig } from './dark-mode/types'
|
||||||
|
@ -29,7 +29,7 @@ import { RendererStatus } from './renderer-status/types'
|
||||||
export interface ApplicationState {
|
export interface ApplicationState {
|
||||||
user: OptionalUserState
|
user: OptionalUserState
|
||||||
config: Config
|
config: Config
|
||||||
banner: BannerState
|
motd: OptionalMotdState
|
||||||
history: HistoryEntry[]
|
history: HistoryEntry[]
|
||||||
apiUrl: ApiUrlObject
|
apiUrl: ApiUrlObject
|
||||||
editorConfig: EditorConfig
|
editorConfig: EditorConfig
|
||||||
|
@ -42,7 +42,7 @@ export interface ApplicationState {
|
||||||
export const allReducers: Reducer<ApplicationState> = combineReducers<ApplicationState>({
|
export const allReducers: Reducer<ApplicationState> = combineReducers<ApplicationState>({
|
||||||
user: UserReducer,
|
user: UserReducer,
|
||||||
config: ConfigReducer,
|
config: ConfigReducer,
|
||||||
banner: BannerReducer,
|
motd: MotdReducer,
|
||||||
apiUrl: ApiUrlReducer,
|
apiUrl: ApiUrlReducer,
|
||||||
history: HistoryReducer,
|
history: HistoryReducer,
|
||||||
editorConfig: EditorConfigReducer,
|
editorConfig: EditorConfigReducer,
|
||||||
|
|
31
src/redux/motd/methods.ts
Normal file
31
src/redux/motd/methods.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { store } from '..'
|
||||||
|
import { DismissMotdAction, MotdActionType, SetMotdAction } from './types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a not-dismissed motd message in the global application state.
|
||||||
|
*
|
||||||
|
* @param text The motd text content
|
||||||
|
* @param lastModified An identifier that describes when the motd was changed the last time.
|
||||||
|
*/
|
||||||
|
export const setMotd = (text: string, lastModified: string | null): void => {
|
||||||
|
store.dispatch({
|
||||||
|
type: MotdActionType.SET_MOTD,
|
||||||
|
text,
|
||||||
|
lastModified
|
||||||
|
} as SetMotdAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dismisses the currently saved motd message.
|
||||||
|
*/
|
||||||
|
export const dismissMotd = (): void => {
|
||||||
|
store.dispatch({
|
||||||
|
type: MotdActionType.DISMISS_MOTD
|
||||||
|
} as DismissMotdAction)
|
||||||
|
}
|
60
src/redux/motd/reducers.ts
Normal file
60
src/redux/motd/reducers.ts
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Reducer } from 'redux'
|
||||||
|
import { MotdActions, MotdActionType, MotdState, OptionalMotdState } from './types'
|
||||||
|
import { MOTD_LOCAL_STORAGE_KEY } from '../../components/application-loader/initializers/fetch-motd'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A reducer that modifies the {@link OptionalMotdState motd state} in the global application state.
|
||||||
|
*/
|
||||||
|
export const MotdReducer: Reducer<OptionalMotdState, MotdActions> = (
|
||||||
|
state: OptionalMotdState = null,
|
||||||
|
action: MotdActions
|
||||||
|
) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case MotdActionType.SET_MOTD:
|
||||||
|
return createNewMotdState(action.text, action.lastModified)
|
||||||
|
case MotdActionType.DISMISS_MOTD:
|
||||||
|
return createDismissedMotdState(state)
|
||||||
|
default:
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@link MotdState motd state} by copying the old state and setting the dismissed flag.
|
||||||
|
* It also writes the "last-modified" identifier into the browser's local storage.
|
||||||
|
*
|
||||||
|
* @param oldState The current motd state that should be copied
|
||||||
|
* @return The new state
|
||||||
|
*/
|
||||||
|
const createDismissedMotdState = (oldState: OptionalMotdState): OptionalMotdState => {
|
||||||
|
if (oldState === null) {
|
||||||
|
return null
|
||||||
|
} else {
|
||||||
|
if (oldState.lastModified) {
|
||||||
|
window.localStorage.setItem(MOTD_LOCAL_STORAGE_KEY, oldState.lastModified)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...oldState,
|
||||||
|
dismissed: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new not-dismissed motd state.
|
||||||
|
* @param text The motd text
|
||||||
|
* @param lastModified An identifier that describes when the motd text was changed the last time
|
||||||
|
*/
|
||||||
|
const createNewMotdState = (text: string, lastModified: string | null): MotdState => {
|
||||||
|
return {
|
||||||
|
text,
|
||||||
|
lastModified,
|
||||||
|
dismissed: false
|
||||||
|
}
|
||||||
|
}
|
32
src/redux/motd/types.ts
Normal file
32
src/redux/motd/types.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Action } from 'redux'
|
||||||
|
|
||||||
|
export enum MotdActionType {
|
||||||
|
SET_MOTD = 'motd/set',
|
||||||
|
DISMISS_MOTD = 'motd/dismiss'
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MotdActions = SetMotdAction | DismissMotdAction
|
||||||
|
|
||||||
|
export interface SetMotdAction extends Action<MotdActionType> {
|
||||||
|
type: MotdActionType.SET_MOTD
|
||||||
|
text: string
|
||||||
|
lastModified: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DismissMotdAction extends Action<MotdActionType> {
|
||||||
|
type: MotdActionType.DISMISS_MOTD
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MotdState {
|
||||||
|
text: string
|
||||||
|
lastModified: string | null
|
||||||
|
dismissed: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OptionalMotdState = MotdState | null
|
Loading…
Reference in a new issue