mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #9882 from overleaf/ii-dashboard-system-message-migration
[web] Project dashboard system & translation message migration GitOrigin-RevId: 0c723a3b526980e5c749da44ebe8a0a3edcc66ad
This commit is contained in:
parent
cdbf8c1831
commit
7a20e1376d
13 changed files with 387 additions and 105 deletions
|
@ -18,6 +18,12 @@ block append meta
|
||||||
meta(name="ol-tags" data-type="json" content=tags)
|
meta(name="ol-tags" data-type="json" content=tags)
|
||||||
meta(name="ol-portalTemplates" data-type="json" content=portalTemplates)
|
meta(name="ol-portalTemplates" data-type="json" content=portalTemplates)
|
||||||
meta(name="ol-prefetchedProjectsBlob" data-type="json" content=prefetchedProjectsBlob)
|
meta(name="ol-prefetchedProjectsBlob" data-type="json" content=prefetchedProjectsBlob)
|
||||||
|
if (suggestedLanguageSubdomainConfig)
|
||||||
|
meta(name="ol-suggestedLanguage" data-type="json" content=Object.assign(suggestedLanguageSubdomainConfig, {
|
||||||
|
lngName: translate(suggestedLanguageSubdomainConfig.lngCode),
|
||||||
|
imgUrl: buildImgPath("flags/24/" + suggestedLanguageSubdomainConfig.lngCode + ".png")
|
||||||
|
}))
|
||||||
|
meta(name="ol-currentUrl" data-type="string" content=currentUrl)
|
||||||
|
|
||||||
block content
|
block content
|
||||||
main.content.content-alt.project-list-react#project-list-root
|
main.content.content-alt.project-list-react#project-list-root
|
||||||
|
|
|
@ -74,6 +74,7 @@
|
||||||
"checking_project_github_status": "",
|
"checking_project_github_status": "",
|
||||||
"clear_cached_files": "",
|
"clear_cached_files": "",
|
||||||
"clear_search": "",
|
"clear_search": "",
|
||||||
|
"click_here_to_view_sl_in_lng": "",
|
||||||
"clone_with_git": "",
|
"clone_with_git": "",
|
||||||
"close": "",
|
"close": "",
|
||||||
"clsi_maintenance": "",
|
"clsi_maintenance": "",
|
||||||
|
@ -105,6 +106,7 @@
|
||||||
"copy_project": "",
|
"copy_project": "",
|
||||||
"copying": "",
|
"copying": "",
|
||||||
"country": "",
|
"country": "",
|
||||||
|
"country_flag": "",
|
||||||
"create": "",
|
"create": "",
|
||||||
"create_first_project": "",
|
"create_first_project": "",
|
||||||
"create_new_folder": "",
|
"create_new_folder": "",
|
||||||
|
|
|
@ -1,20 +1,17 @@
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import classnames from 'classnames'
|
|
||||||
|
|
||||||
type CloseProps = {
|
type CloseProps = {
|
||||||
onDismiss: () => void
|
onDismiss: React.MouseEventHandler<HTMLButtonElement>
|
||||||
} & React.ComponentProps<'div'>
|
}
|
||||||
|
|
||||||
function Close({ onDismiss, className, ...props }: CloseProps) {
|
function Close({ onDismiss }: CloseProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classnames('notification-close', className)} {...props}>
|
|
||||||
<button type="button" className="close pull-right" onClick={onDismiss}>
|
<button type="button" className="close pull-right" onClick={onDismiss}>
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
<span className="sr-only">{t('close')}</span>
|
<span className="sr-only">{t('close')}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,11 @@ function Notification({
|
||||||
<li className={classnames('notification-entry', className)} {...props}>
|
<li className={classnames('notification-entry', className)} {...props}>
|
||||||
<Alert bsStyle={bsStyle}>
|
<Alert bsStyle={bsStyle}>
|
||||||
{children}
|
{children}
|
||||||
{onDismiss ? <Close onDismiss={handleDismiss} /> : null}
|
{onDismiss ? (
|
||||||
|
<div className="notification-close">
|
||||||
|
<Close onDismiss={handleDismiss} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</Alert>
|
</Alert>
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import Close from './close'
|
||||||
|
import usePersistedState from '../../../../shared/hooks/use-persisted-state'
|
||||||
|
|
||||||
|
type SystemMessageProps = {
|
||||||
|
id: string
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
function SystemMessage({ id, children }: SystemMessageProps) {
|
||||||
|
const [hidden, setHidden] = usePersistedState(
|
||||||
|
`systemMessage.hide.${id}`,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
if (hidden) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className="system-message">
|
||||||
|
{id !== 'protected' ? <Close onDismiss={() => setHidden(true)} /> : null}
|
||||||
|
{children}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SystemMessage
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import SystemMessage from './system-message'
|
||||||
|
import TranslationMessage from './translation-message'
|
||||||
|
import useAsync from '../../../../shared/hooks/use-async'
|
||||||
|
import { getJSON } from '../../../../infrastructure/fetch-json'
|
||||||
|
import getMeta from '../../../../utils/meta'
|
||||||
|
import {
|
||||||
|
SystemMessage as TSystemMessage,
|
||||||
|
SuggestedLanguage,
|
||||||
|
} from '../../../../../../types/project/dashboard/system-message'
|
||||||
|
|
||||||
|
const MESSAGE_POLL_INTERVAL = 15 * 60 * 1000
|
||||||
|
|
||||||
|
function SystemMessages() {
|
||||||
|
const { data: messages, runAsync } = useAsync<TSystemMessage[]>()
|
||||||
|
const suggestedLanguage = getMeta('ol-suggestedLanguage', undefined) as
|
||||||
|
| SuggestedLanguage
|
||||||
|
| undefined
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const pollMessages = () => {
|
||||||
|
// Ignore polling if tab is hidden or browser is offline
|
||||||
|
if (document.hidden || !navigator.onLine) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
runAsync(getJSON('/system/messages')).catch(console.error)
|
||||||
|
}
|
||||||
|
pollMessages()
|
||||||
|
|
||||||
|
const interval = setInterval(pollMessages, MESSAGE_POLL_INTERVAL)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval)
|
||||||
|
}
|
||||||
|
}, [runAsync])
|
||||||
|
|
||||||
|
if (!messages?.length && !suggestedLanguage) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className="system-messages">
|
||||||
|
{messages?.map((message, idx) => (
|
||||||
|
<SystemMessage key={idx} id={message._id}>
|
||||||
|
{message.content}
|
||||||
|
</SystemMessage>
|
||||||
|
))}
|
||||||
|
{suggestedLanguage ? <TranslationMessage /> : null}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SystemMessages
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
import Close from './close'
|
||||||
|
import usePersistedState from '../../../../shared/hooks/use-persisted-state'
|
||||||
|
import getMeta from '../../../../utils/meta'
|
||||||
|
import { SuggestedLanguage } from '../../../../../../types/project/dashboard/system-message'
|
||||||
|
|
||||||
|
function TranslationMessage() {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [hidden, setHidden] = usePersistedState('hide-i18n-notification', false)
|
||||||
|
const config = getMeta('ol-suggestedLanguage') as SuggestedLanguage
|
||||||
|
const currentUrl = getMeta('ol-currentUrl') as string
|
||||||
|
|
||||||
|
if (hidden) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className="system-message">
|
||||||
|
<Close onDismiss={() => setHidden(true)} />
|
||||||
|
<div className="text-center">
|
||||||
|
<a href={config.url + currentUrl}>
|
||||||
|
<Trans
|
||||||
|
i18nKey="click_here_to_view_sl_in_lng"
|
||||||
|
components={[<strong />]} // eslint-disable-line react/jsx-key
|
||||||
|
values={{ lngName: config.lngName }}
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
className="ms-1"
|
||||||
|
src={config.imgUrl}
|
||||||
|
alt={t('country_flag', { country: config.lngName })}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TranslationMessage
|
|
@ -13,6 +13,7 @@ import SidebarFilters from './sidebar/sidebar-filters'
|
||||||
import SurveyWidget from './survey-widget'
|
import SurveyWidget from './survey-widget'
|
||||||
import WelcomeMessage from './welcome-message'
|
import WelcomeMessage from './welcome-message'
|
||||||
import LoadingBranded from '../../../shared/components/loading-branded'
|
import LoadingBranded from '../../../shared/components/loading-branded'
|
||||||
|
import SystemMessages from './notifications/system-messages'
|
||||||
import UserNotifications from './notifications/user-notifications'
|
import UserNotifications from './notifications/user-notifications'
|
||||||
import SearchForm from './search-form'
|
import SearchForm from './search-form'
|
||||||
import ProjectsDropdown from './dropdown/projects-dropdown'
|
import ProjectsDropdown from './dropdown/projects-dropdown'
|
||||||
|
@ -51,6 +52,8 @@ function ProjectListPageContent() {
|
||||||
<LoadingBranded loadProgress={loadProgress} />
|
<LoadingBranded loadProgress={loadProgress} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
<>
|
||||||
|
<SystemMessages />
|
||||||
<div className="project-list-wrapper clearfix">
|
<div className="project-list-wrapper clearfix">
|
||||||
{totalProjectsCount > 0 ? (
|
{totalProjectsCount > 0 ? (
|
||||||
<>
|
<>
|
||||||
|
@ -152,6 +155,7 @@ function ProjectListPageContent() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
import SystemMessages from '../../js/features/project-list/components/notifications/system-messages'
|
||||||
|
import useFetchMock from '../hooks/use-fetch-mock'
|
||||||
|
import { FetchMockStatic } from 'fetch-mock'
|
||||||
|
|
||||||
|
export const SystemMessage = (args: any) => {
|
||||||
|
useFetchMock((fetchMock: FetchMockStatic) => {
|
||||||
|
fetchMock.get(/\/system\/messages/, [
|
||||||
|
{
|
||||||
|
_id: 1,
|
||||||
|
content: `
|
||||||
|
Closing this message will mark it as hidden.
|
||||||
|
Remove it from the local storage to make it appear again.
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'protected',
|
||||||
|
content: 'A protected message content - cannot be closed',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
window.metaAttributesCache = new Map()
|
||||||
|
|
||||||
|
return <SystemMessages {...args} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TranslationMessage = (args: any) => {
|
||||||
|
useFetchMock((fetchMock: FetchMockStatic) => {
|
||||||
|
fetchMock.get(/\/system\/messages/, [])
|
||||||
|
})
|
||||||
|
|
||||||
|
window.metaAttributesCache = new Map()
|
||||||
|
window.metaAttributesCache.set('ol-suggestedLanguage', {
|
||||||
|
url: '/dev/null',
|
||||||
|
lngName: 'German',
|
||||||
|
imgUrl: 'https://flagcdn.com/w40/de.png',
|
||||||
|
})
|
||||||
|
|
||||||
|
return <SystemMessages {...args} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Project List / System Messages',
|
||||||
|
component: SystemMessages,
|
||||||
|
}
|
|
@ -1,3 +1,9 @@
|
||||||
|
.system-messages {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.system-message {
|
.system-message {
|
||||||
padding: (@line-height-computed / 4) (@line-height-computed / 2);
|
padding: (@line-height-computed / 4) (@line-height-computed / 2);
|
||||||
background-color: @sys-msg-background;
|
background-color: @sys-msg-background;
|
||||||
|
|
|
@ -1443,6 +1443,7 @@
|
||||||
"address": "Address",
|
"address": "Address",
|
||||||
"coupon_code": "Coupon code",
|
"coupon_code": "Coupon code",
|
||||||
"country": "Country",
|
"country": "Country",
|
||||||
|
"country_flag": "__country__ country flag",
|
||||||
"billing_address": "Billing Address",
|
"billing_address": "Billing Address",
|
||||||
"upgrade_now": "Upgrade Now",
|
"upgrade_now": "Upgrade Now",
|
||||||
"state": "State",
|
"state": "State",
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react'
|
||||||
|
import fetchMock from 'fetch-mock'
|
||||||
|
import SystemMessages from '../../../../../frontend/js/features/project-list/components/notifications/system-messages'
|
||||||
|
|
||||||
|
describe('<SystemMessages />', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
fetchMock.reset()
|
||||||
|
localStorage.clear()
|
||||||
|
window.metaAttributesCache = new Map()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
fetchMock.reset()
|
||||||
|
localStorage.clear()
|
||||||
|
window.metaAttributesCache = new Map()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders non-dismissable system message', async function () {
|
||||||
|
const data = {
|
||||||
|
_id: 'protected',
|
||||||
|
content: 'Random content',
|
||||||
|
}
|
||||||
|
fetchMock.get(/\/system\/messages/, [data])
|
||||||
|
render(<SystemMessages />)
|
||||||
|
|
||||||
|
await fetchMock.flush(true)
|
||||||
|
|
||||||
|
screen.getByText(data.content)
|
||||||
|
expect(screen.queryByRole('button', { name: /close/i })).to.be.null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders and closes dismissable system message', async function () {
|
||||||
|
const data = {
|
||||||
|
_id: 1,
|
||||||
|
content: 'Random content',
|
||||||
|
}
|
||||||
|
fetchMock.get(/\/system\/messages/, [data])
|
||||||
|
render(<SystemMessages />)
|
||||||
|
|
||||||
|
await fetchMock.flush(true)
|
||||||
|
|
||||||
|
screen.getByText(data.content)
|
||||||
|
const closeBtn = screen.getByRole('button', { name: /close/i })
|
||||||
|
fireEvent.click(closeBtn)
|
||||||
|
|
||||||
|
expect(screen.queryByText(data.content)).to.be.null
|
||||||
|
|
||||||
|
const dismissed = localStorage.getItem(`systemMessage.hide.${data._id}`)
|
||||||
|
expect(dismissed).to.equal('true')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders and closes translation message', async function () {
|
||||||
|
const data = {
|
||||||
|
url: '/dev/null',
|
||||||
|
lngName: 'German',
|
||||||
|
imgUrl: 'https://flagcdn.com/w40/de.png',
|
||||||
|
}
|
||||||
|
const currentUrl = '/project'
|
||||||
|
fetchMock.get(/\/system\/messages/, [])
|
||||||
|
window.metaAttributesCache = new Map()
|
||||||
|
window.metaAttributesCache.set('ol-suggestedLanguage', data)
|
||||||
|
window.metaAttributesCache.set('ol-currentUrl', currentUrl)
|
||||||
|
render(<SystemMessages />)
|
||||||
|
|
||||||
|
await fetchMock.flush(true)
|
||||||
|
|
||||||
|
const link = screen.getByRole('link', { name: /click here/i })
|
||||||
|
expect(link.getAttribute('href')).to.equal(`${data.url}${currentUrl}`)
|
||||||
|
|
||||||
|
const flag = screen.getByRole('img', { hidden: true })
|
||||||
|
expect(flag.getAttribute('src')).to.equal(data.imgUrl)
|
||||||
|
|
||||||
|
const closeBtn = screen.getByRole('button', { name: /close/i })
|
||||||
|
fireEvent.click(closeBtn)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.queryByRole('link', {
|
||||||
|
name: `Click here to use Overleaf in ${data.lngName}`,
|
||||||
|
})
|
||||||
|
).to.be.null
|
||||||
|
expect(screen.queryByRole('img')).to.be.null
|
||||||
|
|
||||||
|
const dismissed = localStorage.getItem('hide-i18n-notification')
|
||||||
|
expect(dismissed).to.equal('true')
|
||||||
|
})
|
||||||
|
})
|
11
services/web/types/project/dashboard/system-message.ts
Normal file
11
services/web/types/project/dashboard/system-message.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
export type SystemMessage = {
|
||||||
|
_id: string
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SuggestedLanguage = {
|
||||||
|
url: string
|
||||||
|
imgUrl: string
|
||||||
|
lngCode: string
|
||||||
|
lngName: string
|
||||||
|
}
|
Loading…
Reference in a new issue