mirror of
https://github.com/overleaf/overleaf.git
synced 2025-12-05 01:10:29 +00:00
Compare commits
21 Commits
18fc51bfa4
...
3a206074f2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a206074f2 | ||
|
|
046e74a2c6 | ||
|
|
b0d05c0cf0 | ||
|
|
bfed1ac2ae | ||
|
|
49667a605b | ||
|
|
530020467d | ||
|
|
e8c829deab | ||
|
|
0036ad5f31 | ||
|
|
766a7c6a37 | ||
|
|
448ec1a273 | ||
|
|
ddb037680b | ||
|
|
19f1f6f702 | ||
|
|
87d8e142cc | ||
|
|
cfe6c3ceeb | ||
|
|
d0ba35ab8f | ||
|
|
c2e0c40808 | ||
|
|
698a6013de | ||
|
|
cb0cfcfd82 | ||
|
|
38edeca871 | ||
|
|
fea346f4a6 | ||
|
|
dd1f55e0dd |
2
package-lock.json
generated
2
package-lock.json
generated
@@ -53178,7 +53178,6 @@
|
||||
"bull": "^3.18.0",
|
||||
"bunyan": "^1.8.15",
|
||||
"cache-flow": "^1.9.0",
|
||||
"celebrate": "^15.0.3",
|
||||
"connect-redis": "^6.1.3",
|
||||
"content-disposition": "^0.5.0",
|
||||
"contentful": "^10.8.5",
|
||||
@@ -53201,7 +53200,6 @@
|
||||
"i18next": "^23.10.0",
|
||||
"i18next-fs-backend": "^2.3.1",
|
||||
"i18next-http-middleware": "^3.5.0",
|
||||
"joi": "^17.12.0",
|
||||
"jose": "^4.3.8",
|
||||
"json2csv": "^4.3.3",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
|
||||
@@ -10,7 +10,7 @@ ENV TEXMFVAR=/var/lib/overleaf/tmp/texmf-var
|
||||
|
||||
# Update to ensure dependencies are updated
|
||||
# ------------------------------------------
|
||||
ENV REBUILT_AFTER="2025-10-22"
|
||||
ENV REBUILT_AFTER="2025-10-30"
|
||||
|
||||
# Install dependencies
|
||||
# --------------------
|
||||
|
||||
9
services/web/Jenkinsfile
vendored
9
services/web/Jenkinsfile
vendored
@@ -279,7 +279,7 @@ pipeline {
|
||||
stages {
|
||||
stage('Wait a bit to give tests all the CPU capacity') {
|
||||
steps {
|
||||
sh 'sleep 90'
|
||||
sh 'sleep 60'
|
||||
}
|
||||
}
|
||||
stage('Build Webpack') {
|
||||
@@ -330,6 +330,13 @@ pipeline {
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Push Production image early') {
|
||||
steps {
|
||||
dir('services/web') {
|
||||
sh 'make push_scratch'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,21 +183,26 @@ test_writefull:
|
||||
build_test_frontend_ct:
|
||||
docker run --rm --volume /dev/shm:/dev/shm --user root $(IMAGE_CI) bash -ec 'for path in /overleaf/services/web/cypress/results /overleaf/services/web/node_modules/.cache; do mkdir -p $$path; chown -R node:node $$path; done && tar -cC / overleaf | tar -xC /dev/shm'
|
||||
|
||||
test_frontend_ct_core_other: export CYPRESS_DOWNLOADS=./cypress/downloads/core
|
||||
test_frontend_ct_core_other: export CYPRESS_RESULTS=./cypress/results/core
|
||||
test_frontend_ct_core_other: export CYPRESS_SPEC_PATTERN=./test/frontend/**/*.spec.{js,jsx,ts,tsx}
|
||||
test_frontend_ct_core_other: export CYPRESS_EXCLUDE_SPEC_PATTERN=./test/frontend/features/**/*.spec.{js,jsx,ts,tsx}
|
||||
|
||||
test_frontend_ct_core_features: export CYPRESS_DOWNLOADS=./cypress/downloads/core
|
||||
test_frontend_ct_core_features: export CYPRESS_RESULTS=./cypress/results/core
|
||||
test_frontend_ct_core_features: export CYPRESS_SPEC_PATTERN=./test/frontend/features/**/*.spec.{js,jsx,ts,tsx}
|
||||
test_frontend_ct_core_features: export CYPRESS_EXCLUDE_SPEC_PATTERN=./test/frontend/features/source-editor/**/*.spec.{js,jsx,ts,tsx}
|
||||
|
||||
test_frontend_ct_modules: export CYPRESS_DOWNLOADS=./cypress/downloads/modules
|
||||
test_frontend_ct_modules: export CYPRESS_RESULTS=./cypress/results/modules
|
||||
test_frontend_ct_modules: export CYPRESS_SPEC_PATTERN=./modules/**/test/frontend/**/*.spec.{js,jsx,ts,tsx}
|
||||
|
||||
test_frontend_ct_editor_other: export CYPRESS_DOWNLOADS=./cypress/downloads/editor_other
|
||||
test_frontend_ct_editor_other: export CYPRESS_RESULTS=./cypress/results/editor_other
|
||||
test_frontend_ct_editor_other: export CYPRESS_SPEC_PATTERN=./test/frontend/features/source-editor/**/*.spec.{js,jsx,ts,tsx}
|
||||
test_frontend_ct_editor_other: export CYPRESS_EXCLUDE_SPEC_PATTERN=./test/frontend/features/source-editor/components/codemirror-editor-visual*.spec.{js,jsx,ts,tsx}
|
||||
|
||||
test_frontend_ct_editor_visual: export CYPRESS_DOWNLOADS=./cypress/downloads/editor_visual
|
||||
test_frontend_ct_editor_visual: export CYPRESS_RESULTS=./cypress/results/editor_visual
|
||||
test_frontend_ct_editor_visual: export CYPRESS_SPEC_PATTERN=./test/frontend/features/source-editor/components/codemirror-editor-visual*.spec.{js,jsx,ts,tsx}
|
||||
|
||||
@@ -547,6 +552,10 @@ shellcheck_fix:
|
||||
IMAGE_CI ?= ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER)
|
||||
IMAGE_REPO ?= us-east1-docker.pkg.dev/overleaf-ops/ol-docker/$(PROJECT_NAME)
|
||||
IMAGE_REPO_FINAL ?= $(IMAGE_REPO):$(BRANCH_NAME)-$(BUILD_NUMBER)
|
||||
# A shared scratch tag that is used by all the pipelines for pushing the layers of the production image ASAP from a parallel step.
|
||||
# We do not want to make the image available before all the tests have passed.
|
||||
# Using a single tag avoids generating cruft in our docker repository / AR.
|
||||
IMAGE_SCRATCH ?= $(IMAGE_REPO):do-not-use-this-tag-for-deploys--it-is-used-for-early-pushes-in-ci
|
||||
IMAGE_CACHE ?= $(IMAGE_REPO):cache-$(shell cat \
|
||||
$(MONOREPO)/package.json \
|
||||
$(MONOREPO)/package-lock.json \
|
||||
@@ -613,6 +622,7 @@ build:
|
||||
docker build \
|
||||
--build-arg SENTRY_RELEASE \
|
||||
--tag $(IMAGE_REPO_FINAL) \
|
||||
--tag $(IMAGE_SCRATCH) \
|
||||
--target app \
|
||||
--file Dockerfile \
|
||||
../..
|
||||
@@ -623,6 +633,9 @@ publish:
|
||||
push_branch:
|
||||
docker push $(IMAGE_CACHE)
|
||||
|
||||
push_scratch:
|
||||
docker push $(IMAGE_SCRATCH)
|
||||
|
||||
SENTRY_IMAGE=getsentry/sentry-cli:2.16.1
|
||||
sentry_prefetch:
|
||||
docker pull $(SENTRY_IMAGE)
|
||||
|
||||
@@ -52,6 +52,8 @@ function getNewCompileBackendClass(projectId, compileBackendClass) {
|
||||
return 'n4'
|
||||
case 'c2d':
|
||||
return 'n4'
|
||||
case 'c4d':
|
||||
return 'n4'
|
||||
default:
|
||||
throw new Error('unknown ?compileBackendClass')
|
||||
}
|
||||
|
||||
@@ -279,12 +279,7 @@ const _CompileController = {
|
||||
status,
|
||||
compileTime: timings?.compileE2E,
|
||||
timeout: limits.timeout,
|
||||
server:
|
||||
clsiServerId?.includes('-c2d-') ||
|
||||
clsiServerId?.includes('-c3d-') ||
|
||||
clsiServerId?.includes('-c4d-')
|
||||
? 'faster'
|
||||
: 'normal',
|
||||
server: clsiServerId?.includes('-c4d-') ? 'faster' : 'normal',
|
||||
clsiServerId,
|
||||
isAutoCompile,
|
||||
isInitialCompile: stats?.isInitialCompile === 1,
|
||||
|
||||
@@ -135,7 +135,7 @@ async function _getProjectCompileLimits(project) {
|
||||
timeout:
|
||||
ownerFeatures.compileTimeout || Settings.defaultFeatures.compileTimeout,
|
||||
compileGroup,
|
||||
compileBackendClass: compileGroup === 'standard' ? 'n2d' : 'c2d',
|
||||
compileBackendClass: compileGroup === 'standard' ? 'n2d' : 'c4d',
|
||||
ownerAnalyticsId: analyticsId,
|
||||
}
|
||||
return limits
|
||||
|
||||
@@ -523,6 +523,12 @@ async function previewAddonPurchase(req, res) {
|
||||
'/user/subscription?redirect-reason=ai-assist-unavailable'
|
||||
)
|
||||
}
|
||||
if (
|
||||
err instanceof Error &&
|
||||
err.constructor.name === 'PaymentServiceResourceNotFoundError'
|
||||
) {
|
||||
return res.redirect('/user/subscription/plans#ai-assist')
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
||||
@@ -543,6 +549,12 @@ async function previewAddonPurchase(req, res) {
|
||||
if (err instanceof DuplicateAddOnError) {
|
||||
return res.redirect('/user/subscription?redirect-reason=double-buy')
|
||||
}
|
||||
if (
|
||||
err instanceof Error &&
|
||||
err.constructor.name === 'PaymentServiceResourceNotFoundError'
|
||||
) {
|
||||
return res.redirect('/user/subscription/plans#ai-assist')
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ async function createNewUser(attributes, options = {}) {
|
||||
createdAt: new Date(),
|
||||
reversedHostname,
|
||||
}
|
||||
if (Features.hasFeature('affiliations')) {
|
||||
if (Features.hasFeature('affiliations') && !options.requireAffiliation) {
|
||||
emailData.affiliationUnchecked = true
|
||||
}
|
||||
if (
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// @ts-check
|
||||
|
||||
const { Joi: CelebrateJoi, celebrate, errors } = require('celebrate')
|
||||
const { ObjectId } = require('mongodb-legacy')
|
||||
const { NotFoundError } = require('../Features/Errors/Errors')
|
||||
const {
|
||||
validateReq,
|
||||
@@ -15,29 +13,7 @@ const { isZodErrorLike, fromError } = require('zod-validation-error')
|
||||
* @typedef {import('express').ErrorRequestHandler} ErrorRequestHandler
|
||||
*/
|
||||
|
||||
const objectIdValidator = {
|
||||
type: 'objectId',
|
||||
base: CelebrateJoi.any(),
|
||||
messages: {
|
||||
'objectId.invalid': 'needs to be a valid ObjectId',
|
||||
},
|
||||
coerce(value) {
|
||||
return {
|
||||
value: typeof value === typeof ObjectId ? value : new ObjectId(value),
|
||||
}
|
||||
},
|
||||
prepare(value, helpers) {
|
||||
if (!ObjectId.isValid(value)) {
|
||||
return {
|
||||
errors: helpers.error('objectId.invalid'),
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const Joi = CelebrateJoi.extend(objectIdValidator)
|
||||
const errorMiddleware = [
|
||||
errors(),
|
||||
/** @type {ErrorRequestHandler} */
|
||||
(err, req, res, next) => {
|
||||
if (!isZodErrorLike(err)) {
|
||||
@@ -48,14 +24,6 @@ const errorMiddleware = [
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* Validation middleware
|
||||
* @deprecated Please use Zod schemas and `validateReq` instead
|
||||
*/
|
||||
function validate(schema) {
|
||||
return celebrate(schema, { allowUnknown: true })
|
||||
}
|
||||
|
||||
const validateReqWeb = (req, schema) => {
|
||||
try {
|
||||
return validateReq(req, schema)
|
||||
@@ -69,8 +37,6 @@ const validateReqWeb = (req, schema) => {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Joi,
|
||||
validate,
|
||||
errorMiddleware,
|
||||
validateReq: validateReqWeb,
|
||||
z,
|
||||
|
||||
@@ -39,7 +39,6 @@ const db = {
|
||||
deletedUsers: internalDb.collection('deletedUsers'),
|
||||
dropboxEntities: internalDb.collection('dropboxEntities'),
|
||||
dropboxProjects: internalDb.collection('dropboxProjects'),
|
||||
docHistory: internalDb.collection('docHistory'),
|
||||
docHistoryIndex: internalDb.collection('docHistoryIndex'),
|
||||
docSnapshots: internalDb.collection('docSnapshots'),
|
||||
docs: internalDb.collection('docs'),
|
||||
|
||||
@@ -34,6 +34,7 @@ block append meta
|
||||
meta(name='ol-tags' data-type='json' content=tags)
|
||||
meta(name='ol-portalTemplates' data-type='json' content=portalTemplates)
|
||||
meta(name='ol-userSettings' data-type='json' content=userSettings)
|
||||
meta(name='ol-overallThemes' data-type='json' content=overallThemes)
|
||||
meta(
|
||||
name='ol-prefetchedProjectsBlob'
|
||||
data-type='json'
|
||||
|
||||
@@ -14,6 +14,7 @@ if (process.env.CI) {
|
||||
export default defineConfig({
|
||||
fixturesFolder: 'cypress/fixtures',
|
||||
video: process.env.CYPRESS_VIDEO === 'true',
|
||||
downloadsFolder: process.env.CYPRESS_DOWNLOADS || 'cypress/downloads',
|
||||
screenshotsFolder: process.env.CYPRESS_RESULTS || 'cypress/results',
|
||||
videosFolder: process.env.CYPRESS_RESULTS || 'cypress/results',
|
||||
viewportHeight: 800,
|
||||
|
||||
@@ -89,6 +89,7 @@ services:
|
||||
user: "${DOCKER_USER:-1000:1000}"
|
||||
environment:
|
||||
CI:
|
||||
CYPRESS_DOWNLOADS:
|
||||
CYPRESS_RESULTS:
|
||||
CYPRESS_SPEC_PATTERN:
|
||||
CYPRESS_EXCLUDE_SPEC_PATTERN:
|
||||
|
||||
@@ -1857,6 +1857,7 @@
|
||||
"the_visual_editor_cant_preview_this_type_of_image_file": "",
|
||||
"the_width_you_choose_here_is_based_on_the_width_of_the_text_in_your_document": "",
|
||||
"their_projects_will_be_transferred_to_another_user": "",
|
||||
"theme": "",
|
||||
"then_x_price_per_month": "",
|
||||
"then_x_price_per_year": "",
|
||||
"there_are_lots_of_options_to_edit_and_customize_your_figures": "",
|
||||
|
||||
@@ -7,14 +7,33 @@ import { useCallback, useEffect, useRef } from 'react'
|
||||
import useEventListener from '@/shared/hooks/use-event-listener'
|
||||
import useDomEventListener from '@/shared/hooks/use-dom-event-listener'
|
||||
import { useIsNewEditorEnabled } from '@/features/ide-redesign/utils/new-editor-utils'
|
||||
import {
|
||||
IdeLayout,
|
||||
IdeView,
|
||||
useLayoutContext,
|
||||
} from '@/shared/context/layout-context'
|
||||
import {
|
||||
RailTabKey,
|
||||
useRailContext,
|
||||
} from '@/features/ide-redesign/contexts/rail-context'
|
||||
|
||||
function createEditingSessionHeartbeatData(
|
||||
editorType: EditorType,
|
||||
newEditor: boolean
|
||||
newEditor: boolean,
|
||||
view: IdeView | null,
|
||||
layout: IdeLayout,
|
||||
railOpen: boolean,
|
||||
railTab: RailTabKey,
|
||||
hasDetachedPdf: boolean
|
||||
) {
|
||||
const newEditorSegmentation = newEditor ? { railOpen, railTab } : {}
|
||||
return {
|
||||
editorType,
|
||||
editorRedesign: newEditor,
|
||||
editorView: view,
|
||||
editorLayout: layout,
|
||||
hasDetachedPdf,
|
||||
...newEditorSegmentation,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +50,8 @@ export function useEditingSessionHeartbeat() {
|
||||
const { projectId } = useIdeReactContext()
|
||||
const { getEditorType } = useEditorManagerContext()
|
||||
const newEditor = useIsNewEditorEnabled()
|
||||
const { view, pdfLayout: layout, detachIsLinked } = useLayoutContext()
|
||||
const { isOpen: railIsOpen, selectedTab: selectedRailTab } = useRailContext()
|
||||
|
||||
// Keep track of how many heartbeats we've sent so that we can calculate how
|
||||
// long to wait until the next one
|
||||
@@ -59,7 +80,12 @@ export function useEditingSessionHeartbeat() {
|
||||
|
||||
const segmentation = createEditingSessionHeartbeatData(
|
||||
editorType,
|
||||
newEditor
|
||||
newEditor,
|
||||
view,
|
||||
layout,
|
||||
railIsOpen,
|
||||
selectedRailTab,
|
||||
detachIsLinked
|
||||
)
|
||||
|
||||
debugConsole.log('[Event] send heartbeat request', segmentation)
|
||||
@@ -80,7 +106,16 @@ export function useEditingSessionHeartbeat() {
|
||||
heartBeatResetTimerRef.current = window.setTimeout(() => {
|
||||
heartBeatSentRecentlyRef.current = false
|
||||
}, backoffSecs * 1000)
|
||||
}, [getEditorType, projectId, newEditor])
|
||||
}, [
|
||||
getEditorType,
|
||||
projectId,
|
||||
newEditor,
|
||||
view,
|
||||
layout,
|
||||
railIsOpen,
|
||||
selectedRailTab,
|
||||
detachIsLinked,
|
||||
])
|
||||
|
||||
// Hook the heartbeat up to editor events
|
||||
useEventListener('cursor:editor:update', editingSessionHeartbeat)
|
||||
|
||||
@@ -3,6 +3,7 @@ import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context'
|
||||
import { useStopOnFirstError } from '@/shared/hooks/use-stop-on-first-error'
|
||||
import { sendMB } from '@/infrastructure/event-tracking'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import ErrorState from './error-state'
|
||||
import StartFreeTrialButton from '@/shared/components/start-free-trial-button'
|
||||
@@ -149,7 +150,16 @@ const ReasonsForTimeoutInfo = () => {
|
||||
i18nKey="project_timed_out_optimize_images"
|
||||
components={[
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key
|
||||
<a href="https://www.overleaf.com/learn/how-to/Optimising_very_large_image_files" />,
|
||||
<a
|
||||
href="https://www.overleaf.com/learn/how-to/Optimising_very_large_image_files"
|
||||
onClick={() => {
|
||||
sendMB('paywall-info-click', {
|
||||
'paywall-type': 'compile-timeout',
|
||||
content: 'docs',
|
||||
type: 'optimize',
|
||||
})
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</li>
|
||||
@@ -158,7 +168,16 @@ const ReasonsForTimeoutInfo = () => {
|
||||
i18nKey="a_fatal_compile_error_that_completely_blocks_compilation"
|
||||
components={[
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key
|
||||
<a href="https://www.overleaf.com/learn/how-to/Why_do_I_keep_getting_the_compile_timeout_error_message%3F#Fatal_compile_errors_blocking_the_compilation" />,
|
||||
<a
|
||||
href="https://www.overleaf.com/learn/how-to/Why_do_I_keep_getting_the_compile_timeout_error_message%3F#Fatal_compile_errors_blocking_the_compilation"
|
||||
onClick={() => {
|
||||
sendMB('paywall-info-click', {
|
||||
'paywall-type': 'compile-timeout',
|
||||
content: 'docs',
|
||||
type: 'fatal-error',
|
||||
})
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
{!lastCompileOptions.stopOnFirstError && (
|
||||
@@ -191,6 +210,13 @@ const ReasonsForTimeoutInfo = () => {
|
||||
href="/learn/how-to/Fixing_and_preventing_compile_timeouts"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
onClick={() => {
|
||||
sendMB('paywall-info-click', {
|
||||
'paywall-type': 'compile-timeout',
|
||||
content: 'docs',
|
||||
type: 'learn-more',
|
||||
})
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { sendSearchEvent } from '@/features/event-tracking/search-events'
|
||||
import { useProjectContext } from '@/shared/context/project-context'
|
||||
import useEventListener from '@/shared/hooks/use-event-listener'
|
||||
import usePersistedState from '@/shared/hooks/use-persisted-state'
|
||||
import { isMac } from '@/shared/utils/os'
|
||||
@@ -45,7 +46,11 @@ const RailContext = createContext<
|
||||
>(undefined)
|
||||
|
||||
export const RailProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
||||
const [isOpen, setIsOpen] = usePersistedState('rail-is-open', true)
|
||||
const { projectId } = useProjectContext()
|
||||
const [isOpen, setIsOpen] = usePersistedState(
|
||||
`rail-is-open-${projectId}`,
|
||||
true
|
||||
)
|
||||
const [resizing, setResizing] = useState(false)
|
||||
const [activeModal, setActiveModalInternal] = useState<RailModalKey | null>(
|
||||
null
|
||||
@@ -70,7 +75,7 @@ export const RailProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
||||
}, [setIsOpen])
|
||||
|
||||
const [selectedTab, setSelectedTab] = usePersistedState<RailTabKey>(
|
||||
'selected-rail-tab',
|
||||
`selected-rail-tab-${projectId}`,
|
||||
'file-tree'
|
||||
)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import PdfLogEntry from './pdf-log-entry'
|
||||
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
|
||||
import { useStopOnFirstError } from '../../../shared/hooks/use-stop-on-first-error'
|
||||
import getMeta from '../../../utils/meta'
|
||||
import { sendMB } from '@/infrastructure/event-tracking'
|
||||
|
||||
function PdfPreviewError({
|
||||
error,
|
||||
@@ -300,7 +301,16 @@ function TimedOutLogEntry() {
|
||||
i18nKey="project_timed_out_optimize_images"
|
||||
components={[
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key
|
||||
<a href="https://www.overleaf.com/learn/how-to/Optimising_very_large_image_files" />,
|
||||
<a
|
||||
href="https://www.overleaf.com/learn/how-to/Optimising_very_large_image_files"
|
||||
onClick={() => {
|
||||
sendMB('paywall-info-click', {
|
||||
'paywall-type': 'compile-timeout',
|
||||
content: 'docs',
|
||||
type: 'optimize',
|
||||
})
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</li>
|
||||
@@ -309,7 +319,16 @@ function TimedOutLogEntry() {
|
||||
i18nKey="project_timed_out_fatal_error"
|
||||
components={[
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key
|
||||
<a href="https://www.overleaf.com/learn/how-to/Why_do_I_keep_getting_the_compile_timeout_error_message%3F#Fatal_compile_errors_blocking_the_compilation" />,
|
||||
<a
|
||||
href="https://www.overleaf.com/learn/how-to/Why_do_I_keep_getting_the_compile_timeout_error_message%3F#Fatal_compile_errors_blocking_the_compilation"
|
||||
onClick={() => {
|
||||
sendMB('paywall-info-click', {
|
||||
'paywall-type': 'compile-timeout',
|
||||
content: 'docs',
|
||||
type: 'fatal-error',
|
||||
})
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
{!lastCompileOptions.stopOnFirstError && (
|
||||
@@ -335,7 +354,16 @@ function TimedOutLogEntry() {
|
||||
i18nKey="project_timed_out_learn_more"
|
||||
components={[
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key
|
||||
<a href="https://www.overleaf.com/learn/how-to/Why_do_I_keep_getting_the_compile_timeout_error_message%3F" />,
|
||||
<a
|
||||
href="https://www.overleaf.com/learn/how-to/Why_do_I_keep_getting_the_compile_timeout_error_message%3F"
|
||||
onClick={() => {
|
||||
sendMB('paywall-info-click', {
|
||||
'paywall-type': 'compile-timeout',
|
||||
content: 'docs',
|
||||
type: 'learn-more',
|
||||
})
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</p>
|
||||
|
||||
@@ -179,6 +179,13 @@ const PreventTimeoutHelpMessage = memo(function PreventTimeoutHelpMessage({
|
||||
href="/learn/how-to/Optimising_very_large_image_files"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
onClick={() => {
|
||||
eventTracking.sendMB('paywall-info-click', {
|
||||
'paywall-type': 'compile-timeout',
|
||||
content: 'docs',
|
||||
type: 'optimize',
|
||||
})
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
@@ -192,6 +199,13 @@ const PreventTimeoutHelpMessage = memo(function PreventTimeoutHelpMessage({
|
||||
href="/learn/how-to/Fixing_and_preventing_compile_timeouts#Step_3:_Assess_your_project_for_time-consuming_tasks_and_fatal_errors"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
onClick={() => {
|
||||
eventTracking.sendMB('paywall-info-click', {
|
||||
'paywall-type': 'compile-timeout',
|
||||
content: 'docs',
|
||||
type: 'fatal-error',
|
||||
})
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
@@ -225,6 +239,13 @@ const PreventTimeoutHelpMessage = memo(function PreventTimeoutHelpMessage({
|
||||
href="/learn/how-to/Fixing_and_preventing_compile_timeouts"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
onClick={() => {
|
||||
eventTracking.sendMB('paywall-info-click', {
|
||||
'paywall-type': 'compile-timeout',
|
||||
content: 'docs',
|
||||
type: 'learn-more',
|
||||
})
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -17,6 +17,7 @@ import { AccountMenuItems } from '@/shared/components/navbar/account-menu-items'
|
||||
import { useScrolled } from '@/features/project-list/components/sidebar/use-scroll'
|
||||
import { useSendProjectListMB } from '@/features/project-list/components/project-list-events'
|
||||
import { SurveyWidgetDsNav } from '@/features/project-list/components/survey-widget-ds-nav'
|
||||
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||
|
||||
function SidebarDsNav() {
|
||||
const { t } = useTranslation()
|
||||
@@ -36,6 +37,8 @@ function SidebarDsNav() {
|
||||
item => item.text === 'help_and_resources'
|
||||
) as NavbarDropdownItemData
|
||||
const { containerRef, scrolledUp, scrolledDown } = useScrolled()
|
||||
const themedDsNav = useFeatureFlag('themed-project-dashboard')
|
||||
|
||||
return (
|
||||
<div
|
||||
className="project-list-sidebar-wrapper-react d-none d-md-flex"
|
||||
@@ -159,6 +162,7 @@ function SidebarDsNav() {
|
||||
<AccountMenuItems
|
||||
sessionUser={sessionUser}
|
||||
showSubscriptionLink={showSubscriptionLink}
|
||||
showThemeToggle={themedDsNav}
|
||||
/>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import useSetOverallTheme from '@/features/editor-left-menu/hooks/use-set-overall-theme'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import OLTooltip from '@/shared/components/ol/ol-tooltip'
|
||||
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
|
||||
import getMeta from '@/utils/meta'
|
||||
import { OverallThemeMeta } from '@ol-types/project-settings'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const getIcon = (theme: OverallThemeMeta) => {
|
||||
switch (theme.val) {
|
||||
case 'light-':
|
||||
return 'light_mode'
|
||||
case 'system':
|
||||
return 'computer'
|
||||
default:
|
||||
return 'dark_mode'
|
||||
}
|
||||
}
|
||||
|
||||
export default function ThemeToggle() {
|
||||
const {
|
||||
userSettings: { overallTheme },
|
||||
} = useUserSettingsContext()
|
||||
const setOverallTheme = useSetOverallTheme()
|
||||
const overallThemes = getMeta('ol-overallThemes')
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<fieldset className="dropdown-item theme-toggle">
|
||||
<legend>{t('theme')}</legend>
|
||||
<div className="theme-toggle-radios">
|
||||
{overallThemes.map(theme => (
|
||||
<OLTooltip
|
||||
key={theme.val}
|
||||
description={theme.name}
|
||||
id={`theme-switch-${theme.name}-tooltip`}
|
||||
>
|
||||
<div className="theme-toggle-radio">
|
||||
<input
|
||||
id={`theme-switch-${theme.name}`}
|
||||
type="radio"
|
||||
value={theme.val}
|
||||
checked={overallTheme === theme.val}
|
||||
onChange={() => setOverallTheme(theme.val)}
|
||||
/>
|
||||
<label htmlFor={`theme-switch-${theme.name}`}>
|
||||
<MaterialIcon type={getIcon(theme)} />
|
||||
</label>
|
||||
</div>
|
||||
</OLTooltip>
|
||||
))}
|
||||
</div>
|
||||
</fieldset>
|
||||
)
|
||||
}
|
||||
@@ -245,6 +245,7 @@ function AddEmail() {
|
||||
<AddEmailViaSSO
|
||||
email={newEmail}
|
||||
domainInfo={newEmailMatchedDomain}
|
||||
userInstitutions={state.data.linkedInstitutionIds}
|
||||
/>
|
||||
</div>
|
||||
</Cell>
|
||||
@@ -259,12 +260,28 @@ function AddEmail() {
|
||||
function AddEmailViaSSO({
|
||||
email,
|
||||
domainInfo,
|
||||
userInstitutions,
|
||||
}: {
|
||||
email: string
|
||||
domainInfo: DomainInfo
|
||||
userInstitutions: string[]
|
||||
}) {
|
||||
if (domainInfo.university.ssoEnabled) {
|
||||
// SSO for Commons institution
|
||||
// Check if the user has already linked this institution
|
||||
if (userInstitutions.includes(domainInfo.university.id.toString())) {
|
||||
return (
|
||||
<Notification
|
||||
type="error"
|
||||
ariaLive="polite"
|
||||
content={
|
||||
<>
|
||||
This institution is already linked with your account via another
|
||||
email address.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return <SsoLinkingInfo email={email} domainInfo={domainInfo} />
|
||||
} else if (
|
||||
domainInfo.group?.domainCaptureEnabled &&
|
||||
|
||||
@@ -16,25 +16,31 @@ export const ToolbarButtonMenu: FC<
|
||||
disabled?: boolean
|
||||
disablePopover?: boolean
|
||||
altCommand?: (view: EditorView) => void
|
||||
onToggle?: (isOpen: boolean) => void
|
||||
}>
|
||||
> = memo(function ButtonMenu({
|
||||
icon,
|
||||
id,
|
||||
label,
|
||||
altCommand,
|
||||
onToggle,
|
||||
disabled,
|
||||
disablePopover,
|
||||
children,
|
||||
}) {
|
||||
const target = useRef<any>(null)
|
||||
const { open, onToggle, ref } = useDropdown()
|
||||
const { open, onToggle: handleToggle, ref } = useDropdown()
|
||||
const view = useCodeMirrorViewContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (disablePopover && open) {
|
||||
onToggle(false)
|
||||
handleToggle(false)
|
||||
}
|
||||
}, [open, disablePopover, onToggle])
|
||||
}, [open, disablePopover, handleToggle])
|
||||
|
||||
useEffect(() => {
|
||||
onToggle?.(open)
|
||||
}, [open, onToggle])
|
||||
|
||||
const button = (
|
||||
<button
|
||||
@@ -57,7 +63,7 @@ export const ToolbarButtonMenu: FC<
|
||||
altCommand(view)
|
||||
view.focus()
|
||||
} else {
|
||||
onToggle(!open)
|
||||
handleToggle(!open)
|
||||
}
|
||||
}}
|
||||
ref={target}
|
||||
@@ -75,7 +81,7 @@ export const ToolbarButtonMenu: FC<
|
||||
containerPadding={0}
|
||||
transition
|
||||
rootClose
|
||||
onHide={() => onToggle(false)}
|
||||
onHide={() => handleToggle(false)}
|
||||
>
|
||||
<OLPopover
|
||||
id={`${id}-menu`}
|
||||
@@ -85,7 +91,7 @@ export const ToolbarButtonMenu: FC<
|
||||
<OLListGroup
|
||||
role="menu"
|
||||
onClick={() => {
|
||||
onToggle(false)
|
||||
handleToggle(false)
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -35,10 +35,31 @@ const en = {
|
||||
'translate.es': 'Spanish',
|
||||
'translate.de': 'German',
|
||||
'translate.ja': 'Japanese',
|
||||
'translate.sq': 'Albanian',
|
||||
'translate.ar': 'Arabic',
|
||||
'translate.bg': 'Bulgarian',
|
||||
'translate.cs': 'Czech',
|
||||
'translate.nl': 'Dutch',
|
||||
'translate.fil': 'Filipino',
|
||||
'translate.el': 'Greek',
|
||||
'translate.he': 'Hebrew',
|
||||
'translate.hu': 'Hungarian',
|
||||
'translate.it': 'Italian',
|
||||
'translate.ms': 'Malay',
|
||||
'translate.fa': 'Persian(Farsi)',
|
||||
'translate.pl': 'Polish',
|
||||
'translate.ro': 'Romanian',
|
||||
'translate.ru': 'Russian',
|
||||
'translate.sr': 'Serbian',
|
||||
'translate.sv': 'Swedish',
|
||||
'translate.tr': 'Turkish',
|
||||
'translate.uk': 'Ukrainian',
|
||||
'translate.vi': 'Vietnamese',
|
||||
'translate.other': 'Other',
|
||||
'translate.not-listed': 'Language not listed?',
|
||||
'translate.more-languages-coming-soon.title': 'More languages coming soon',
|
||||
'translate.more-languages-coming-soon.body':
|
||||
'Sorry, we don’t currently offer any other languages for Translate. We will be adding more throughout October and November so watch this space!',
|
||||
'Sorry, we don’t currently offer any other languages for Translate. We will be adding more throughout November so watch this space!',
|
||||
'blocked-suggestion-signpost.question':
|
||||
'Do you want to permanently stop this suggestion from appearing again?',
|
||||
'blocked-suggestion-signpost.tooltip': 'You can block a suggestion here.',
|
||||
@@ -376,10 +397,31 @@ const es = {
|
||||
'translate.es': 'Español',
|
||||
'translate.de': 'Alemán',
|
||||
'translate.ja': 'Japonés',
|
||||
'translate.sq': 'Albanés',
|
||||
'translate.ar': 'Árabe',
|
||||
'translate.bg': 'Búlgaro',
|
||||
'translate.cs': 'Checo',
|
||||
'translate.nl': 'Neerlandés',
|
||||
'translate.fil': 'Filipino',
|
||||
'translate.el': 'Griego',
|
||||
'translate.he': 'Hebreo',
|
||||
'translate.hu': 'Húngaro',
|
||||
'translate.it': 'Italiano',
|
||||
'translate.ms': 'Malayo',
|
||||
'translate.fa': 'Persa (Farsi)',
|
||||
'translate.pl': 'Polaco',
|
||||
'translate.ro': 'Rumano',
|
||||
'translate.ru': 'Ruso',
|
||||
'translate.sr': 'Serbio',
|
||||
'translate.sv': 'Sueco',
|
||||
'translate.tr': 'Turco',
|
||||
'translate.uk': 'Ucraniano',
|
||||
'translate.vi': 'Vietnamita',
|
||||
'translate.other': 'Otro',
|
||||
'translate.not-listed': '¿Idioma no listado?',
|
||||
'translate.more-languages-coming-soon.title': 'Más idiomas próximamente',
|
||||
'translate.more-languages-coming-soon.body':
|
||||
'Lo sentimos, actualmente no ofrecemos otros idiomas para la traducción. Agregaremos más a lo largo de octubre y noviembre, ¡así que mantente atento!',
|
||||
'Lo sentimos, actualmente no ofrecemos otros idiomas para la traducción. Agregaremos más a lo largo de noviembre, ¡así que mantente atento!',
|
||||
'blocked-suggestion-signpost.question':
|
||||
'¿Quieres dejar de ver esta sugerencia permanentemente? Puedes bloquear esta sugerencia aquí.',
|
||||
'blocked-suggestion-signpost.tooltip':
|
||||
|
||||
@@ -7,17 +7,21 @@ import NavDropdownDivider from './nav-dropdown-divider'
|
||||
import NavDropdownLinkItem from './nav-dropdown-link-item'
|
||||
import { useDsNavStyle } from '@/features/project-list/components/use-is-ds-nav'
|
||||
import { SignOut } from '@phosphor-icons/react'
|
||||
import ThemeToggle from '@/features/project-list/components/sidebar/theme-toggle'
|
||||
|
||||
export function AccountMenuItems({
|
||||
sessionUser,
|
||||
showSubscriptionLink,
|
||||
showThemeToggle = false,
|
||||
}: {
|
||||
sessionUser: NavbarSessionUser
|
||||
showSubscriptionLink: boolean
|
||||
showThemeToggle?: boolean
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const logOutFormId = 'logOutForm'
|
||||
const dsNavStyle = useDsNavStyle()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dropdown.Item as="li" disabled role="menuitem">
|
||||
@@ -32,6 +36,12 @@ export function AccountMenuItems({
|
||||
{t('subscription')}
|
||||
</NavDropdownLinkItem>
|
||||
) : null}
|
||||
{showThemeToggle && (
|
||||
<DropdownListItem>
|
||||
<ThemeToggle />
|
||||
</DropdownListItem>
|
||||
)}
|
||||
|
||||
<NavDropdownDivider />
|
||||
<DropdownListItem>
|
||||
{
|
||||
|
||||
297
services/web/frontend/stories/page-layouts.stories.tsx
Normal file
297
services/web/frontend/stories/page-layouts.stories.tsx
Normal file
@@ -0,0 +1,297 @@
|
||||
import _ from 'lodash'
|
||||
import { ComponentType } from 'react'
|
||||
import { Navbar } from 'react-bootstrap'
|
||||
import OLPageContentCard from '@/shared/components/ol/ol-page-content-card'
|
||||
import OLRow from '@/shared/components/ol/ol-row'
|
||||
import OLCol from '@/shared/components/ol/ol-col'
|
||||
import OLButton from '@/shared/components/ol/ol-button'
|
||||
import overleafLogo from '@/shared/svgs/overleaf-a-ds-solution-mallard.svg'
|
||||
|
||||
const lorem = (n: number) => {
|
||||
const quacks = ['quack', 'quack', 'quack', 'quak']
|
||||
let result = ''
|
||||
if (n >= 1) result += 'Lorem'
|
||||
if (n >= 2) result += ' epsom'
|
||||
for (let i = 2; i < n; i++) {
|
||||
const next =
|
||||
result.at(-1) === '.'
|
||||
? ' ' + _.capitalize(quacks[Math.floor(Math.random() * quacks.length)])
|
||||
: quacks[Math.floor(Math.random() * (quacks.length + 1))]
|
||||
result += next ? ' ' + next : '.'
|
||||
}
|
||||
if (result.at(-1) !== '.') result += '.'
|
||||
return result
|
||||
}
|
||||
|
||||
const Nav = () => <Navbar className="navbar-default navbar-main" />
|
||||
|
||||
export const UnsuportedBrowser = () => (
|
||||
<main className="content content-alt full-height" id="main-content">
|
||||
<div className="container full-height">
|
||||
<div className="error-container full-height">
|
||||
<div className="error-details">
|
||||
<h1 className="error-status">Unsupported Browser</h1>
|
||||
<p className="error-description">{lorem(60)}</p>
|
||||
<hr />
|
||||
<p>{lorem(40)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
|
||||
export const Error400 = () => (
|
||||
<main className="content content-alt full-height" id="main-content">
|
||||
<div className="container full-height">
|
||||
<div className="error-container full-height">
|
||||
<div className="error-details">
|
||||
<p className="error-status">Something went wrong, sorry.</p>
|
||||
<p className="error-description">{lorem(15)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
|
||||
export const Error404 = () => (
|
||||
<>
|
||||
<Nav />
|
||||
<main className="content content-alt" id="main-content">
|
||||
<div className="container">
|
||||
<div className="error-container">
|
||||
<div className="error-details">
|
||||
<p className="error-status">Not found</p>
|
||||
<p className="error-description">{lorem(20)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
|
||||
export const Closed = () => (
|
||||
<>
|
||||
<Nav />
|
||||
<main className="content" id="main-content">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-lg-8 col-lg-offset-2 text-center">
|
||||
<div className="page-header">
|
||||
<h1>Maintenance</h1>
|
||||
</div>
|
||||
<p>{lorem(6)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
|
||||
export const PlannedMaintenance = () => (
|
||||
<>
|
||||
<Nav />
|
||||
<main className="content" id="main-content">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-lg-8 col-lg-offset-2">
|
||||
<div className="page-header">
|
||||
<h1>Planned Maintenance</h1>
|
||||
</div>
|
||||
<p>{lorem(6)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
|
||||
export const PostGateway = () => (
|
||||
<>
|
||||
<div className="content content-alt">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-lg-6 offset-lg-3">
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
<p className="text-center">
|
||||
Please wait while we process your request.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
export const AccountSuspended = () => (
|
||||
<main className="content content-alt" id="main-content">
|
||||
<div className="container-custom-sm mx-auto">
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
<h3>Your account is suspended</h3>
|
||||
<p>{lorem(6)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
|
||||
export const Restricted = () => (
|
||||
<>
|
||||
<Nav />
|
||||
<main className="content" id="main-content">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-md-8 offset-md-2 text-center">
|
||||
<div className="page-header">
|
||||
<h2>
|
||||
Restricted, sorry you don’t have permission to load this page.
|
||||
</h2>
|
||||
</div>
|
||||
<p>{lorem(23)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
|
||||
export const OneTimeLogin = () => (
|
||||
<>
|
||||
<Nav />
|
||||
<main className="content content-alt" id="main-content">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-lg-6 offset-lg-3 col-xl-4 offset-xl-4">
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
<div className="page-header">
|
||||
<h1>We're back!</h1>
|
||||
</div>
|
||||
<p>Overleaf is now running normally.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
|
||||
export const Invite = () => (
|
||||
<main className="content content-alt" id="invite-root">
|
||||
<OLRow className="row row-spaced">
|
||||
<OLCol lg={{ span: 8, offset: 2 }}>
|
||||
<OLPageContentCard>
|
||||
<div className="page-header">
|
||||
<h1 className="text-center">
|
||||
<span className="team-invite-name">
|
||||
max.mustermann@example.com
|
||||
</span>{' '}
|
||||
has invited you to join a group subscription on Overleaf
|
||||
</h1>
|
||||
</div>
|
||||
<p className="text-center">{lorem(20)}</p>
|
||||
</OLPageContentCard>
|
||||
</OLCol>
|
||||
</OLRow>
|
||||
</main>
|
||||
)
|
||||
|
||||
export const NotValid = () => (
|
||||
<>
|
||||
<Nav />
|
||||
<main className="content content-alt" id="main-content">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-md-8 col-md-offset-2 offset-md-2">
|
||||
<div className="card project-invite-invalid">
|
||||
<div className="card-body">
|
||||
<div className="page-header text-center">
|
||||
<h1>Invite not valid</h1>
|
||||
</div>
|
||||
<div className="row text-center">
|
||||
<div className="col-12 col-md-12">
|
||||
<p>{lorem(20)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
|
||||
export const CompleteRegistration = () => (
|
||||
<>
|
||||
<Nav />
|
||||
<main className="content content-alt" id="main-content">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12 col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2 offset-md-1 offset-lg-2">
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
<div className="page-header">
|
||||
<h1 className="text-center">Dropbox Sync</h1>
|
||||
</div>
|
||||
<p>{lorem(20)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
|
||||
export const Ciam = () => (
|
||||
<div className="ciam-layout">
|
||||
<a
|
||||
href="/"
|
||||
aria-label="Overleaf"
|
||||
className="brand"
|
||||
style={{ backgroundImage: `url("${overleafLogo}")` }}
|
||||
/>
|
||||
<div className="ciam-container">
|
||||
<main className="ciam-card" id="main-content">
|
||||
<h1>Create your Overleaf account</h1>
|
||||
<p>{lorem(20)}</p>
|
||||
<hr />
|
||||
<p>{lorem(20)}</p>
|
||||
<OLButton>Button</OLButton>
|
||||
</main>
|
||||
</div>
|
||||
<footer>
|
||||
<a href="https://www.overleaf.com/legal#Privacy">Privacy</a>
|
||||
<a href="https://www.overleaf.com/legal#Terms">Terms</a>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default {
|
||||
title: 'Shared / Layouts',
|
||||
args: {
|
||||
label: 'Option',
|
||||
},
|
||||
|
||||
parameters: {
|
||||
layout: 'fullscreen', // This is crucial for vh/vw layouts
|
||||
},
|
||||
decorators: [
|
||||
(Story: ComponentType) => (
|
||||
<div style={{ height: '100vh', width: '100vw' }}>
|
||||
<style>
|
||||
{`.content {
|
||||
min-height: 100vh;
|
||||
padding-top: 93px;
|
||||
}`}
|
||||
</style>
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
}
|
||||
70
services/web/frontend/stylesheets/ciam/all.scss
Normal file
70
services/web/frontend/stylesheets/ciam/all.scss
Normal file
@@ -0,0 +1,70 @@
|
||||
@import 'ciam-variables';
|
||||
@import 'ciam-mixins';
|
||||
|
||||
.ciam-layout {
|
||||
padding: var(--ciam-spacing-350);
|
||||
display: flex;
|
||||
min-height: 100%;
|
||||
flex-direction: column;
|
||||
font-family: var(--ciam-font-family-sans), sans-serif;
|
||||
color: var(--ciam-color-text-primary);
|
||||
font-size: var(--ciam-font-size-400);
|
||||
line-height: 1.5;
|
||||
|
||||
@include ciam-body-md-regular;
|
||||
|
||||
.ciam-container {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.brand {
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-size: contain;
|
||||
height: 64px;
|
||||
width: 130px;
|
||||
margin: var(--ciam-spacing-350) auto;
|
||||
display: block;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
margin: var(--ciam-spacing-350) var(--ciam-spacing-800);
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
@include ciam-heading-sm-semibold;
|
||||
}
|
||||
|
||||
.ciam-card {
|
||||
box-shadow:
|
||||
0 4px 6px -4px rgb(0 0 0 / 10%),
|
||||
0 1px 29px -3px rgb(0 0 0 / 16%);
|
||||
padding: var(--ciam-spacing-800) var(--ciam-spacing-400);
|
||||
border-radius: var(--ciam-border-radius-400);
|
||||
max-width: 460px;
|
||||
margin: var(--ciam-spacing-400) auto;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
padding: var(--ciam-spacing-1300);
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
display: flex;
|
||||
gap: var(--ciam-spacing-600);
|
||||
text-transform: uppercase;
|
||||
justify-content: center;
|
||||
margin: var(--ciam-spacing-350) auto;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
margin: var(--ciam-spacing-350) var(--ciam-spacing-800);
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
|
||||
@include ciam-body-sm-regular;
|
||||
}
|
||||
}
|
||||
}
|
||||
68
services/web/frontend/stylesheets/ciam/ciam-colors.scss
Normal file
68
services/web/frontend/stylesheets/ciam/ciam-colors.scss
Normal file
@@ -0,0 +1,68 @@
|
||||
.ciam-layout {
|
||||
--ciam-color-neutral-50: #fafafa;
|
||||
--ciam-color-neutral-100: #f2f2f2;
|
||||
--ciam-color-neutral-200: #e6e6e6;
|
||||
--ciam-color-neutral-300: #d6d6d6;
|
||||
--ciam-color-neutral-400: #c7c7c7;
|
||||
--ciam-color-neutral-500: #b5b5b5;
|
||||
--ciam-color-neutral-600: #a1a1a1;
|
||||
--ciam-color-neutral-700: #8a8a8a;
|
||||
--ciam-color-neutral-800: #6b6b6b;
|
||||
--ciam-color-neutral-900: #383838;
|
||||
--ciam-color-neutral-950: #262626;
|
||||
--ciam-color-green-50: #f2f8f5;
|
||||
--ciam-color-green-100: #e3f2eb;
|
||||
--ciam-color-green-200: #c0e7d6;
|
||||
--ciam-color-green-300: #8adbb7;
|
||||
--ciam-color-green-400: #38cc89;
|
||||
--ciam-color-green-500: #26b072;
|
||||
--ciam-color-green-600: #158954;
|
||||
--ciam-color-green-700: #19754c;
|
||||
--ciam-color-green-800: #196241;
|
||||
--ciam-color-green-900: #164630;
|
||||
--ciam-color-green-950: #112c20;
|
||||
--ciam-color-yellow-50: #fffaeb;
|
||||
--ciam-color-yellow-100: #fff7db;
|
||||
--ciam-color-yellow-200: #ffeeb8;
|
||||
--ciam-color-yellow-300: #ffe58f;
|
||||
--ciam-color-yellow-400: #ffda61;
|
||||
--ciam-color-yellow-500: #ffcc20;
|
||||
--ciam-color-yellow-600: #f0b800;
|
||||
--ciam-color-yellow-700: #d1a000;
|
||||
--ciam-color-yellow-800: #ad8500;
|
||||
--ciam-color-yellow-900: #806200;
|
||||
--ciam-color-yellow-950: #574200;
|
||||
--ciam-color-red-50: #fff5f7;
|
||||
--ciam-color-red-100: #fee7eb;
|
||||
--ciam-color-red-200: #fdc9d3;
|
||||
--ciam-color-red-300: #fba7b7;
|
||||
--ciam-color-red-400: #f97b92;
|
||||
--ciam-color-red-500: #f51d43;
|
||||
--ciam-color-red-600: #e60a32;
|
||||
--ciam-color-red-700: #c3092b;
|
||||
--ciam-color-red-800: #a10723;
|
||||
--ciam-color-red-900: #75051a;
|
||||
--ciam-color-red-950: #530412;
|
||||
--ciam-color-blue-50: #f7fafd;
|
||||
--ciam-color-blue-100: #ecf2f9;
|
||||
--ciam-color-blue-200: #d4e3f2;
|
||||
--ciam-color-blue-300: #bdd4ea;
|
||||
--ciam-color-blue-400: #a2c3e2;
|
||||
--ciam-color-blue-500: #7facd7;
|
||||
--ciam-color-blue-600: #5893cb;
|
||||
--ciam-color-blue-700: #3470a8;
|
||||
--ciam-color-blue-800: #2b5d8c;
|
||||
--ciam-color-blue-900: #1e4161;
|
||||
--ciam-color-blue-950: #17314a;
|
||||
--ciam-color-teal-50: #f1f8f8;
|
||||
--ciam-color-teal-100: #e3f2f1;
|
||||
--ciam-color-teal-200: #c1e2df;
|
||||
--ciam-color-teal-300: #9ed1cd;
|
||||
--ciam-color-teal-400: #6dbab4;
|
||||
--ciam-color-teal-500: #4a9d96;
|
||||
--ciam-color-teal-600: #438e88;
|
||||
--ciam-color-teal-700: #3b7d77;
|
||||
--ciam-color-teal-800: #2f6460;
|
||||
--ciam-color-teal-900: #244c49;
|
||||
--ciam-color-teal-950: #193432;
|
||||
}
|
||||
107
services/web/frontend/stylesheets/ciam/ciam-mixins.scss
Normal file
107
services/web/frontend/stylesheets/ciam/ciam-mixins.scss
Normal file
@@ -0,0 +1,107 @@
|
||||
@mixin ciam-body-xs-regular() {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
@mixin ciam-body-xs-semibold() {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
@mixin ciam-body-sm-regular() {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
@mixin ciam-body-sm-semibold() {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
@mixin ciam-body-md-regular() {
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
@mixin ciam-body-md-semibold() {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
@mixin ciam-body-lg-regular() {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
|
||||
@mixin ciam-body-lg-semibold() {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
|
||||
@mixin ciam-heading-xs-regular() {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
@mixin ciam-heading-xs-semibold() {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
@mixin ciam-heading-sm-regular() {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
|
||||
@mixin ciam-heading-sm-semibold() {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
|
||||
@mixin ciam-heading-md-regular() {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 400;
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
@mixin ciam-heading-md-semibold() {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
@mixin ciam-heading-lg-regular() {
|
||||
font-size: 2rem;
|
||||
font-weight: 400;
|
||||
line-height: 2.5rem;
|
||||
}
|
||||
|
||||
@mixin ciam-heading-lg-semibold() {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
line-height: 2.5rem;
|
||||
}
|
||||
|
||||
@mixin ciam-heading-xl-regular() {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 400;
|
||||
line-height: 3rem;
|
||||
}
|
||||
|
||||
@mixin ciam-heading-xl-semibold() {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 600;
|
||||
line-height: 3rem;
|
||||
}
|
||||
35
services/web/frontend/stylesheets/ciam/ciam-variables.scss
Normal file
35
services/web/frontend/stylesheets/ciam/ciam-variables.scss
Normal file
@@ -0,0 +1,35 @@
|
||||
@import 'ciam-mixins';
|
||||
@import 'ciam-colors';
|
||||
|
||||
// TODO: Replace `fuchsia` by the correct colors.
|
||||
|
||||
.ciam-layout {
|
||||
// Spacings
|
||||
--ciam-spacing-200: 8px;
|
||||
--ciam-spacing-250: 10px;
|
||||
--ciam-spacing-350: 12px;
|
||||
--ciam-spacing-400: 16px;
|
||||
--ciam-spacing-600: 24px; // TODO: confirm this variable name (couldn't find in design system)
|
||||
--ciam-spacing-800: 32px; // TODO: confirm this variable name (couldn't find in design system)
|
||||
--ciam-spacing-1300: 52px;
|
||||
|
||||
// Base variables
|
||||
--ciam-color-text-secondary: var(--ciam-color-neutral-800);
|
||||
--ciam-color-text-primary: var(--ciam-color-neutral-900);
|
||||
--ciam-border-radius-200: var(--ciam-spacing-300);
|
||||
--ciam-border-radius-400: var(--ciam-spacing-400);
|
||||
--ciam-font-family-sans: 'Inter', sans-serif;
|
||||
|
||||
// Links
|
||||
// used in services/web/frontend/stylesheets/base/links.scss
|
||||
--link-color: var(--ciam-color-text-secondary);
|
||||
--link-hover-color: fuchsia;
|
||||
|
||||
// TODO: validate that this is correct
|
||||
--link-visited-color: var(--ciam-color-text-secondary);
|
||||
--link-color-dark: fuchsia;
|
||||
--link-hover-color-dark: fuchsia;
|
||||
--link-visited-color-dark: fuchsia;
|
||||
--link-text-decoration: underline;
|
||||
--link-hover-text-decoration: none;
|
||||
}
|
||||
@@ -1,3 +1,48 @@
|
||||
@mixin light-dropdown-menu {
|
||||
--dropdown-text-color: var(--neutral-90);
|
||||
--dropdown-text-secondary: var(--content-secondary);
|
||||
--dropdown-text-active: var(--green-70);
|
||||
--dropdown-text-danger: var(--content-danger);
|
||||
--dropdown-text-danger-hover: var(--content-danger);
|
||||
--dropdown-header-text-color: var(--content-secondary);
|
||||
--dropdown-background: var(--white);
|
||||
--dropdown-background-active: var(--bg-accent-03);
|
||||
--dropdown-background-hover: var(--bg-light-secondary);
|
||||
--dropdown-background-danger-hover: var(--bg-danger-03);
|
||||
--dropdown-border-divider: var(--border-divider);
|
||||
--dropdown-text-hover: var(--content-primary);
|
||||
--dropdown-border-width: 0;
|
||||
--dropdown-border-color: transparent;
|
||||
}
|
||||
|
||||
@mixin dark-dropdown-menu {
|
||||
--dropdown-background: var(--bg-dark-primary);
|
||||
--dropdown-background-hover: var(--bg-dark-secondary);
|
||||
--dropdown-background-danger-hover: var(--bg-danger-02);
|
||||
--dropdown-text-color: var(--content-primary-dark);
|
||||
--dropdown-text-danger: var(--red-40);
|
||||
--dropdown-text-danger-hover: var(--red-20);
|
||||
--dropdown-text-secondary: var(--content-secondary-dark);
|
||||
--dropdown-header-text-color: var(--content-secondary-dark);
|
||||
--dropdown-text-hover: var(--content-primary-dark);
|
||||
--dropdown-background-active: var(--green-70);
|
||||
--dropdown-text-active: var(--green-10);
|
||||
--dropdown-border-divider: var(--border-divider-dark);
|
||||
--dropdown-border-width: 1px;
|
||||
--dropdown-border-color: var(--border-primary);
|
||||
}
|
||||
|
||||
:root {
|
||||
@include light-dropdown-menu;
|
||||
}
|
||||
|
||||
@include theme('default') {
|
||||
.ide-redesign-main,
|
||||
.project-ds-nav-page {
|
||||
@include dark-dropdown-menu;
|
||||
}
|
||||
}
|
||||
|
||||
$dropdown-item-min-height: 36px;
|
||||
|
||||
.dropdown {
|
||||
@@ -25,6 +70,7 @@ $dropdown-item-min-height: 36px;
|
||||
min-height: $dropdown-item-min-height; // a minimum height of 36px to be accessible for touch screens
|
||||
padding: var(--spacing-05) var(--spacing-06) var(--spacing-02)
|
||||
var(--spacing-04);
|
||||
color: var(--dropdown-header-text-color);
|
||||
}
|
||||
|
||||
.dropdown-menu.dropdown-menu-unpositioned {
|
||||
@@ -39,7 +85,9 @@ $dropdown-item-min-height: 36px;
|
||||
.dropdown-menu {
|
||||
@include shadow-md;
|
||||
|
||||
border: var(--dropdown-border-width) solid var(--dropdown-border-color);
|
||||
min-width: 240px;
|
||||
background-color: var(--dropdown-background);
|
||||
|
||||
&.dropdown-menu-sm-width {
|
||||
min-width: 160px;
|
||||
@@ -60,37 +108,40 @@ $dropdown-item-min-height: 36px;
|
||||
place-content: center start;
|
||||
min-height: $dropdown-item-min-height; // a minimum height of 36px to be accessible for touch screens
|
||||
position: relative;
|
||||
background-color: var(--dropdown-background);
|
||||
|
||||
&:active {
|
||||
background-color: var(--bg-accent-03);
|
||||
background-color: var(--dropdown-background-active);
|
||||
}
|
||||
|
||||
&,
|
||||
&:active,
|
||||
&:visited {
|
||||
color: var(--neutral-90);
|
||||
color: var(--dropdown-text-color);
|
||||
}
|
||||
|
||||
&:hover:not(.active),
|
||||
&:focus:not(.active),
|
||||
&.nested-dropdown-toggle-shown {
|
||||
background-color: var(--bg-light-secondary);
|
||||
color: var(--dropdown-text-hover);
|
||||
background-color: var(--dropdown-background-hover);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&[variant='danger'] {
|
||||
color: var(--content-danger);
|
||||
color: var(--dropdown-text-danger);
|
||||
|
||||
&:hover:not(.active),
|
||||
&:focus:not(.active) {
|
||||
background-color: var(--bg-danger-03);
|
||||
color: var(--dropdown-text-danger-hover);
|
||||
background-color: var(--dropdown-background-danger-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: var(--bg-accent-03);
|
||||
color: var(--green-70);
|
||||
background-color: var(--dropdown-background-active);
|
||||
color: var(--dropdown-text-active);
|
||||
}
|
||||
|
||||
&.btn-link {
|
||||
@@ -104,14 +155,14 @@ $dropdown-item-min-height: 36px;
|
||||
}
|
||||
|
||||
.dropdown-divider {
|
||||
border-top-color: var(--border-divider);
|
||||
border-top-color: var(--dropdown-border-divider);
|
||||
margin: var(--spacing-01) var(--spacing-03);
|
||||
}
|
||||
|
||||
.dropdown-item-description {
|
||||
@include body-xs;
|
||||
|
||||
color: var(--content-secondary);
|
||||
color: var(--dropdown-text-secondary);
|
||||
margin-top: var(--spacing-01);
|
||||
text-wrap: wrap;
|
||||
}
|
||||
@@ -163,7 +214,7 @@ $dropdown-item-min-height: 36px;
|
||||
// override disabled styles when the state is active
|
||||
.dropdown-item.active .dropdown-item-description {
|
||||
background-color: initial;
|
||||
color: var(--green-70);
|
||||
color: var(--dropdown-text-active);
|
||||
}
|
||||
|
||||
.dropdown-button-toggle {
|
||||
@@ -198,7 +249,7 @@ $dropdown-item-min-height: 36px;
|
||||
}
|
||||
|
||||
.dropdown-item-highlighted {
|
||||
background-color: var(--bg-light-secondary);
|
||||
background-color: var(--dropdown-background-hover);
|
||||
}
|
||||
|
||||
.dropdown-item-material-icon-small {
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
--bs-heading-color: var(--content-primary);
|
||||
}
|
||||
|
||||
.modal {
|
||||
// Until we add a dark modal version, all dropdowns in modals should use light theme
|
||||
@include light-dropdown-menu;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
.modal-dialog {
|
||||
@include modal-md;
|
||||
|
||||
@@ -203,6 +203,7 @@
|
||||
.dropdown-item {
|
||||
padding: var(--spacing-02) var(--spacing-06) var(--spacing-02)
|
||||
var(--spacing-08);
|
||||
background-color: transparent;
|
||||
|
||||
&:not(.disabled) {
|
||||
color: var(--navbar-hamburger-submenu-item-color);
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
:root {
|
||||
--select-background-highlighted: var(--bg-light-secondary);
|
||||
}
|
||||
|
||||
.select-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
@@ -11,5 +15,5 @@
|
||||
}
|
||||
|
||||
.select-highlighted {
|
||||
background-color: var(--bg-light-secondary);
|
||||
background-color: var(--select-background-highlighted);
|
||||
}
|
||||
|
||||
@@ -49,3 +49,6 @@
|
||||
// Module styles
|
||||
// TODO: find a way for modules to add styles dynamically
|
||||
@import 'modules/all';
|
||||
|
||||
// DS unified access styles
|
||||
@import 'ciam/all';
|
||||
|
||||
@@ -337,3 +337,62 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
cursor: default !important;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent !important;
|
||||
color: var(--content-primary-themed);
|
||||
}
|
||||
|
||||
legend {
|
||||
font-size: var(--font-size-02);
|
||||
line-height: var(--line-height-02);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-toggle-radios {
|
||||
display: flex;
|
||||
border-radius: var(--border-radius-full);
|
||||
background-color: var(--bg-secondary-themed);
|
||||
padding: var(--spacing-01);
|
||||
gap: var(--spacing-01);
|
||||
}
|
||||
|
||||
:root {
|
||||
--theme-toggle-selected-background: var(--green-70);
|
||||
}
|
||||
|
||||
@include theme('light') {
|
||||
--theme-toggle-selected-background: var(--green-20);
|
||||
}
|
||||
|
||||
.theme-toggle-radio {
|
||||
display: flex;
|
||||
|
||||
input {
|
||||
all: unset;
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
padding: var(--spacing-03);
|
||||
margin-bottom: 0;
|
||||
border-radius: var(--border-radius-full);
|
||||
cursor: pointer;
|
||||
color: var(--content-primary-themed);
|
||||
}
|
||||
|
||||
.material-symbols {
|
||||
font-size: var(--font-size-03);
|
||||
}
|
||||
|
||||
input:checked + label {
|
||||
background-color: var(--theme-toggle-selected-background);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +116,6 @@
|
||||
"bull": "^3.18.0",
|
||||
"bunyan": "^1.8.15",
|
||||
"cache-flow": "^1.9.0",
|
||||
"celebrate": "^15.0.3",
|
||||
"connect-redis": "^6.1.3",
|
||||
"content-disposition": "^0.5.0",
|
||||
"contentful": "^10.8.5",
|
||||
@@ -139,7 +138,6 @@
|
||||
"i18next": "^23.10.0",
|
||||
"i18next-fs-backend": "^2.3.1",
|
||||
"i18next-http-middleware": "^3.5.0",
|
||||
"joi": "^17.12.0",
|
||||
"jose": "^4.3.8",
|
||||
"json2csv": "^4.3.3",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
|
||||
@@ -17,15 +17,18 @@ import { batchedUpdate } from '@overleaf/mongo-utils/batchedUpdate.js'
|
||||
import { db } from '../../app/src/infrastructure/mongodb.js'
|
||||
import AccountMappingHelper from '../../app/src/Features/Analytics/AccountMappingHelper.js'
|
||||
import { registerAccountMapping } from '../../app/src/Features/Analytics/AnalyticsManager.js'
|
||||
import { triggerGracefulShutdown } from '../../app/src/infrastructure/GracefulShutdown.js'
|
||||
import { gracefulShutdown } from '../../app/src/infrastructure/GracefulShutdown.js'
|
||||
import Validation from '../../app/src/infrastructure/Validation.js'
|
||||
import { scriptRunner } from '../lib/ScriptRunner.mjs'
|
||||
|
||||
const paramsSchema = Validation.Joi.object({
|
||||
endDate: Validation.Joi.string().isoDate(),
|
||||
commit: Validation.Joi.boolean().default(false),
|
||||
verbose: Validation.Joi.boolean().default(false),
|
||||
}).unknown(true)
|
||||
const paramsSchema = Validation.z.object({
|
||||
endDate: Validation.z.iso
|
||||
.date()
|
||||
.transform(v => new Date(v).toISOString())
|
||||
.optional(),
|
||||
commit: Validation.z.boolean().default(false).optional(),
|
||||
verbose: Validation.z.boolean().default(false).optional(),
|
||||
})
|
||||
|
||||
let mapped = 0
|
||||
let subscriptionCount = 0
|
||||
@@ -96,33 +99,31 @@ async function main(trackProgress) {
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
error,
|
||||
value: { commit, endDate, verbose },
|
||||
} = paramsSchema.validate(
|
||||
const { error, data } = paramsSchema.safeParse(
|
||||
minimist(process.argv.slice(2), {
|
||||
boolean: ['commit', 'verbose'],
|
||||
string: ['endDate'],
|
||||
})
|
||||
)
|
||||
|
||||
logger.logger.level(verbose ? 'debug' : 'info')
|
||||
|
||||
if (error) {
|
||||
logger.error({ error }, 'error with parameters')
|
||||
triggerGracefulShutdown({
|
||||
close(done) {
|
||||
logger.info({}, 'shutting down')
|
||||
done(1)
|
||||
},
|
||||
})
|
||||
} else {
|
||||
logger.info({ verbose, commit, endDate }, commit ? 'COMMITTING' : 'DRY RUN')
|
||||
await scriptRunner(main)
|
||||
triggerGracefulShutdown({
|
||||
await gracefulShutdown({
|
||||
close(done) {
|
||||
logger.info({}, 'shutting down')
|
||||
done()
|
||||
},
|
||||
})
|
||||
process.exit(1)
|
||||
}
|
||||
const { commit, endDate, verbose } = data
|
||||
logger.logger.level(verbose ? 'debug' : 'info')
|
||||
logger.info({ verbose, commit, endDate }, commit ? 'COMMITTING' : 'DRY RUN')
|
||||
await scriptRunner(main)
|
||||
await gracefulShutdown({
|
||||
close(done) {
|
||||
logger.info({}, 'shutting down')
|
||||
done()
|
||||
},
|
||||
})
|
||||
process.exit()
|
||||
|
||||
@@ -48,7 +48,8 @@ async function setAllowDowngradeToFalse() {
|
||||
}
|
||||
|
||||
async function deleteHistoryCollections() {
|
||||
await gracefullyDropCollection(db.docHistory)
|
||||
const docHistory = await getCollectionInternal('docHistory')
|
||||
await gracefullyDropCollection(docHistory)
|
||||
await gracefullyDropCollection(db.docHistoryIndex)
|
||||
const projectHistoryMetaData = await getCollectionInternal(
|
||||
'projectHistoryMetaData'
|
||||
|
||||
@@ -289,6 +289,32 @@ describe('<EmailsSection />', function () {
|
||||
await screen.findByRole('button', { name: 'Link accounts and add email' })
|
||||
})
|
||||
|
||||
it('prevents user from linking to same SSO institution twice', async function () {
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', [
|
||||
{ email: 'bar@autocomplete.edu', samlProviderId: '1234' },
|
||||
])
|
||||
render(<EmailsSection />)
|
||||
|
||||
const button = await screen.findByRole<HTMLButtonElement>('button', {
|
||||
name: 'Add another email',
|
||||
})
|
||||
|
||||
await fetchMock.callHistory.flush(true)
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
fetchMock.get('express:/institutions/domains', institutionDomainData)
|
||||
|
||||
await userEvent.click(button)
|
||||
|
||||
const input = screen.getByRole('textbox', { name: 'Email' })
|
||||
fireEvent.change(input, {
|
||||
target: { value: 'baz@autocomplete.edu' },
|
||||
})
|
||||
|
||||
await screen.findByText(
|
||||
'This institution is already linked with your account via another email address.'
|
||||
)
|
||||
})
|
||||
|
||||
it('adds new email address with existing institution and custom departments', async function () {
|
||||
const country = 'Germany'
|
||||
const customDepartment = 'Custom department'
|
||||
|
||||
@@ -852,7 +852,7 @@ describe('ClsiManager', function () {
|
||||
ctx.project._id,
|
||||
ctx.user_id,
|
||||
{
|
||||
compileBackendClass: 'c2d',
|
||||
compileBackendClass: 'c4d',
|
||||
compileGroup: 'priority',
|
||||
}
|
||||
)
|
||||
@@ -868,7 +868,7 @@ describe('ClsiManager', function () {
|
||||
url.host === CLSI_HOST &&
|
||||
url.pathname ===
|
||||
`/project/${ctx.project._id}/user/${ctx.user_id}/compile` &&
|
||||
url.searchParams.get('compileBackendClass') === 'c2d' &&
|
||||
url.searchParams.get('compileBackendClass') === 'c4d' &&
|
||||
url.searchParams.get('compileGroup') === 'priority'
|
||||
)
|
||||
)
|
||||
@@ -885,7 +885,7 @@ describe('ClsiManager', function () {
|
||||
ctx.AnalyticsManager.recordEventForUserInBackground
|
||||
).to.have.been.calledWith(ctx.user_id, 'double-compile-result', {
|
||||
projectId: 'project-id',
|
||||
compileBackendClass: 'c2d',
|
||||
compileBackendClass: 'c4d',
|
||||
newCompileBackendClass: 'n4',
|
||||
status: 'success',
|
||||
compileTime: 1337,
|
||||
@@ -1055,7 +1055,7 @@ describe('ClsiManager', function () {
|
||||
await ctx.ClsiManager.promises.deleteAuxFiles(
|
||||
ctx.project._id,
|
||||
ctx.user_id,
|
||||
{ compileBackendClass: 'c2d', compileGroup: 'priority' },
|
||||
{ compileBackendClass: 'c4d', compileGroup: 'priority' },
|
||||
'node-1'
|
||||
)
|
||||
// wait for the background task to finish
|
||||
@@ -1065,7 +1065,7 @@ describe('ClsiManager', function () {
|
||||
it('should clear both cookies', function (ctx) {
|
||||
expect(
|
||||
ctx.ClsiCookieManager.promises.clearServerId
|
||||
).to.have.been.calledWith(ctx.project._id, ctx.user_id, 'c2d')
|
||||
).to.have.been.calledWith(ctx.project._id, ctx.user_id, 'c4d')
|
||||
expect(
|
||||
ctx.ClsiCookieManager.promises.clearServerId
|
||||
).to.have.been.calledWith(ctx.project._id, ctx.user_id, 'n4')
|
||||
@@ -1078,7 +1078,7 @@ describe('ClsiManager', function () {
|
||||
url.host === CLSI_HOST &&
|
||||
url.pathname ===
|
||||
`/project/${ctx.project._id}/user/${ctx.user_id}` &&
|
||||
url.searchParams.get('compileBackendClass') === 'c2d' &&
|
||||
url.searchParams.get('compileBackendClass') === 'c4d' &&
|
||||
url.searchParams.get('compileGroup') === 'priority' &&
|
||||
url.searchParams.get('clsiserverid') === 'node-1'
|
||||
),
|
||||
@@ -1168,7 +1168,7 @@ describe('ClsiManager', function () {
|
||||
ctx.project._id,
|
||||
ctx.user_id,
|
||||
false,
|
||||
{ compileBackendClass: 'c2d', compileGroup: 'priority' },
|
||||
{ compileBackendClass: 'c4d', compileGroup: 'priority' },
|
||||
'node-1'
|
||||
)
|
||||
// wait for the background task to finish
|
||||
@@ -1180,7 +1180,7 @@ describe('ClsiManager', function () {
|
||||
sinon.match(
|
||||
url =>
|
||||
url.toString() ===
|
||||
`http://clsi.example.com/project/${ctx.project._id}/user/${ctx.user_id}/wordcount?compileBackendClass=c2d&compileGroup=priority&file=main.tex&image=mock-image-name&clsiserverid=node-1`
|
||||
`http://clsi.example.com/project/${ctx.project._id}/user/${ctx.user_id}/wordcount?compileBackendClass=c4d&compileGroup=priority&file=main.tex&image=mock-image-name&clsiserverid=node-1`
|
||||
)
|
||||
)
|
||||
expect(ctx.FetchUtils.fetchStringWithResponse).to.have.been.calledWith(
|
||||
|
||||
@@ -783,7 +783,7 @@ describe('CompileController', function () {
|
||||
.stub()
|
||||
.resolves({
|
||||
compileGroup: 'priority',
|
||||
compileBackendClass: 'c2d',
|
||||
compileBackendClass: 'c4d',
|
||||
})
|
||||
await ctx.CompileController._proxyToClsi(
|
||||
ctx.projectId,
|
||||
@@ -798,7 +798,7 @@ describe('CompileController', function () {
|
||||
|
||||
it('should open a request to the CLSI', function (ctx) {
|
||||
ctx.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
|
||||
`${ctx.settings.apis.clsi.url}${ctx.url}?compileGroup=priority&compileBackendClass=c2d`
|
||||
`${ctx.settings.apis.clsi.url}${ctx.url}?compileGroup=priority&compileBackendClass=c4d`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -254,7 +254,7 @@ describe('CompileManager', function () {
|
||||
.calledWith(null, {
|
||||
timeout: ctx.timeout,
|
||||
compileGroup: ctx.group,
|
||||
compileBackendClass: 'c2d',
|
||||
compileBackendClass: 'c4d',
|
||||
ownerAnalyticsId: 'abc',
|
||||
})
|
||||
.should.equal(true)
|
||||
@@ -284,7 +284,7 @@ describe('CompileManager', function () {
|
||||
await ctx.CompileManager.promises.getProjectCompileLimits(
|
||||
ctx.project_id
|
||||
)
|
||||
expect(compileBackendClass).to.equal('c2d')
|
||||
expect(compileBackendClass).to.equal('c4d')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -264,17 +264,6 @@ describe('SubscriptionController', function () {
|
||||
})
|
||||
)
|
||||
|
||||
vi.doMock('celebrate', () => ({
|
||||
default: (ctx.celebrate = {
|
||||
celebrate: sinon.stub(),
|
||||
errors: sinon.stub(),
|
||||
Joi: {
|
||||
any: sinon.stub(),
|
||||
extend: sinon.stub(),
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.doMock(
|
||||
'../../../../app/src/Features/Subscription/GroupPlansData',
|
||||
() => ({
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
const { Joi } = require('../../../../app/src/infrastructure/Validation')
|
||||
const { ObjectId } = require('mongodb-legacy')
|
||||
const { expect } = require('chai')
|
||||
const { ValidationError } = require('joi')
|
||||
|
||||
describe('Validation', function () {
|
||||
const validObjectId = '123456781234567812345678'
|
||||
const invalidObjectId = '12345678-1234-1234-12345678'
|
||||
|
||||
it('accepts valid ObjectId strings', async function () {
|
||||
const schema = Joi.object({
|
||||
test: Joi.objectId(),
|
||||
})
|
||||
|
||||
const value = await schema.validateAsync({
|
||||
test: validObjectId,
|
||||
})
|
||||
|
||||
expect(value.test).to.be.instanceof(ObjectId)
|
||||
expect(value.test.toHexString()).to.equal(validObjectId)
|
||||
})
|
||||
|
||||
it('rejects invalid ObjectId strings', async function () {
|
||||
const schema = Joi.object({
|
||||
test: Joi.objectId(),
|
||||
})
|
||||
|
||||
const promise = schema.validateAsync({
|
||||
test: invalidObjectId,
|
||||
})
|
||||
|
||||
expect(promise).to.be.rejectedWith(ValidationError)
|
||||
})
|
||||
|
||||
it('accepts valid ObjectId objects', async function () {
|
||||
const schema = Joi.object({
|
||||
test: Joi.objectId(),
|
||||
})
|
||||
|
||||
const value = await schema.validateAsync({
|
||||
test: new ObjectId(validObjectId),
|
||||
})
|
||||
|
||||
expect(value.test).to.be.instanceof(ObjectId)
|
||||
expect(value.test.toHexString()).to.equal(validObjectId)
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
|
||||
import Helpers from './lib/helpers.mjs'
|
||||
import { getCollectionInternal } from './lib/mongodb.mjs'
|
||||
|
||||
const tags = ['server-ce', 'server-pro', 'saas']
|
||||
|
||||
@@ -42,16 +43,16 @@ const indexes = [
|
||||
]
|
||||
|
||||
const migrate = async client => {
|
||||
const { db } = client
|
||||
const docHistory = await getCollectionInternal('docHistory')
|
||||
|
||||
await Helpers.addIndexesToCollection(db.docHistory, indexes)
|
||||
await Helpers.addIndexesToCollection(docHistory, indexes)
|
||||
}
|
||||
|
||||
const rollback = async client => {
|
||||
const { db } = client
|
||||
const docHistory = await getCollectionInternal('docHistory')
|
||||
|
||||
try {
|
||||
await Helpers.dropIndexesFromCollection(db.docHistory, indexes)
|
||||
await Helpers.dropIndexesFromCollection(docHistory, indexes)
|
||||
} catch (err) {
|
||||
console.error('Something went wrong rolling back the migrations', err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import Helpers from './lib/helpers.mjs'
|
||||
|
||||
const tags = ['saas']
|
||||
|
||||
const migrate = async () => {
|
||||
await Helpers.dropCollection('docHistory')
|
||||
}
|
||||
|
||||
const rollback = async () => {
|
||||
// Can't really do anything here
|
||||
}
|
||||
|
||||
export default {
|
||||
tags,
|
||||
migrate,
|
||||
rollback,
|
||||
}
|
||||
@@ -18,7 +18,6 @@ export const db = {
|
||||
deletedUsers: internalDb.collection('deletedUsers'),
|
||||
dropboxEntities: internalDb.collection('dropboxEntities'),
|
||||
dropboxProjects: internalDb.collection('dropboxProjects'),
|
||||
docHistory: internalDb.collection('docHistory'),
|
||||
docHistoryIndex: internalDb.collection('docHistoryIndex'),
|
||||
docSnapshots: internalDb.collection('docSnapshots'),
|
||||
docs: internalDb.collection('docs'),
|
||||
|
||||
Reference in New Issue
Block a user