mirror of
https://github.com/overleaf/overleaf.git
synced 2024-10-17 21:05:04 -04:00
Default LaTeX beginners to the Visual Editor (#18917)
* open visual code if user havent used latex before * test tooltip on code editor switch * firstTimeLoadedEditor * track editor.codeEditorOpened value * lastEditorLoadedDate * odc data loaded from mongo * fix a typo * use tutorial to check if it was dissmised * use getInactiveTutorials fn * fix test * check if code editor was opened * added translations * pass classname to tooltip * use signUpDate instead of lastEditorLoadedDate * refactor visual fallback value * use tutorial completed data only for tooltip * set lastUsedMode in odc form * safer usedLatex check * getOnboardingDataValue helper function * move tooltip to a separate component * move classname to tooltipProps * usedLatex in meta tag * codeEdtiorOpened fallback value * fix release date year * fix 24 hours criteria for showing the tooltip * fix tests * hide tooltip when code editor is opened * remove setting lastUsedMode in ODC form * remove empty comment * change date for checking signUpDate * fix linting error GitOrigin-RevId: 0a57ba3f4717492d4546633571117f667d3a05f8
This commit is contained in:
parent
8cba7935b7
commit
0766c91079
13 changed files with 214 additions and 9 deletions
|
@ -1,16 +1,29 @@
|
||||||
const {
|
const {
|
||||||
OnboardingDataCollection,
|
OnboardingDataCollection,
|
||||||
|
OnboardingDataCollectionSchema,
|
||||||
} = require('../../models/OnboardingDataCollection')
|
} = require('../../models/OnboardingDataCollection')
|
||||||
const OError = require('@overleaf/o-error')
|
const OError = require('@overleaf/o-error')
|
||||||
|
|
||||||
async function getOnboardingDataCollection(userId) {
|
async function getOnboardingDataCollection(userId, projection = {}) {
|
||||||
try {
|
try {
|
||||||
return await OnboardingDataCollection.findOne({ _id: userId }).exec()
|
return await OnboardingDataCollection.findOne(
|
||||||
|
{ _id: userId },
|
||||||
|
projection
|
||||||
|
).exec()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw OError.tag(error, 'Failed to get OnboardingDataCollection')
|
throw OError.tag(error, 'Failed to get OnboardingDataCollection')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getOnboardingDataValue(userId, key) {
|
||||||
|
if (!OnboardingDataCollectionSchema.paths[key]) {
|
||||||
|
throw new Error(`${key} is not a valid onboarding data key`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await getOnboardingDataCollection(userId, { [key]: 1 })
|
||||||
|
return result ? result[key] : null
|
||||||
|
}
|
||||||
|
|
||||||
async function upsertOnboardingDataCollection({
|
async function upsertOnboardingDataCollection({
|
||||||
userId,
|
userId,
|
||||||
firstName,
|
firstName,
|
||||||
|
@ -60,4 +73,5 @@ module.exports = {
|
||||||
getOnboardingDataCollection,
|
getOnboardingDataCollection,
|
||||||
upsertOnboardingDataCollection,
|
upsertOnboardingDataCollection,
|
||||||
deleteOnboardingDataCollection,
|
deleteOnboardingDataCollection,
|
||||||
|
getOnboardingDataValue,
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,7 @@ const ProjectAuditLogHandler = require('./ProjectAuditLogHandler')
|
||||||
const PublicAccessLevels = require('../Authorization/PublicAccessLevels')
|
const PublicAccessLevels = require('../Authorization/PublicAccessLevels')
|
||||||
const TagsHandler = require('../Tags/TagsHandler')
|
const TagsHandler = require('../Tags/TagsHandler')
|
||||||
const TutorialHandler = require('../Tutorial/TutorialHandler')
|
const TutorialHandler = require('../Tutorial/TutorialHandler')
|
||||||
|
const OnboardingDataCollectionManager = require('../OnboardingDataCollection/OnboardingDataCollectionManager')
|
||||||
const UserUpdater = require('../User/UserUpdater')
|
const UserUpdater = require('../User/UserUpdater')
|
||||||
const Modules = require('../../infrastructure/Modules')
|
const Modules = require('../../infrastructure/Modules')
|
||||||
const UserGetter = require('../User/UserGetter')
|
const UserGetter = require('../User/UserGetter')
|
||||||
|
@ -381,6 +382,13 @@ const _ProjectController = {
|
||||||
userId,
|
userId,
|
||||||
projectId
|
projectId
|
||||||
),
|
),
|
||||||
|
usedLatex: OnboardingDataCollectionManager.getOnboardingDataValue(
|
||||||
|
userId,
|
||||||
|
'usedLatex'
|
||||||
|
).catch(err => {
|
||||||
|
logger.error({ err, userId })
|
||||||
|
return null
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
const splitTestAssignments = {}
|
const splitTestAssignments = {}
|
||||||
|
@ -428,6 +436,7 @@ const _ProjectController = {
|
||||||
subscription,
|
subscription,
|
||||||
isTokenMember,
|
isTokenMember,
|
||||||
isInvitedMember,
|
isInvitedMember,
|
||||||
|
usedLatex,
|
||||||
} = userValues
|
} = userValues
|
||||||
|
|
||||||
// check if a user is not in the writefull-oauth-promotion, in which case they may be part of the auto trial group
|
// check if a user is not in the writefull-oauth-promotion, in which case they may be part of the auto trial group
|
||||||
|
@ -724,6 +733,7 @@ const _ProjectController = {
|
||||||
hasTrackChangesFeature: Features.hasFeature('track-changes'),
|
hasTrackChangesFeature: Features.hasFeature('track-changes'),
|
||||||
projectTags,
|
projectTags,
|
||||||
linkSharingWarning: linkSharingChanges.variant === 'active',
|
linkSharingWarning: linkSharingChanges.variant === 'active',
|
||||||
|
usedLatex,
|
||||||
})
|
})
|
||||||
timer.done()
|
timer.done()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -8,6 +8,7 @@ const VALID_KEYS = [
|
||||||
'writefull-oauth-promotion',
|
'writefull-oauth-promotion',
|
||||||
'bib-file-tpr-prompt',
|
'bib-file-tpr-prompt',
|
||||||
'ai-error-assistant-consent',
|
'ai-error-assistant-consent',
|
||||||
|
'code-editor-mode-prompt',
|
||||||
]
|
]
|
||||||
|
|
||||||
async function completeTutorial(req, res, next) {
|
async function completeTutorial(req, res, next) {
|
||||||
|
|
|
@ -37,6 +37,7 @@ meta(name="ol-hasTrackChangesFeature", data-type="boolean" content=hasTrackChang
|
||||||
meta(name="ol-inactiveTutorials", data-type="json" content=user.inactiveTutorials)
|
meta(name="ol-inactiveTutorials", data-type="json" content=user.inactiveTutorials)
|
||||||
meta(name="ol-projectTags" data-type="json" content=projectTags)
|
meta(name="ol-projectTags" data-type="json" content=projectTags)
|
||||||
meta(name="ol-linkSharingWarning" data-type="boolean" content=linkSharingWarning)
|
meta(name="ol-linkSharingWarning" data-type="boolean" content=linkSharingWarning)
|
||||||
|
meta(name="ol-usedLatex" data-type="string" content=usedLatex)
|
||||||
|
|
||||||
// translations for the loading page, before i18n has loaded in the client
|
// translations for the loading page, before i18n has loaded in the client
|
||||||
meta(name="ol-loadingText", data-type="string" content=translate("loading"))
|
meta(name="ol-loadingText", data-type="string" content=translate("loading"))
|
||||||
|
|
|
@ -191,6 +191,8 @@
|
||||||
"code_check_failed": "",
|
"code_check_failed": "",
|
||||||
"code_check_failed_explanation": "",
|
"code_check_failed_explanation": "",
|
||||||
"code_editor": "",
|
"code_editor": "",
|
||||||
|
"code_editor_tooltip_message": "",
|
||||||
|
"code_editor_tooltip_title": "",
|
||||||
"collaborate_online_and_offline": "",
|
"collaborate_online_and_offline": "",
|
||||||
"collabs_per_proj": "",
|
"collabs_per_proj": "",
|
||||||
"collabs_per_proj_single": "",
|
"collabs_per_proj_single": "",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { ReactScopeValueStore } from '@/features/ide-react/scope-value-store/react-scope-value-store'
|
import { ReactScopeValueStore } from '@/features/ide-react/scope-value-store/react-scope-value-store'
|
||||||
import customLocalStorage from '@/infrastructure/local-storage'
|
import customLocalStorage from '@/infrastructure/local-storage'
|
||||||
|
import getMeta from '@/utils/meta'
|
||||||
|
|
||||||
export function populateEditorScope(
|
export function populateEditorScope(
|
||||||
store: ReactScopeValueStore,
|
store: ReactScopeValueStore,
|
||||||
|
@ -26,13 +27,24 @@ export function populateEditorScope(
|
||||||
})
|
})
|
||||||
store.persisted(
|
store.persisted(
|
||||||
'editor.showVisual',
|
'editor.showVisual',
|
||||||
showVisualFallbackValue(projectId),
|
getMeta('ol-usedLatex') === 'never' || showVisualFallbackValue(projectId),
|
||||||
`editor.lastUsedMode`,
|
`editor.lastUsedMode`,
|
||||||
{
|
{
|
||||||
toPersisted: showVisual => (showVisual ? 'visual' : 'code'),
|
toPersisted: showVisual => (showVisual ? 'visual' : 'code'),
|
||||||
fromPersisted: mode => mode === 'visual',
|
fromPersisted: mode => mode === 'visual',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
store.persisted(
|
||||||
|
'editor.codeEditorOpened',
|
||||||
|
codeEditorOpenedFallbackValue(),
|
||||||
|
'editor.codeEditorOpened'
|
||||||
|
)
|
||||||
|
store.watch('editor.showVisual', showVisual => {
|
||||||
|
if (store.get('editor.codeEditorOpened') !== true && showVisual === false) {
|
||||||
|
store.set('editor.codeEditorOpened', true)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function showVisualFallbackValue(projectId: string) {
|
function showVisualFallbackValue(projectId: string) {
|
||||||
|
@ -46,3 +58,16 @@ function showVisualFallbackValue(projectId: string) {
|
||||||
|
|
||||||
return editorModeVal === 'rich-text'
|
return editorModeVal === 'rich-text'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function codeEditorOpenedFallbackValue() {
|
||||||
|
const signUpDate = getMeta('ol-user').signUpDate
|
||||||
|
if (
|
||||||
|
typeof signUpDate === 'string' &&
|
||||||
|
new Date(signUpDate) < new Date('2024-08-02')
|
||||||
|
) {
|
||||||
|
// if signUpDate is before releasing "codeEditorOpened" value
|
||||||
|
// it is assumed that the user has opened the code editor at some point
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,118 @@
|
||||||
|
import { ReactElement, useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
import Tooltip from '../../../shared/components/tooltip'
|
||||||
|
import Close from '@/shared/components/close'
|
||||||
|
import { useEditorContext } from '@/shared/context/editor-context'
|
||||||
|
import useTutorial from '@/shared/hooks/promotions/use-tutorial'
|
||||||
|
import { useUserContext } from '@/shared/context/user-context'
|
||||||
|
import useScopeValue from '@/shared/hooks/use-scope-value'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import getMeta from '@/utils/meta'
|
||||||
|
|
||||||
|
const CODE_EDITOR_TOOLTIP_TIMEOUT = 1000
|
||||||
|
export const codeEditorModePrompt = 'code-editor-mode-prompt'
|
||||||
|
|
||||||
|
export const EditorSwitchBeginnerTooltip = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactElement
|
||||||
|
}) => {
|
||||||
|
const toolbarRef = useRef<any>(null)
|
||||||
|
const user = useUserContext()
|
||||||
|
const { inactiveTutorials } = useEditorContext()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [codeEditorOpened] = useScopeValue('editor.codeEditorOpened')
|
||||||
|
const { completeTutorial } = useTutorial(codeEditorModePrompt, {
|
||||||
|
location: 'logs',
|
||||||
|
name: codeEditorModePrompt,
|
||||||
|
})
|
||||||
|
const [tooltipShown, setTooltipShown] = useState(false)
|
||||||
|
|
||||||
|
const shouldShowCodeEditorTooltip = useCallback(() => {
|
||||||
|
if (inactiveTutorials.includes(codeEditorModePrompt)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getMeta('ol-usedLatex') !== 'never') {
|
||||||
|
// only show tooltip to the users that never used LaTeX (submitted in onboarding data collection)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (codeEditorOpened) {
|
||||||
|
// dont show tooltip if code editor was opened at some point
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const msSinceSignedUp =
|
||||||
|
user.signUpDate && Date.now() - new Date(user.signUpDate).getTime()
|
||||||
|
|
||||||
|
if (msSinceSignedUp && msSinceSignedUp < 24 * 60 * 60 * 1000) {
|
||||||
|
// dont show tooltip if user has signed up is less than 24 hours
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}, [codeEditorOpened, inactiveTutorials, user.signUpDate])
|
||||||
|
|
||||||
|
const showCodeEditorTooltip = useCallback(() => {
|
||||||
|
if (toolbarRef.current && 'show' in toolbarRef.current) {
|
||||||
|
toolbarRef.current.show()
|
||||||
|
setTooltipShown(true)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const hideCodeEditorTooltip = useCallback(() => {
|
||||||
|
if (toolbarRef.current && 'hide' in toolbarRef.current) {
|
||||||
|
toolbarRef.current.hide()
|
||||||
|
setTooltipShown(false)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (tooltipShown && codeEditorOpened) {
|
||||||
|
hideCodeEditorTooltip()
|
||||||
|
}
|
||||||
|
}, [codeEditorOpened, hideCodeEditorTooltip, tooltipShown])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
if (shouldShowCodeEditorTooltip()) {
|
||||||
|
showCodeEditorTooltip()
|
||||||
|
}
|
||||||
|
}, CODE_EDITOR_TOOLTIP_TIMEOUT)
|
||||||
|
|
||||||
|
return () => clearTimeout(timeout)
|
||||||
|
}, [showCodeEditorTooltip, shouldShowCodeEditorTooltip])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
id="editor-switch-tooltip"
|
||||||
|
description={
|
||||||
|
<div>
|
||||||
|
<Close
|
||||||
|
variant="dark"
|
||||||
|
onDismiss={() => {
|
||||||
|
hideCodeEditorTooltip()
|
||||||
|
completeTutorial({ event: 'promo-click', action: 'complete' })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="tooltip-title">{t('code_editor_tooltip_title')}</div>
|
||||||
|
<div>{t('code_editor_tooltip_message')}</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
tooltipProps={{
|
||||||
|
className: 'editor-switch-tooltip',
|
||||||
|
}}
|
||||||
|
overlayProps={{
|
||||||
|
ref: toolbarRef,
|
||||||
|
placement: 'bottom',
|
||||||
|
shouldUpdatePosition: true,
|
||||||
|
// @ts-ignore
|
||||||
|
// trigger: null is used to prevent the tooltip from showing on hover
|
||||||
|
// but it is not allowed in the type definition
|
||||||
|
trigger: null,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,16 +1,26 @@
|
||||||
import { ChangeEvent, FC, memo, useCallback } from 'react'
|
import { ChangeEvent, FC, memo, useCallback } from 'react'
|
||||||
import useScopeValue from '../../../shared/hooks/use-scope-value'
|
import useScopeValue from '@/shared/hooks/use-scope-value'
|
||||||
import Tooltip from '../../../shared/components/tooltip'
|
import Tooltip from '@/shared/components/tooltip'
|
||||||
|
import useTutorial from '@/shared/hooks/promotions/use-tutorial'
|
||||||
import { sendMB } from '../../../infrastructure/event-tracking'
|
import { sendMB } from '../../../infrastructure/event-tracking'
|
||||||
import isValidTeXFile from '../../../main/is-valid-tex-file'
|
import isValidTeXFile from '../../../main/is-valid-tex-file'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import {
|
||||||
|
EditorSwitchBeginnerTooltip,
|
||||||
|
codeEditorModePrompt,
|
||||||
|
} from './editor-switch-beginner-tooltip'
|
||||||
|
|
||||||
function EditorSwitch() {
|
function EditorSwitch() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [visual, setVisual] = useScopeValue('editor.showVisual')
|
const [visual, setVisual] = useScopeValue('editor.showVisual')
|
||||||
const [docName] = useScopeValue('editor.open_doc_name')
|
const [docName] = useScopeValue('editor.open_doc_name')
|
||||||
|
const [codeEditorOpened] = useScopeValue('editor.codeEditorOpened')
|
||||||
|
|
||||||
const richTextAvailable = isValidTeXFile(docName)
|
const richTextAvailable = isValidTeXFile(docName)
|
||||||
|
const { completeTutorial } = useTutorial(codeEditorModePrompt, {
|
||||||
|
location: 'logs',
|
||||||
|
name: codeEditorModePrompt,
|
||||||
|
})
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
event => {
|
event => {
|
||||||
|
@ -19,6 +29,9 @@ function EditorSwitch() {
|
||||||
switch (editorType) {
|
switch (editorType) {
|
||||||
case 'cm6':
|
case 'cm6':
|
||||||
setVisual(false)
|
setVisual(false)
|
||||||
|
if (!codeEditorOpened) {
|
||||||
|
completeTutorial({ event: 'promo-click', action: 'complete' })
|
||||||
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'rich-text':
|
case 'rich-text':
|
||||||
|
@ -28,7 +41,7 @@ function EditorSwitch() {
|
||||||
|
|
||||||
sendMB('editor-switch-change', { editorType })
|
sendMB('editor-switch-change', { editorType })
|
||||||
},
|
},
|
||||||
[setVisual]
|
[codeEditorOpened, completeTutorial, setVisual]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -45,9 +58,11 @@ function EditorSwitch() {
|
||||||
checked={!richTextAvailable || !visual}
|
checked={!richTextAvailable || !visual}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
<EditorSwitchBeginnerTooltip>
|
||||||
<label htmlFor="editor-switch-cm6" className="toggle-switch-label">
|
<label htmlFor="editor-switch-cm6" className="toggle-switch-label">
|
||||||
<span>{t('code_editor')}</span>
|
<span>{t('code_editor')}</span>
|
||||||
</label>
|
</label>
|
||||||
|
</EditorSwitchBeginnerTooltip>
|
||||||
|
|
||||||
<RichTextToggle
|
<RichTextToggle
|
||||||
checked={richTextAvailable && visual}
|
checked={richTextAvailable && visual}
|
||||||
|
|
|
@ -196,6 +196,7 @@ export interface Meta {
|
||||||
'ol-translationMaintenance': string
|
'ol-translationMaintenance': string
|
||||||
'ol-translationUnableToJoin': string
|
'ol-translationUnableToJoin': string
|
||||||
'ol-useShareJsHash': boolean
|
'ol-useShareJsHash': boolean
|
||||||
|
'ol-usedLatex': 'never' | 'occasionally' | 'often' | undefined
|
||||||
'ol-user': User
|
'ol-user': User
|
||||||
'ol-userAffiliations': Affiliation[]
|
'ol-userAffiliations': Affiliation[]
|
||||||
'ol-userEmails': UserEmailData[]
|
'ol-userEmails': UserEmailData[]
|
||||||
|
|
|
@ -337,6 +337,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editor-switch-tooltip .tooltip-inner {
|
||||||
|
text-align: left;
|
||||||
|
max-width: 325px;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
.tooltip-title {
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
/**************************************
|
/**************************************
|
||||||
Formatting buttons
|
Formatting buttons
|
||||||
***************************************/
|
***************************************/
|
||||||
|
|
|
@ -277,6 +277,8 @@
|
||||||
"code_check_failed": "Code check failed",
|
"code_check_failed": "Code check failed",
|
||||||
"code_check_failed_explanation": "Your code has errors that need to be fixed before the auto-compile can run",
|
"code_check_failed_explanation": "Your code has errors that need to be fixed before the auto-compile can run",
|
||||||
"code_editor": "Code Editor",
|
"code_editor": "Code Editor",
|
||||||
|
"code_editor_tooltip_message": "You can see the code behind your project (and make edits to it) in the Code Editor",
|
||||||
|
"code_editor_tooltip_title": "Want to view and edit the LaTeX code?",
|
||||||
"collaborate_easily_on_your_projects": "Collaborate easily on your projects. Work on longer or more complex docs.",
|
"collaborate_easily_on_your_projects": "Collaborate easily on your projects. Work on longer or more complex docs.",
|
||||||
"collaborate_online_and_offline": "Collaborate online and offline, using your own workflow",
|
"collaborate_online_and_offline": "Collaborate online and offline, using your own workflow",
|
||||||
"collaboration": "Collaboration",
|
"collaboration": "Collaboration",
|
||||||
|
|
|
@ -197,6 +197,9 @@ describe('ProjectController', function () {
|
||||||
this.TutorialHandler = {
|
this.TutorialHandler = {
|
||||||
getInactiveTutorials: sinon.stub().returns([]),
|
getInactiveTutorials: sinon.stub().returns([]),
|
||||||
}
|
}
|
||||||
|
this.OnboardingDataCollectionManager = {
|
||||||
|
getOnboardingDataValue: sinon.stub().resolves(null),
|
||||||
|
}
|
||||||
|
|
||||||
this.ProjectController = SandboxedModule.require(MODULE_PATH, {
|
this.ProjectController = SandboxedModule.require(MODULE_PATH, {
|
||||||
requires: {
|
requires: {
|
||||||
|
@ -245,6 +248,8 @@ describe('ProjectController', function () {
|
||||||
'../Survey/SurveyHandler': this.SurveyHandler,
|
'../Survey/SurveyHandler': this.SurveyHandler,
|
||||||
'./ProjectAuditLogHandler': this.ProjectAuditLogHandler,
|
'./ProjectAuditLogHandler': this.ProjectAuditLogHandler,
|
||||||
'../Tutorial/TutorialHandler': this.TutorialHandler,
|
'../Tutorial/TutorialHandler': this.TutorialHandler,
|
||||||
|
'../OnboardingDataCollection/OnboardingDataCollectionManager':
|
||||||
|
this.OnboardingDataCollectionManager,
|
||||||
'../User/UserUpdater': {
|
'../User/UserUpdater': {
|
||||||
promises: {
|
promises: {
|
||||||
updateUser: sinon.stub().resolves(),
|
updateUser: sinon.stub().resolves(),
|
||||||
|
|
|
@ -35,6 +35,7 @@ export type User = {
|
||||||
alphaProgram?: boolean
|
alphaProgram?: boolean
|
||||||
betaProgram?: boolean
|
betaProgram?: boolean
|
||||||
labsProgram?: boolean
|
labsProgram?: boolean
|
||||||
|
isLatexBeginner?: boolean
|
||||||
signUpDate?: string // date string
|
signUpDate?: string // date string
|
||||||
features?: Features
|
features?: Features
|
||||||
refProviders?: RefProviders
|
refProviders?: RefProviders
|
||||||
|
|
Loading…
Reference in a new issue