mirror of
https://github.com/overleaf/overleaf.git
synced 2025-12-05 01:10:29 +00:00
Compare commits
8 Commits
b8da04078d
...
7b6565c98f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b6565c98f | ||
|
|
0ecfc246a2 | ||
|
|
81ababb7aa | ||
|
|
5a11958a57 | ||
|
|
0abab86dc1 | ||
|
|
e5e279a19f | ||
|
|
95fda8dd36 | ||
|
|
2153fd7fa5 |
1
package-lock.json
generated
1
package-lock.json
generated
@@ -52822,6 +52822,7 @@
|
||||
"nock": "^13.5.6",
|
||||
"nvd3": "^1.8.6",
|
||||
"p-reflect": "^3.1.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"pdfjs-dist": "5.1.91",
|
||||
"pirates": "^4.0.1",
|
||||
"postcss": "^8.4.31",
|
||||
|
||||
@@ -880,6 +880,7 @@ function _finaliseRequest(projectId, options, project, docs, files) {
|
||||
pdfCachingMinChunkSize: options.pdfCachingMinChunkSize,
|
||||
flags,
|
||||
metricsMethod: options.compileGroup,
|
||||
metricsPath: options.metricsPath,
|
||||
},
|
||||
rootResourcePath,
|
||||
resources,
|
||||
|
||||
@@ -974,6 +974,35 @@ templates.removeGroupMember = NoCTAEmailTemplate({
|
||||
},
|
||||
})
|
||||
|
||||
templates.taxExemptCertificateRequired = NoCTAEmailTemplate({
|
||||
subject(opts) {
|
||||
return `Action required: Tax exemption verification for Overleaf [${opts.ein}]`
|
||||
},
|
||||
title() {
|
||||
return 'Action required: Tax exemption verification'
|
||||
},
|
||||
greeting() {
|
||||
return ''
|
||||
},
|
||||
message(opts) {
|
||||
return [
|
||||
'Thanks for letting us know your organization is tax exempt. To confirm this, we need some additional verification.',
|
||||
'Please reply to this email with one of the following documents attached:',
|
||||
'<ul>',
|
||||
'<li>Your IRS determination letter (for non-profits and similar organizations)</li>',
|
||||
'<li>Your state resale or exemption certificate</li>',
|
||||
'</ul>',
|
||||
`These should match the EIN you provided:${opts.ein}.`,
|
||||
'If you have any questions, let us know by replying to this email.',
|
||||
'<br/>',
|
||||
'Best wishes,',
|
||||
'Team Overleaf',
|
||||
'<br/>',
|
||||
`Our reference:${opts.stripeCustomerId}`,
|
||||
]
|
||||
},
|
||||
})
|
||||
|
||||
function _formatUserNameAndEmail(user, placeholder) {
|
||||
if (user.first_name && user.last_name) {
|
||||
const fullName = `${user.first_name} ${user.last_name}`
|
||||
|
||||
@@ -104,6 +104,9 @@ async function sendEmail(options, emailType) {
|
||||
replyTo: options.replyTo || EMAIL_SETTINGS.replyToAddress,
|
||||
socketTimeout: 30 * 1000,
|
||||
}
|
||||
if (options.cc) {
|
||||
sendMailOptions.cc = options.cc
|
||||
}
|
||||
if (EMAIL_SETTINGS.textEncoding != null) {
|
||||
sendMailOptions.textEncoding = EMAIL_SETTINGS.textEncoding
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ function ipMatcherAffiliation(userId) {
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
if (response.statusCode !== 200) {
|
||||
if (response.statusCode !== 200 || !body) {
|
||||
return callback()
|
||||
}
|
||||
|
||||
|
||||
@@ -227,7 +227,10 @@ async function getInstitutionUsersByHostname(hostname) {
|
||||
samlIdentifiers: 1,
|
||||
}
|
||||
|
||||
const users = await UserGetter.getUsersByHostname(hostname, projection)
|
||||
const users = await UserGetter.promises.getUsersByHostname(
|
||||
hostname,
|
||||
projection
|
||||
)
|
||||
users.forEach(user => {
|
||||
user.emails = decorateFullEmails(
|
||||
user.email,
|
||||
|
||||
@@ -1,88 +1,60 @@
|
||||
/* eslint-disable
|
||||
n/handle-callback-err,
|
||||
max-len,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const UserGetter = require('../User/UserGetter')
|
||||
const { isObjectIdInstance } = require('../Helpers/Mongo')
|
||||
const { promisify } = require('@overleaf/promise-utils')
|
||||
|
||||
const UserMembershipViewModel = {
|
||||
build(userOrEmail) {
|
||||
if (userOrEmail._id) {
|
||||
return buildUserViewModel(userOrEmail)
|
||||
return buildUserViewModel(userOrEmail, false)
|
||||
} else {
|
||||
return buildUserViewModelWithEmail(userOrEmail)
|
||||
}
|
||||
},
|
||||
|
||||
buildAsync(userOrIdOrEmailArray, callback) {
|
||||
if (callback == null) {
|
||||
callback = function () {}
|
||||
}
|
||||
|
||||
async buildAsync(userOrIdOrEmailArray) {
|
||||
const userObjectIds = userOrIdOrEmailArray.filter(isObjectIdInstance)
|
||||
|
||||
return UserGetter.getUsers(
|
||||
userObjectIds,
|
||||
{
|
||||
const results = []
|
||||
try {
|
||||
const users = await UserGetter.promises.getUsers(userObjectIds, {
|
||||
email: 1,
|
||||
first_name: 1,
|
||||
last_name: 1,
|
||||
lastLoggedIn: 1,
|
||||
lastActive: 1,
|
||||
enrollment: 1,
|
||||
},
|
||||
function (error, users) {
|
||||
const results = []
|
||||
|
||||
if (error != null) {
|
||||
userOrIdOrEmailArray.forEach(item => {
|
||||
if (isObjectIdInstance(item)) {
|
||||
results.push(buildUserViewModelWithId(item.toString()))
|
||||
} else {
|
||||
// `item` is a user or an email and can be parsed by #build
|
||||
results.push(UserMembershipViewModel.build(item))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
const usersMap = new Map()
|
||||
for (const user of users) {
|
||||
usersMap.set(user._id.toString(), user)
|
||||
}
|
||||
|
||||
userOrIdOrEmailArray.forEach(item => {
|
||||
if (isObjectIdInstance(item)) {
|
||||
const user = usersMap.get(item.toString())
|
||||
if (user == null) {
|
||||
results.push(buildUserViewModelWithId(item.toString()))
|
||||
} else {
|
||||
results.push(buildUserViewModel(user))
|
||||
}
|
||||
} else {
|
||||
// `item` is a user or an email and can be parsed by #build
|
||||
results.push(UserMembershipViewModel.build(item))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
callback(null, results)
|
||||
})
|
||||
const usersMap = new Map()
|
||||
for (const user of users) {
|
||||
usersMap.set(user._id.toString(), user)
|
||||
}
|
||||
)
|
||||
|
||||
userOrIdOrEmailArray.forEach(item => {
|
||||
if (isObjectIdInstance(item)) {
|
||||
const user = usersMap.get(item.toString())
|
||||
if (!user) {
|
||||
results.push(buildUserViewModelWithId(item.toString()))
|
||||
} else {
|
||||
results.push(buildUserViewModel(user, false))
|
||||
}
|
||||
} else {
|
||||
// `item` is a user or an email and can be parsed by #build
|
||||
results.push(UserMembershipViewModel.build(item))
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
userOrIdOrEmailArray.forEach(item => {
|
||||
if (isObjectIdInstance(item)) {
|
||||
results.push(buildUserViewModelWithId(item.toString()))
|
||||
} else {
|
||||
// `item` is a user or an email and can be parsed by #build
|
||||
results.push(UserMembershipViewModel.build(item))
|
||||
}
|
||||
})
|
||||
}
|
||||
return results
|
||||
},
|
||||
}
|
||||
|
||||
function buildUserViewModel(user, isInvite) {
|
||||
if (isInvite == null) {
|
||||
isInvite = false
|
||||
}
|
||||
return {
|
||||
_id: user._id || null,
|
||||
email: user.email || null,
|
||||
@@ -106,7 +78,7 @@ const buildUserViewModelWithEmail = email => buildUserViewModel({ email }, true)
|
||||
const buildUserViewModelWithId = id => buildUserViewModel({ _id: id }, false)
|
||||
|
||||
UserMembershipViewModel.promises = {
|
||||
buildAsync: promisify(UserMembershipViewModel.buildAsync),
|
||||
buildAsync: UserMembershipViewModel.buildAsync,
|
||||
}
|
||||
|
||||
module.exports = UserMembershipViewModel
|
||||
|
||||
@@ -1159,7 +1159,7 @@ async function initialize(webRouter, privateApiRouter, publicApiRouter) {
|
||||
CompileManager.compile(
|
||||
projectId,
|
||||
testUserId,
|
||||
{},
|
||||
{ metricsPath: 'health-check' },
|
||||
function (error, status, _outputFiles, clsiServerId) {
|
||||
if (handler) {
|
||||
clearTimeout(handler)
|
||||
|
||||
@@ -12,6 +12,7 @@ block append meta
|
||||
- const canDisplayAdminRedirect = canRedirectToAdminDomain()
|
||||
- const sessionUser = getSessionUser()
|
||||
- const staffAccess = sessionUser?.staffAccess
|
||||
- const canDisplayProjectUrlLookup = hasFeature('saas') && canDisplayAdminMenu && hasAdminCapability('view-project-setting', false)
|
||||
- const canDisplaySplitTestMenu = hasFeature('saas') && ((canDisplayAdminMenu && hasAdminCapability('view-split-test')) || staffAccess?.splitTestMetrics || staffAccess?.splitTestManagement)
|
||||
- const canDisplaySurveyMenu = hasFeature('saas') && canDisplayAdminMenu && hasAdminCapability('manage-survey', false)
|
||||
- const canDisplayScriptLogMenu = hasFeature('saas') && hasAdminCapability('view-script-log', false) && canDisplayAdminMenu
|
||||
@@ -26,6 +27,7 @@ block append meta
|
||||
title: nav.title,
|
||||
canDisplayAdminMenu,
|
||||
canDisplayAdminRedirect,
|
||||
canDisplayProjectUrlLookup,
|
||||
canDisplaySplitTestMenu,
|
||||
canDisplaySurveyMenu,
|
||||
canDisplayScriptLogMenu,
|
||||
|
||||
@@ -30,6 +30,7 @@ nav.navbar.navbar-default.navbar-main.navbar-expand-lg(
|
||||
|
||||
- var canDisplayAdminMenu = hasAdminAccess()
|
||||
- var canDisplayAdminRedirect = canRedirectToAdminDomain()
|
||||
- var canDisplayProjectUrlLookup = hasFeature('saas') && canDisplayAdminMenu && hasAdminCapability('view-project-setting', false)
|
||||
- var canDisplaySplitTestMenu = hasFeature('saas') && ((canDisplayAdminMenu && hasAdminCapability('view-split-test')) || (getSessionUser() && getSessionUser().staffAccess && (getSessionUser().staffAccess.splitTestMetrics || getSessionUser().staffAccess.splitTestManagement)))
|
||||
- var canDisplaySurveyMenu = hasFeature('saas') && canDisplayAdminMenu && hasAdminCapability('manage-survey', false)
|
||||
- var canDisplayScriptLogMenu = hasFeature('saas') && hasAdminCapability('view-script-log', false) && canDisplayAdminMenu
|
||||
@@ -47,7 +48,7 @@ nav.navbar.navbar-default.navbar-main.navbar-expand-lg(
|
||||
|
||||
#navbar-main-collapse.navbar-collapse.collapse
|
||||
ul.nav.navbar-nav.navbar-right.ms-auto(role='menubar')
|
||||
if canDisplayAdminMenu || canDisplayAdminRedirect || canDisplaySplitTestMenu
|
||||
if canDisplayAdminMenu || canDisplayAdminRedirect || canDisplayProjectUrlLookup || canDisplaySplitTestMenu
|
||||
+nav-item.dropdown.subdued
|
||||
button.dropdown-toggle(
|
||||
aria-haspopup='true'
|
||||
@@ -64,6 +65,7 @@ nav.navbar.navbar-default.navbar-main.navbar-expand-lg(
|
||||
if canDisplayAdminMenu
|
||||
+dropdown-menu-link-item(href='/admin') Manage Site
|
||||
+dropdown-menu-link-item(href='/admin/user') Manage Users
|
||||
if canDisplayProjectUrlLookup
|
||||
+dropdown-menu-link-item(href='/admin/project') Project URL Lookup
|
||||
if canDisplayAdminRedirect
|
||||
+dropdown-menu-link-item(href=settings.adminUrl) Switch to Admin
|
||||
|
||||
@@ -1287,6 +1287,7 @@
|
||||
"please_contact_support_to_makes_change_to_your_plan": "",
|
||||
"please_enter_confirmation_code": "",
|
||||
"please_get_in_touch": "",
|
||||
"please_keep_an_eye_out_for_issues": "",
|
||||
"please_link_before_making_primary": "",
|
||||
"please_provide_a_message": "",
|
||||
"please_provide_a_subject": "",
|
||||
@@ -1869,6 +1870,7 @@
|
||||
"this_project_exceeded_collaborator_limit": "",
|
||||
"this_project_exceeded_compile_timeout_limit_on_free_plan": "",
|
||||
"this_project_has_more_than_max_collabs": "",
|
||||
"this_project_is_compiled_using_untested_version": "",
|
||||
"this_project_is_public": "",
|
||||
"this_project_is_public_read_only": "",
|
||||
"this_project_will_appear_in_your_dropbox_folder_at": "",
|
||||
|
||||
@@ -17,6 +17,7 @@ import getMeta from '@/utils/meta'
|
||||
import PdfClearCacheButton from '@/features/pdf-preview/components/pdf-clear-cache-button'
|
||||
import PdfDownloadFilesButton from '@/features/pdf-preview/components/pdf-download-files-button'
|
||||
import { useIsNewErrorLogsPositionEnabled } from '../../utils/new-editor-utils'
|
||||
import RollingBuildSelectedReminder from './rolling-build-selected-reminder'
|
||||
|
||||
const logsComponents: Array<{
|
||||
import: { default: ElementType }
|
||||
@@ -85,6 +86,7 @@ function ErrorLogs({
|
||||
))}
|
||||
<TabContent className="error-logs new-error-logs">
|
||||
<div className="logs-pane-content">
|
||||
<RollingBuildSelectedReminder />
|
||||
{stoppedOnFirstError && includeErrors && <StopOnFirstErrorPrompt />}
|
||||
|
||||
{loadingError && (
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import OLNotification from '@/shared/components/ol/ol-notification'
|
||||
import { useTranslation, Trans } from 'react-i18next'
|
||||
import { useProjectContext } from '@/shared/context/project-context'
|
||||
import { onRollingBuild } from '@/shared/utils/rolling-build'
|
||||
|
||||
const RollingBuildSelectedReminder = () => {
|
||||
const { t } = useTranslation()
|
||||
const { project } = useProjectContext()
|
||||
if (!onRollingBuild(project?.imageName)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const content = (
|
||||
<Trans
|
||||
i18nKey="please_keep_an_eye_out_for_issues"
|
||||
components={[
|
||||
<a href="https://forms.gle/yD8CVm4Kop9KwShx9" />, // eslint-disable-line react/jsx-key, jsx-a11y/anchor-has-content
|
||||
<a href="https://docs.overleaf.com/getting-started/recompiling-your-project/selecting-a-tex-live-version-and-latex-compiler" />, // eslint-disable-line react/jsx-key, jsx-a11y/anchor-has-content
|
||||
]}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<OLNotification
|
||||
title={t('this_project_is_compiled_using_untested_version')}
|
||||
content={content}
|
||||
type="info"
|
||||
className="mb-0"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default RollingBuildSelectedReminder
|
||||
@@ -1,31 +1,28 @@
|
||||
import { useTutorial } from '@/shared/hooks/promotions/use-tutorial'
|
||||
|
||||
import { useEditorContext } from '@/shared/context/editor-context'
|
||||
import { useProjectSettingsContext } from '../editor-left-menu/context/project-settings-context'
|
||||
|
||||
import { useProjectContext } from '@/shared/context/project-context'
|
||||
import OLNotification from '@/shared/components/ol/ol-notification'
|
||||
import getMeta from '@/utils/meta'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCallback } from 'react'
|
||||
import { onRollingBuild } from '@/shared/utils/rolling-build'
|
||||
|
||||
export const TUTORIAL_KEY = 'rolling-compile-image-changed'
|
||||
const rollingImages = getMeta('ol-imageNames')
|
||||
.filter(img => img.rolling)
|
||||
.map(img => img.imageName)
|
||||
|
||||
const RollingCompileImageChangedAlert = () => {
|
||||
const { completeTutorial } = useTutorial(TUTORIAL_KEY)
|
||||
|
||||
const { project } = useProjectContext()
|
||||
const { inactiveTutorials } = useEditorContext()
|
||||
const { imageName } = useProjectSettingsContext()
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
completeTutorial({ event: 'promo-click', action: 'complete' })
|
||||
}, [completeTutorial])
|
||||
|
||||
const onRollingBuild = imageName && rollingImages.includes(imageName)
|
||||
if (inactiveTutorials.includes(TUTORIAL_KEY) || !onRollingBuild) {
|
||||
if (
|
||||
inactiveTutorials.includes(TUTORIAL_KEY) ||
|
||||
!onRollingBuild(project?.imageName)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { memo } from 'react'
|
||||
import classnames from 'classnames'
|
||||
import RollingBuildSelectedReminder from './rolling-build-selected-reminder'
|
||||
import PdfValidationIssue from './pdf-validation-issue'
|
||||
import StopOnFirstErrorPrompt from './stop-on-first-error-prompt'
|
||||
import TimeoutUpgradePromptNew from './timeout-upgrade-prompt-new'
|
||||
@@ -41,6 +42,8 @@ function PdfLogsViewer({ alwaysVisible = false }: { alwaysVisible?: boolean }) {
|
||||
data-testid="logs-pane"
|
||||
>
|
||||
<div className="logs-pane-content">
|
||||
<RollingBuildSelectedReminder />
|
||||
|
||||
{codeCheckFailed && <PdfCodeCheckFailedNotice />}
|
||||
|
||||
{stoppedOnFirstError && <StopOnFirstErrorPrompt />}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import OLNotification from '@/shared/components/ol/ol-notification'
|
||||
import { useTranslation, Trans } from 'react-i18next'
|
||||
import { useProjectContext } from '@/shared/context/project-context'
|
||||
import { onRollingBuild } from '@/shared/utils/rolling-build'
|
||||
|
||||
const RollingBuildSelectedReminder = () => {
|
||||
const { t } = useTranslation()
|
||||
const { project } = useProjectContext()
|
||||
|
||||
if (!onRollingBuild(project?.imageName)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const content = (
|
||||
<Trans
|
||||
i18nKey="please_keep_an_eye_out_for_issues"
|
||||
components={[
|
||||
<a href="https://forms.gle/yD8CVm4Kop9KwShx9" />, // eslint-disable-line react/jsx-key, jsx-a11y/anchor-has-content
|
||||
<a href="https://docs.overleaf.com/getting-started/recompiling-your-project/selecting-a-tex-live-version-and-latex-compiler" />, // eslint-disable-line react/jsx-key, jsx-a11y/anchor-has-content
|
||||
]}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<OLNotification
|
||||
title={t('this_project_is_compiled_using_untested_version')}
|
||||
content={content}
|
||||
type="info"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default RollingBuildSelectedReminder
|
||||
@@ -2,6 +2,7 @@ import pLimit from 'p-limit'
|
||||
import { Change, Chunk, Snapshot, File } from 'overleaf-editor-core'
|
||||
import { RawChange, RawChunk } from 'overleaf-editor-core/lib/types'
|
||||
import { FetchError, getJSON, postJSON } from '@/infrastructure/fetch-json'
|
||||
import path from 'path-browserify'
|
||||
|
||||
const DOWNLOAD_BLOBS_CONCURRENCY = 10
|
||||
|
||||
@@ -101,21 +102,21 @@ export class ProjectSnapshot {
|
||||
|
||||
const snapshotPaths = new Set(this.snapshot.getFilePathnames())
|
||||
|
||||
const baseURLs = [
|
||||
const basePaths = [
|
||||
// relative to the root of the compile directory
|
||||
new URL('https://overleaf.invalid'),
|
||||
'/',
|
||||
]
|
||||
|
||||
if (currentPath !== '/') {
|
||||
// relative to the current directory
|
||||
baseURLs.push(new URL(currentPath, 'https://overleaf.invalid'))
|
||||
basePaths.push(currentPath)
|
||||
}
|
||||
|
||||
const extensionsToTest = ['', ...extensions]
|
||||
|
||||
for (const baseURL of baseURLs) {
|
||||
for (const basePath of basePaths) {
|
||||
for (const extension of extensionsToTest) {
|
||||
const { pathname } = new URL(`${filePath}${extension}`, baseURL)
|
||||
const pathname = path.resolve(basePath, `${filePath}${extension}`)
|
||||
const snapshotPath = pathname.substring(1) // remove leading slash
|
||||
if (snapshotPaths.has(snapshotPath)) {
|
||||
return snapshotPath
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useSendProjectListMB } from '@/features/project-list/components/project
|
||||
export default function AdminMenu({
|
||||
canDisplayAdminMenu,
|
||||
canDisplayAdminRedirect,
|
||||
canDisplayProjectUrlLookup,
|
||||
canDisplaySplitTestMenu,
|
||||
canDisplaySurveyMenu,
|
||||
canDisplayScriptLogMenu,
|
||||
@@ -14,6 +15,7 @@ export default function AdminMenu({
|
||||
DefaultNavbarMetadata,
|
||||
| 'canDisplayAdminMenu'
|
||||
| 'canDisplayAdminRedirect'
|
||||
| 'canDisplayProjectUrlLookup'
|
||||
| 'canDisplaySplitTestMenu'
|
||||
| 'canDisplaySurveyMenu'
|
||||
| 'canDisplayScriptLogMenu'
|
||||
@@ -39,11 +41,13 @@ export default function AdminMenu({
|
||||
<NavDropdownLinkItem href="/admin/user">
|
||||
Manage Users
|
||||
</NavDropdownLinkItem>
|
||||
<NavDropdownLinkItem href="/admin/project">
|
||||
Project URL lookup
|
||||
</NavDropdownLinkItem>
|
||||
</>
|
||||
) : null}
|
||||
{canDisplayProjectUrlLookup ? (
|
||||
<NavDropdownLinkItem href="/admin/project">
|
||||
Project URL lookup
|
||||
</NavDropdownLinkItem>
|
||||
) : null}
|
||||
{canDisplayAdminRedirect && adminUrl ? (
|
||||
<NavDropdownLinkItem href={adminUrl}>
|
||||
Switch to Admin
|
||||
|
||||
@@ -26,6 +26,7 @@ function DefaultNavbar(
|
||||
title,
|
||||
canDisplayAdminMenu,
|
||||
canDisplayAdminRedirect,
|
||||
canDisplayProjectUrlLookup,
|
||||
canDisplaySplitTestMenu,
|
||||
canDisplaySurveyMenu,
|
||||
canDisplayScriptLogMenu,
|
||||
@@ -112,6 +113,7 @@ function DefaultNavbar(
|
||||
<AdminMenu
|
||||
canDisplayAdminMenu={canDisplayAdminMenu}
|
||||
canDisplayAdminRedirect={canDisplayAdminRedirect}
|
||||
canDisplayProjectUrlLookup={canDisplayProjectUrlLookup}
|
||||
canDisplaySplitTestMenu={canDisplaySplitTestMenu}
|
||||
canDisplaySurveyMenu={canDisplaySurveyMenu}
|
||||
canDisplayScriptLogMenu={canDisplayScriptLogMenu}
|
||||
|
||||
@@ -8,6 +8,7 @@ export type DefaultNavbarMetadata = {
|
||||
title?: string
|
||||
canDisplayAdminMenu: boolean
|
||||
canDisplayAdminRedirect: boolean
|
||||
canDisplayProjectUrlLookup: boolean
|
||||
canDisplaySplitTestMenu: boolean
|
||||
canDisplaySurveyMenu: boolean
|
||||
canDisplayScriptLogMenu: boolean
|
||||
|
||||
10
services/web/frontend/js/shared/utils/rolling-build.ts
Normal file
10
services/web/frontend/js/shared/utils/rolling-build.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import getMeta from '@/utils/meta'
|
||||
const images = getMeta('ol-imageNames') || []
|
||||
|
||||
const rollingImages = images
|
||||
.filter(img => img.rolling)
|
||||
.map(img => img.imageName)
|
||||
|
||||
export function onRollingBuild(imageName: string | undefined) {
|
||||
return Boolean(imageName && rollingImages.includes(imageName))
|
||||
}
|
||||
@@ -1685,6 +1685,7 @@
|
||||
"please_enter_confirmation_code": "Please enter your confirmation code",
|
||||
"please_enter_email": "Please enter your email address",
|
||||
"please_get_in_touch": "Please get in touch",
|
||||
"please_keep_an_eye_out_for_issues": "Please keep an eye out for any issues with TeX Live, packages, or compilation, and <0>provide feedback here</0>. If you encounter problems, you can <1>revert your project</1> to a stable TeX Live version.",
|
||||
"please_link_before_making_primary": "Please confirm your email by linking to your institutional account before making it the primary email.",
|
||||
"please_provide_a_message": "Please provide a message",
|
||||
"please_provide_a_subject": "Please provide a subject",
|
||||
@@ -2389,6 +2390,7 @@
|
||||
"this_project_exceeded_collaborator_limit": "This project exceeded the collaborator limit for your plan. All other users now have view-only access.",
|
||||
"this_project_exceeded_compile_timeout_limit_on_free_plan": "This project exceeded the compile timeout limit on our free plan.",
|
||||
"this_project_has_more_than_max_collabs": "This project has more than the maximum number of collaborators allowed on the project owner’s Overleaf plan. This means you could lose edit access from __linkSharingDate__.",
|
||||
"this_project_is_compiled_using_untested_version": "This project is compiled using an untested version of TeX Live",
|
||||
"this_project_is_public": "This project is public and can be edited by anyone with the URL.",
|
||||
"this_project_is_public_read_only": "This project is public and can be viewed but not edited by anyone with the URL",
|
||||
"this_project_will_appear_in_your_dropbox_folder_at": "This project will appear in your Dropbox folder at ",
|
||||
|
||||
@@ -351,6 +351,7 @@
|
||||
"mock-fs": "^5.1.2",
|
||||
"nock": "^13.5.6",
|
||||
"nvd3": "^1.8.6",
|
||||
"path-browserify": "^1.0.1",
|
||||
"p-reflect": "^3.1.0",
|
||||
"pdfjs-dist": "5.1.91",
|
||||
"pirates": "^4.0.1",
|
||||
|
||||
@@ -880,6 +880,57 @@ describe('EmailBuilder', function () {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('taxExemptCertificateRequired', function () {
|
||||
beforeEach(function () {
|
||||
this.emailAddress = 'customer@example.com'
|
||||
this.opts = {
|
||||
to: this.emailAddress,
|
||||
ein: '12-3456789',
|
||||
stripeCustomerId: 'cus_123456789',
|
||||
}
|
||||
this.email = this.EmailBuilder.buildEmail(
|
||||
'taxExemptCertificateRequired',
|
||||
this.opts
|
||||
)
|
||||
this.dom = cheerio.load(this.email.html)
|
||||
})
|
||||
|
||||
it('should build the email', function () {
|
||||
expect(this.email.html).to.exist
|
||||
expect(this.email.text).to.exist
|
||||
})
|
||||
|
||||
describe('HTML email', function () {
|
||||
it('should include the EIN', function () {
|
||||
expect(this.email.html).to.contain(this.opts.ein)
|
||||
})
|
||||
|
||||
it('should include the Stripe customer ID', function () {
|
||||
expect(this.email.html).to.contain(this.opts.stripeCustomerId)
|
||||
})
|
||||
|
||||
it('should include tax exemption verification text', function () {
|
||||
expect(this.email.html).to.contain('tax exempt')
|
||||
expect(this.email.html).to.contain('verification')
|
||||
})
|
||||
})
|
||||
|
||||
describe('plain text email', function () {
|
||||
it('should include the EIN', function () {
|
||||
expect(this.email.text).to.contain(this.opts.ein)
|
||||
})
|
||||
|
||||
it('should include the Stripe customer ID', function () {
|
||||
expect(this.email.text).to.contain(this.opts.stripeCustomerId)
|
||||
})
|
||||
|
||||
it('should include tax exemption verification text', function () {
|
||||
expect(this.email.text).to.contain('tax exempt')
|
||||
expect(this.email.text).to.contain('verification')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,15 +1,3 @@
|
||||
/* eslint-disable
|
||||
n/handle-callback-err,
|
||||
max-len,
|
||||
no-return-assign,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const { expect } = require('chai')
|
||||
const sinon = require('sinon')
|
||||
const assertCalledWith = sinon.assert.calledWith
|
||||
@@ -25,7 +13,7 @@ const {
|
||||
|
||||
describe('UserMembershipViewModel', function () {
|
||||
beforeEach(function () {
|
||||
this.UserGetter = { getUsers: sinon.stub() }
|
||||
this.UserGetter = { promises: { getUsers: sinon.stub() } }
|
||||
this.UserMembershipViewModel = SandboxedModule.require(modulePath, {
|
||||
requires: {
|
||||
'mongodb-legacy': { ObjectId },
|
||||
@@ -54,7 +42,7 @@ describe('UserMembershipViewModel', function () {
|
||||
describe('build', function () {
|
||||
it('build email', function () {
|
||||
const viewModel = this.UserMembershipViewModel.build(this.email)
|
||||
return expect(viewModel).to.deep.equal({
|
||||
expect(viewModel).to.deep.equal({
|
||||
email: this.email,
|
||||
invite: true,
|
||||
last_active_at: null,
|
||||
@@ -83,67 +71,49 @@ describe('UserMembershipViewModel', function () {
|
||||
|
||||
describe('build async', function () {
|
||||
beforeEach(function () {
|
||||
return (this.UserMembershipViewModel.build = sinon.stub())
|
||||
this.UserMembershipViewModel.build = sinon.stub()
|
||||
})
|
||||
|
||||
it('build email', function (done) {
|
||||
this.UserGetter.getUsers.yields(null, [])
|
||||
return this.UserMembershipViewModel.buildAsync(
|
||||
[this.email],
|
||||
(error, [viewModel]) => {
|
||||
assertCalledWith(this.UserMembershipViewModel.build, this.email)
|
||||
return done()
|
||||
}
|
||||
)
|
||||
it('build email', async function () {
|
||||
this.UserGetter.promises.getUsers.resolves([])
|
||||
await this.UserMembershipViewModel.buildAsync([this.email])
|
||||
assertCalledWith(this.UserMembershipViewModel.build, this.email)
|
||||
})
|
||||
|
||||
it('build user', function (done) {
|
||||
this.UserGetter.getUsers.yields(null, [])
|
||||
return this.UserMembershipViewModel.buildAsync(
|
||||
[this.user],
|
||||
(error, [viewModel]) => {
|
||||
assertCalledWith(this.UserMembershipViewModel.build, this.user)
|
||||
return done()
|
||||
}
|
||||
)
|
||||
it('build user', async function () {
|
||||
this.UserGetter.promises.getUsers.resolves([])
|
||||
await this.UserMembershipViewModel.buildAsync([this.user])
|
||||
assertCalledWith(this.UserMembershipViewModel.build, this.user)
|
||||
})
|
||||
|
||||
it('build user id', function (done) {
|
||||
it('build user id', async function () {
|
||||
const user = {
|
||||
...this.user,
|
||||
_id: new ObjectId(),
|
||||
}
|
||||
this.UserGetter.getUsers.yields(null, [user])
|
||||
return this.UserMembershipViewModel.buildAsync(
|
||||
[user._id],
|
||||
(error, [viewModel]) => {
|
||||
expect(error).not.to.exist
|
||||
assertNotCalled(this.UserMembershipViewModel.build)
|
||||
expect(viewModel._id.toString()).to.equal(user._id.toString())
|
||||
expect(viewModel.email).to.equal(user.email)
|
||||
expect(viewModel.first_name).to.equal(user.first_name)
|
||||
expect(viewModel.invite).to.equal(false)
|
||||
expect(viewModel.email).to.exist
|
||||
expect(viewModel.enrollment).to.exist
|
||||
expect(viewModel.enrollment).to.deep.equal(user.enrollment)
|
||||
return done()
|
||||
}
|
||||
)
|
||||
this.UserGetter.promises.getUsers.resolves([user])
|
||||
const [viewModel] = await this.UserMembershipViewModel.buildAsync([
|
||||
user._id,
|
||||
])
|
||||
assertNotCalled(this.UserMembershipViewModel.build)
|
||||
expect(viewModel._id.toString()).to.equal(user._id.toString())
|
||||
expect(viewModel.email).to.equal(user.email)
|
||||
expect(viewModel.first_name).to.equal(user.first_name)
|
||||
expect(viewModel.invite).to.equal(false)
|
||||
expect(viewModel.email).to.exist
|
||||
expect(viewModel.enrollment).to.exist
|
||||
expect(viewModel.enrollment).to.deep.equal(user.enrollment)
|
||||
})
|
||||
|
||||
it('build user id with error', function (done) {
|
||||
this.UserGetter.getUsers.yields(new Error('nope'), [])
|
||||
it('build user id with error', async function () {
|
||||
this.UserGetter.promises.getUsers.rejects(new Error('nope'))
|
||||
const userId = new ObjectId()
|
||||
return this.UserMembershipViewModel.buildAsync(
|
||||
[userId],
|
||||
(error, [viewModel]) => {
|
||||
expect(error).not.to.exist
|
||||
assertNotCalled(this.UserMembershipViewModel.build)
|
||||
expect(viewModel._id).to.equal(userId.toString())
|
||||
expect(viewModel.email).not.to.exist
|
||||
return done()
|
||||
}
|
||||
)
|
||||
const [viewModel] = await this.UserMembershipViewModel.buildAsync([
|
||||
userId,
|
||||
])
|
||||
assertNotCalled(this.UserMembershipViewModel.build)
|
||||
expect(viewModel._id).to.equal(userId.toString())
|
||||
expect(viewModel.email).not.to.exist
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user