5 Commits

Author SHA1 Message Date
Miguel Serrano
3bba9c9fb7 Merge pull request #28631 from overleaf/msm-group-audit-log-filter
[web] Group Audit Log filter/pagination

GitOrigin-RevId: 440ad8bef3d1734613884a4e252350eee603af27
2025-09-26 08:05:59 +00:00
Rebeka Dekany
3b5ea89a1c Update loading spinner status (#28177)
* Update test for the loading spinner component

* Create a story for the loading spinner component

* Move role and use CSS for spacing instead

* Add a classname prop

* Reuse LoadingSpinner

* Use OLSpinner instead of Spinner

* Use data-testid since status role was moved

* Wait for journals to load

* Use `isLoading` prop instead and fix the button's height

* Use `isLoading` prop instead

* Use LoadingSpinner instead and remove spacing

* Update test for the loading spinner component

* Use `isLoading` prop instead

* Add aria-describedby to layout button for processing state

* Replace `spinner` to `ol-spinner`

* Scope status

* Remove redundant `div.loading`

---------

Co-authored-by: Antoine Clausse <antoine.clausse@overleaf.com>
GitOrigin-RevId: 8f43b991f8f458b2abd5a4661913ac9b972d892a
2025-09-26 08:05:46 +00:00
Rebeka Dekany
aebff54a6b Improvement to OLButton loading labels (#28659)
* Create eslint rule for requiring loadingLabel prop when isLoading is specified on OLButton

* Add `loadingLabel` props for OLButton components with `isLoading`

* Clarify loading label and button loading state

GitOrigin-RevId: 89279d5b4c346f9c3b67a59d0db822a2ff04314a
2025-09-26 08:05:41 +00:00
Davinder Singh
ab84e48545 Introducing group discount banner on plans page (#28666)
* adding placeholder mixin for the group discount banner

* adding desktop version

* adding mobile size screen content

* format:fix

* adding variable

* adding new copy

* moving the mixin in the correct place

* adding translations and lint:fix

GitOrigin-RevId: 23e72ffd5d25b474fa3bbbde396dc7a75748fb48
2025-09-26 08:05:36 +00:00
Olzhas Askar
18caa66bdf Merge pull request #28673 from overleaf/oa-signup-date
[web] Expose signup date for Customer.IO

GitOrigin-RevId: 950b396e154e495071b104ea86e2441052df4bfb
2025-09-26 08:05:31 +00:00
61 changed files with 268 additions and 224 deletions

View File

@@ -210,7 +210,7 @@ export function openFile(fileName: string, waitFor: string) {
.click({ force: true })
// wait until we've switched to the selected file
cy.findByText('Loading…').should('not.exist')
cy.findByRole('status').should('not.exist')
cy.findByText(waitFor)
}
@@ -230,7 +230,10 @@ export function createNewFile() {
.click({ force: true })
// wait until we've switched to the newly created empty file
cy.findByText('Loading').should('not.exist')
cy.findByRole('textbox', { name: 'Source Editor editing' }).within(() => {
cy.findByRole('status').should('not.exist')
cy.get('.cm-line').should('have.length', 1)
})
cy.get('.cm-line').should('have.length', 1)
return fileName

View File

@@ -531,6 +531,7 @@ module.exports = {
rules: {
'@overleaf/no-unnecessary-trans': 'error',
'@overleaf/should-unescape-trans': 'error',
'@overleaf/require-loading-label': 'error',
// https://astexplorer.net/
'no-restricted-syntax': [

View File

@@ -536,6 +536,7 @@ async function projectListPage(req, res, next) {
aiBlocked,
hasAiAssist,
lastActive: user.lastActive,
signUpDate: user.signUpDate,
})
}

View File

@@ -3,7 +3,7 @@ if(customerIoEnabled && ExposedSettings.cioWriteKey && ExposedSettings.cioSiteId
function boolAttr(value) {
return value !== undefined ? String(value) : null;
}
script(type="text/javascript", id="cio-loader", nonce=scriptNonce, data-best-subscription=(usersBestSubscription && usersBestSubscription.type), data-ai-blocked=boolAttr(aiBlocked), data-has-ai-assist=boolAttr(hasAiAssist), data-cio-write-key=ExposedSettings.cioWriteKey, data-cio-site-id=ExposedSettings.cioSiteId, data-session-analytics-id=getSessionAnalyticsId(), data-user-id=getLoggedInUserId(), data-last-active=lastActive).
script(type="text/javascript", id="cio-loader", nonce=scriptNonce, data-best-subscription=(usersBestSubscription && usersBestSubscription.type), data-ai-blocked=boolAttr(aiBlocked), data-has-ai-assist=boolAttr(hasAiAssist), data-cio-write-key=ExposedSettings.cioWriteKey, data-cio-site-id=ExposedSettings.cioSiteId, data-session-analytics-id=getSessionAnalyticsId(), data-user-id=getLoggedInUserId(), data-last-active=lastActive, data-sign-up-date=signUpDate).
function parseBool(value) {
return value === 'true' ? true : value === 'false' ? false : undefined;
@@ -18,6 +18,7 @@ if(customerIoEnabled && ExposedSettings.cioWriteKey && ExposedSettings.cioSiteId
var aiBlocked = parseBool(cioSettings.aiBlocked);
var hasAiAssist = parseBool(cioSettings.hasAiAssist);
var lastActive = cioSettings.lastActive;
var signUpDate = cioSettings.signUpDate;
!function(){var i="cioanalytics", analytics=(window[i]=window[i]||[]);if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","reset","group","track","ready","alias","debug","page","once","off","on","addSourceMiddleware","addIntegrationMiddleware","setAnonymousId","addDestinationMiddleware"];analytics.factory=function(e){return function(){var t=Array.prototype.slice.call(arguments);t.unshift(e);analytics.push(t);return analytics}};for(var e=0;e<analytics.methods.length;e++){var key=analytics.methods[e];analytics[key]=analytics.factory(key)}analytics.load=function(key,e){var t=document.createElement("script");t.type="text/javascript";t.async=!0;t.setAttribute('data-global-customerio-analytics-key', i);t.src="https://cdp.customer.io/v1/analytics-js/snippet/" + key + "/analytics.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(t,n);analytics._writeKey=key;analytics._loadOptions=e};analytics.SNIPPET_VERSION="4.15.3";
@@ -40,7 +41,7 @@ if(customerIoEnabled && ExposedSettings.cioWriteKey && ExposedSettings.cioSiteId
if (typeof value !== 'undefined') obj[key] = value;
}
var identifyData = {overleafId: userId, lastActive};
var identifyData = {overleafId: userId, lastActive, signUpDate};
addIfDefined(identifyData, 'best-subscription-type', usersBestSubscription);
addIfDefined(identifyData, 'aiBlocked', aiBlocked);
addIfDefined(identifyData, 'hasAiAssist', hasAiAssist);

View File

@@ -45,6 +45,7 @@
"accept_selected_changes": "",
"accept_terms_and_conditions": "",
"accepted_invite": "",
"accepting": "",
"access_all_premium_features": "",
"access_all_premium_features_including_more_collaborators_real_time_track_changes_and_a_longer_compile_time": "",
"access_denied": "",
@@ -125,6 +126,7 @@
"alignment": "",
"all": "",
"all_borders": "",
"all_events": "",
"all_features_in_group_standard_plus": "",
"all_logs": "",
"all_premium_features": "",
@@ -267,7 +269,9 @@
"cite_directly_or_import_references": "",
"cite_faster": "",
"clear_cached_files": "",
"clear_filters": "",
"clear_search": "",
"clearing": "",
"click_here_to_view_sl_in_lng": "",
"click_to_unpause": "",
"clicking_delete_will_remove_sso_config_and_clear_saml_data": "",
@@ -359,6 +363,7 @@
"create_project_in_github": "",
"created": "",
"created_at": "",
"creating": "",
"cross_reference": "",
"current_file": "",
"current_password": "",
@@ -540,6 +545,7 @@
"enables_real_time_syntax_checking_in_the_editor": "",
"enabling": "",
"end_of_document": "",
"end_time_utc": "",
"ensure_recover_account": "",
"enter_any_size_including_units_or_valid_latex_command": "",
"enter_the_code": "",
@@ -556,6 +562,7 @@
"error_processing_file": "",
"errors": "",
"essential_cookies_only": "",
"event_type": "",
"example_project": "",
"existing_plan_active_until_term_end": "",
"expand": "",
@@ -573,6 +580,7 @@
"fair_usage_policy_applies": "",
"fast": "",
"fast_draft": "",
"feature_enabled_or_disabled": "",
"features_like_track_changes": "",
"feedback": "",
"figure": "",
@@ -646,6 +654,7 @@
"generate_from_text_or_image": "",
"generate_tables_and_equations": "",
"generate_token": "",
"generating": "",
"generic_if_problem_continues_contact_us": "",
"generic_linked_file_compile_error": "",
"generic_something_went_wrong": "",
@@ -1177,7 +1186,6 @@
"open_pdf_in_separate_tab": "",
"open_project": "",
"open_target": "",
"operation": "",
"optional": "",
"or": "",
"organization_name": "",
@@ -1393,9 +1401,11 @@
"recompile_from_scratch": "",
"recompile_pdf": "",
"reconfirm_secondary_email": "",
"reconfirming": "",
"reconnect": "",
"reconnecting": "",
"reconnecting_in_x_secs": "",
"recovering": "",
"recurly_email_update_needed": "",
"recurly_email_updated": "",
"redirect_to_editor": "",
@@ -1417,6 +1427,7 @@
"refresh_page_after_starting_free_trial": "",
"refreshing": "",
"regards": "",
"registering": "",
"reject_change": "",
"reject_selected_changes": "",
"relink_your_account": "",
@@ -1471,9 +1482,11 @@
"restore_file_version": "",
"restore_project_to_this_version": "",
"restore_this_version": "",
"restoring": "",
"resync_completed": "",
"resync_message": "",
"resync_project_history": "",
"resyncing": "",
"retry_test": "",
"reverse_x_sort_order": "",
"revert_pending_plan_change": "",
@@ -1576,6 +1589,7 @@
"send_first_message": "",
"send_message": "",
"send_request": "",
"sending": "",
"server_error": "",
"server_pro_license_entitlement_line_1": "",
"server_pro_license_entitlement_line_2": "",
@@ -1702,6 +1716,7 @@
"start_free_trial": "",
"start_free_trial_without_exclamation": "",
"start_the_conversation_by_saying_hello_or_sharing_an_update": "",
"start_time_utc": "",
"start_typing_find_your_company": "",
"start_typing_find_your_organization": "",
"start_typing_find_your_university": "",
@@ -1721,6 +1736,7 @@
"subject_area": "",
"subject_to_additional_vat": "",
"submit_title": "",
"submitting": "",
"subscribe": "",
"subscribe_to_find_the_symbols_you_need_faster": "",
"subscribe_to_plan": "",
@@ -2005,6 +2021,7 @@
"unlinking": "",
"unmerge_cells": "",
"unpause_subscription": "",
"unpausing": "",
"unpublish": "",
"unpublishing": "",
"unsubscribe": "",

View File

@@ -2,8 +2,8 @@ import OLFormGroup from '@/shared/components/ol/ol-form-group'
import OLFormLabel from '@/shared/components/ol/ol-form-label'
import OLFormSelect from '@/shared/components/ol/ol-form-select'
import { ChangeEventHandler, useCallback, useEffect, useRef } from 'react'
import { Spinner } from 'react-bootstrap'
import { useEditorLeftMenuContext } from '@/features/editor-left-menu/components/editor-left-menu-context'
import OLSpinner from '@/shared/components/ol/ol-spinner'
type PossibleValue = string | number | boolean
@@ -82,14 +82,7 @@ export default function SettingsMenuSelect<T extends PossibleValue = string>({
>
<OLFormLabel>{label}</OLFormLabel>
{loading ? (
<p className="mb-0">
<Spinner
animation="border"
aria-hidden="true"
size="sm"
role="status"
/>
</p>
<OLSpinner size="sm" />
) : (
<OLFormSelect
size="sm"

View File

@@ -1,5 +1,4 @@
import { memo, useCallback, forwardRef } from 'react'
import { Spinner } from 'react-bootstrap'
import {
Dropdown,
DropdownItem,
@@ -18,6 +17,7 @@ import useEventListener from '../../../shared/hooks/use-event-listener'
import { DetachRole } from '@/shared/context/detach-context'
import MaterialIcon from '@/shared/components/material-icon'
import OLTooltip from '@/shared/components/ol/ol-tooltip'
import OLSpinner from '@/shared/components/ol/ol-spinner'
const isActiveDropdownItem = ({
iconFor,
@@ -171,24 +171,18 @@ export const LayoutDropdownButtonUi = ({
const { t } = useTranslation()
return (
<>
{processing && (
<div aria-live="assertive" className="visually-hidden">
{t('layout_processing')}
</div>
)}
<div aria-live="assertive" className="visually-hidden" id="layout-status">
{processing ? t('layout_processing') : ''}
</div>
<Dropdown className="toolbar-item layout-dropdown" align="end">
<DropdownToggle
aria-describedby={processing ? 'layout-status' : undefined}
id="layout-dropdown-btn"
className="btn-full-height"
as={LayoutDropdownToggleButton}
>
{processing ? (
<Spinner
animation="border"
aria-hidden="true"
size="sm"
role="status"
/>
<OLSpinner size="sm" />
) : (
<MaterialIcon type="dock_to_right" className="align-middle" />
)}
@@ -257,15 +251,7 @@ export const LayoutDropdownButtonUi = ({
detachIsLinked ? (
'check'
) : (
<span className="spinner-container">
<Spinner
animation="border"
aria-hidden="true"
size="sm"
role="status"
/>
<span className="visually-hidden">{t('loading')}</span>
</span>
<OLSpinner size="sm" />
)
) : null
}

View File

@@ -4,7 +4,6 @@ import { useIdeRedesignSwitcherContext } from '../ide-react/context/ide-redesign
import { useTranslation } from 'react-i18next'
import { canUseNewEditorViaPrimaryFeatureFlag } from '../ide-redesign/utils/new-editor-utils'
import { useSwitchEnableNewEditorState } from '../ide-redesign/hooks/use-switch-enable-new-editor-state'
import { Spinner } from 'react-bootstrap'
const TryNewEditorButton = () => {
const { t } = useTranslation()
@@ -27,17 +26,9 @@ const TryNewEditorButton = () => {
onClick={onClick}
size="sm"
variant="secondary"
isLoading={loading}
>
{loading ? (
<Spinner
animation="border"
aria-hidden="true"
size="sm"
role="status"
/>
) : (
t('try_the_new_editor')
)}
{t('try_the_new_editor')}
</OLButton>
</div>
)

View File

@@ -77,6 +77,7 @@ export function FileTreeModalCreateFileFooterContent({
form="create-file"
disabled={inFlight || !valid}
isLoading={inFlight}
loadingLabel={t('creating')}
>
{t('create')}
</OLButton>

View File

@@ -24,7 +24,7 @@ import OLFormGroup from '@/shared/components/ol/ol-form-group'
import OLFormLabel from '@/shared/components/ol/ol-form-label'
import OLForm from '@/shared/components/ol/ol-form'
import OLFormSelect from '@/shared/components/ol/ol-form-select'
import { Spinner } from 'react-bootstrap'
import OLSpinner from '@/shared/components/ol/ol-spinner'
export default function FileTreeImportFromProject() {
const { t } = useTranslation()
@@ -209,20 +209,8 @@ function SelectProject({
return (
<OLFormGroup controlId="project-select">
<OLFormLabel>{t('select_a_project')}</OLFormLabel>
{loading && (
<span>
&nbsp;
<Spinner
animation="border"
aria-hidden="true"
size="sm"
role="status"
/>
</span>
)}
<OLFormLabel className="me-1">{t('select_a_project')}</OLFormLabel>
{loading && <OLSpinner size="sm" />}
<OLFormSelect
disabled={!data}
value={selectedProject ? selectedProject._id : ''}
@@ -275,20 +263,8 @@ function SelectProjectOutputFile({
className="row-spaced-small"
controlId="project-output-file-select"
>
<OLFormLabel>{t('select_an_output_file')}</OLFormLabel>
{loading && (
<span>
&nbsp;
<Spinner
animation="border"
aria-hidden="true"
size="sm"
role="status"
/>
</span>
)}
<OLFormLabel className="me-1">{t('select_an_output_file')}</OLFormLabel>
{loading && <OLSpinner size="sm" />}
<OLFormSelect
disabled={!data}
value={selectedProjectOutputFile?.path || ''}
@@ -334,20 +310,8 @@ function SelectProjectEntity({
return (
<OLFormGroup className="row-spaced-small" controlId="project-entity-select">
<OLFormLabel>{t('select_a_file')}</OLFormLabel>
{loading && (
<span>
&nbsp;
<Spinner
animation="border"
aria-hidden="true"
size="sm"
role="status"
/>
</span>
)}
<OLFormLabel className="me-1">{t('select_a_file')}</OLFormLabel>
{loading && <OLSpinner size="sm" />}
<OLFormSelect
disabled={!data}
value={selectedProjectEntity?.path || ''}

View File

@@ -85,7 +85,12 @@ function FileTreeModalCreateFolder() {
<OLModalFooter>
{inFlight ? (
<OLButton variant="primary" disabled isLoading={inFlight} />
<OLButton
variant="primary"
disabled
isLoading={inFlight}
loadingLabel={t('creating')}
/>
) : (
<>
<OLButton variant="secondary" onClick={handleHide}>

View File

@@ -59,7 +59,12 @@ function FileTreeModalDelete() {
<OLModalFooter>
{inFlight ? (
<OLButton variant="danger" disabled isLoading />
<OLButton
variant="danger"
disabled
isLoading
loadingLabel={t('deleting')}
/>
) : (
<>
<OLButton variant="secondary" onClick={handleHide}>

View File

@@ -265,6 +265,7 @@ export function ManagersTable({
variant="primary"
onClick={addManagers}
isLoading={inviteUserInflightCount > 0}
loadingLabel={t('adding')}
>
{t('add')}
</OLButton>

View File

@@ -19,8 +19,8 @@ import { useGroupMembersContext } from '../../context/group-members-context'
import getMeta from '@/utils/meta'
import MaterialIcon from '@/shared/components/material-icon'
import DropdownListItem from '@/shared/components/dropdown/dropdown-list-item'
import { Spinner } from 'react-bootstrap'
import { sendMB } from '@/infrastructure/event-tracking'
import OLSpinner from '@/shared/components/ol/ol-spinner'
type resendInviteResponse = {
success: boolean
@@ -322,16 +322,7 @@ function MenuItemButton({
as="button"
tabIndex={-1}
onClick={onClick}
leadingIcon={
isLoading ? (
<Spinner
animation="border"
aria-hidden="true"
size="sm"
role="status"
/>
) : null
}
leadingIcon={isLoading ? <OLSpinner size="sm" /> : null}
data-testid={dataTestId}
variant={variant}
>

View File

@@ -128,6 +128,7 @@ export default function RemoveManagedUserModal({
variant="danger"
disabled={isLoading || isSuccess || !shouldEnableRemoveUserButton}
isLoading={isLoading}
loadingLabel={t('removing')}
>
{t('remove_user')}
</OLButton>

View File

@@ -118,6 +118,7 @@ function AddLabelModal({ show, setShow, version }: AddLabelModalProps) {
variant="primary"
disabled={isLoading || !comment.length}
isLoading={isLoading}
loadingLabel={t('adding')}
>
{t('history_add_label')}
</OLButton>

View File

@@ -122,6 +122,7 @@ const ChangeTag = forwardRef<HTMLElement, TagProps>(
variant="danger"
disabled={isLoading}
isLoading={isLoading}
loadingLabel={t('deleting')}
onClick={localDeleteHandler}
>
{t('history_delete_label')}

View File

@@ -51,6 +51,7 @@ export const RestoreProjectModal = ({
onClick={onRestore}
disabled={isRestoring}
isLoading={isRestoring}
loadingLabel={t('restoring')}
>
{t('restore')}
</OLButton>

View File

@@ -20,6 +20,7 @@ export default function ToolbarRestoreFileButton({
size="sm"
className="history-react-toolbar-restore-file-button"
isLoading={isLoading}
loadingLabel={t('restoring')}
onClick={() => restoreDeletedFile(selection)}
>
{t('restore_file')}

View File

@@ -37,6 +37,7 @@ function ToolbarRestoreFileToVersionButton({
variant="secondary"
size="sm"
isLoading={isLoading}
loadingLabel={t('restoring')}
onClick={() => setShowConfirmModal(true)}
>
{t('restore_file_version')}

View File

@@ -2,7 +2,7 @@ import OLFormSelect from '@/shared/components/ol/ol-form-select'
import { ChangeEventHandler, useCallback } from 'react'
import Setting from './setting'
import classNames from 'classnames'
import { Spinner } from 'react-bootstrap'
import OLSpinner from '@/shared/components/ol/ol-spinner'
type PossibleValue = string | number | boolean
@@ -64,12 +64,7 @@ export default function DropdownSetting<T extends PossibleValue = string>({
return (
<Setting controlId={id} label={label} description={description}>
{loading ? (
<Spinner
animation="border"
aria-hidden="true"
size="sm"
role="status"
/>
<OLSpinner size="sm" />
) : (
<OLFormSelect
id={id}

View File

@@ -11,8 +11,8 @@ import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import useEventListener from '@/shared/hooks/use-event-listener'
import { DetachRole } from '@/shared/context/detach-context'
import { Spinner } from 'react-bootstrap'
import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
import OLSpinner from '@/shared/components/ol/ol-spinner'
type LayoutOption = 'sideBySide' | 'editorOnly' | 'pdfOnly' | 'detachedPdf'
@@ -65,9 +65,7 @@ const LayoutDropdownItem = ({
}) => {
let trailingIcon: string | React.ReactNode | null = null
if (processing) {
trailingIcon = (
<Spinner animation="border" aria-hidden="true" size="sm" role="status" />
)
trailingIcon = <OLSpinner size="sm" />
} else if (active) {
trailingIcon = 'check'
}

View File

@@ -36,6 +36,7 @@ function DetachCompileButton() {
})}
size="sm"
isLoading={compiling}
loadingLabel={t('compiling')}
>
{t('recompile')}
</OLButton>

View File

@@ -9,7 +9,7 @@ import PdfHybridDownloadButton from './pdf-hybrid-download-button'
import PdfHybridCodeCheckButton from './pdf-hybrid-code-check-button'
import PdfOrphanRefreshButton from './pdf-orphan-refresh-button'
import { DetachedSynctexControl } from './detach-synctex-control'
import { Spinner } from 'react-bootstrap'
import LoadingSpinner from '@/shared/components/loading-spinner'
const ORPHAN_UI_TIMEOUT_MS = 5000
@@ -91,14 +91,7 @@ function PdfPreviewHybridToolbarConnectingInner() {
return (
<>
<div className="toolbar-pdf-orphan">
<Spinner
animation="border"
aria-hidden="true"
size="sm"
role="status"
/>
&nbsp;
{t('tab_connecting')}
<LoadingSpinner size="sm" loadingText={`${t('tab_connecting')}`} />
</div>
</>
)

View File

@@ -6,10 +6,10 @@ import { useTranslation } from 'react-i18next'
import OLTooltip from '@/shared/components/ol/ol-tooltip'
import OLButton from '@/shared/components/ol/ol-button'
import MaterialIcon from '@/shared/components/material-icon'
import { Spinner } from 'react-bootstrap'
import { Placement } from 'react-bootstrap/types'
import useSynctex from '../hooks/use-synctex'
import { useFeatureFlag } from '@/shared/context/split-test-context'
import OLSpinner from '@/shared/components/ol/ol-spinner'
const GoToCodeButton = memo(function GoToCodeButton({
syncToCode,
@@ -27,9 +27,7 @@ const GoToCodeButton = memo(function GoToCodeButton({
let buttonIcon = null
if (syncToCodeInFlight) {
buttonIcon = (
<Spinner animation="border" aria-hidden="true" size="sm" role="status" />
)
buttonIcon = <OLSpinner size="sm" />
} else if (!isDetachLayout) {
buttonIcon = (
<MaterialIcon type="arrow_left_alt" className="synctex-control-icon" />
@@ -89,9 +87,7 @@ const GoToPdfButton = memo(function GoToPdfButton({
let buttonIcon = null
if (syncToPdfInFlight) {
buttonIcon = (
<Spinner animation="border" aria-hidden="true" size="sm" role="status" />
)
buttonIcon = <OLSpinner size="sm" />
} else if (!isDetachLayout) {
buttonIcon = (
<MaterialIcon type="arrow_right_alt" className="synctex-control-icon" />

View File

@@ -1,5 +1,4 @@
import { useTranslation } from 'react-i18next'
import { Spinner } from 'react-bootstrap'
import {
Dropdown,
DropdownItem,
@@ -18,6 +17,7 @@ import { Project } from '../../../../../../types/project/dashboard/api'
import CompileAndDownloadProjectPDFButton from '../table/cells/action-buttons/compile-and-download-project-pdf-button'
import RenameProjectButton from '../table/cells/action-buttons/rename-project-button'
import MaterialIcon from '@/shared/components/material-icon'
import OLSpinner from '@/shared/components/ol/ol-spinner'
type ActionDropdownProps = {
project: Project
@@ -89,13 +89,9 @@ function ActionsDropdown({ project }: ActionDropdownProps) {
}}
leadingIcon={
pendingCompile ? (
<Spinner
animation="border"
aria-hidden="true"
as="span"
className="dropdown-item-leading-icon spinner"
<OLSpinner
size="sm"
role="status"
className="dropdown-item-leading-icon spinner"
/>
) : (
'picture_as_pdf'

View File

@@ -129,6 +129,7 @@ export default function CreateTagModal({
status === 'pending' || !tagName?.length || !!validationError
}
isLoading={isLoading}
loadingLabel={t('creating')}
>
{t('create')}
</OLButton>

View File

@@ -72,6 +72,7 @@ export default function DeleteTagModal({
variant="danger"
disabled={isLoading}
isLoading={isLoading}
loadingLabel={t('deleting')}
>
{t('delete')}
</OLButton>

View File

@@ -139,6 +139,7 @@ export function EditTagModal({ id, tag, onEdit, onClose }: EditTagModalProps) {
!!validationError
}
isLoading={isLoading}
loadingLabel={t('saving')}
>
{t('save')}
</OLButton>

View File

@@ -127,6 +127,7 @@ export function ManageTagModal({
className="me-auto"
disabled={isDeleteLoading || isUpdateLoading}
isLoading={isDeleteLoading}
loadingLabel={t('deleting')}
>
{t('delete_tag')}
</OLButton>
@@ -147,6 +148,7 @@ export function ManageTagModal({
(newTagName === tag?.name && selectedColor === getTagColor(tag))
)}
isLoading={isUpdateLoading}
loadingLabel={t('saving')}
>
{t('save_or_cancel-save')}
</OLButton>

View File

@@ -109,6 +109,7 @@ function ModalContentNewProjectForm({ onCancel, template = 'none' }: Props) {
onClick={createNewProject}
disabled={projectName === '' || isLoading || redirecting}
isLoading={isLoading}
loadingLabel={t('creating')}
>
{t('create')}
</OLButton>

View File

@@ -58,6 +58,7 @@ function ReconfirmAffiliation({
<OLButton
variant="secondary"
isLoading={isPending}
loadingLabel={t('reconfirming')}
disabled={isPending}
onClick={() => {
setIsPending(true)

View File

@@ -9,7 +9,7 @@ import EmailsHeader from './emails/header'
import EmailsRow from './emails/row'
import AddEmail from './emails/add-email'
import OLNotification from '@/shared/components/ol/ol-notification'
import OLSpinner from '@/shared/components/ol/ol-spinner'
import LoadingSpinner from '@/shared/components/loading-spinner'
function EmailsSectionContent() {
const { t } = useTranslation()
@@ -62,7 +62,7 @@ function EmailsSectionContent() {
{isInitializing ? (
<div className="affiliations-table-row-highlighted">
<div className="affiliations-table-cell text-center">
<OLSpinner size="sm" /> {t('loading')}...
<LoadingSpinner size="sm" />
</div>
</div>
) : (

View File

@@ -1,4 +1,5 @@
import OLButton, { OLButtonProps } from '@/shared/components/ol/ol-button'
import { useTranslation } from 'react-i18next'
function PrimaryButton({
children,
@@ -6,11 +7,13 @@ function PrimaryButton({
isLoading,
onClick,
}: OLButtonProps) {
const { t } = useTranslation()
return (
<OLButton
size="sm"
disabled={disabled && !isLoading}
isLoading={isLoading}
loadingLabel={t('processing')}
onClick={onClick}
variant="secondary"
>

View File

@@ -22,6 +22,7 @@ function AddNewEmailBtn({
variant="primary"
disabled={(disabled && !isLoading) || !isValidEmail(email)}
isLoading={isLoading}
loadingLabel={t('adding')}
{...props}
>
{t('add_new_email')}

View File

@@ -96,6 +96,7 @@ function ResendConfirmationCodeModal({
variant={triggerVariant}
disabled={groupLoading}
isLoading={isLoading}
loadingLabel={t('sending')}
onClick={handleResendConfirmationEmail}
className={triggerVariant === 'link' ? 'btn-inline-link' : undefined}
>

View File

@@ -7,9 +7,9 @@ import classnames from 'classnames'
import MaterialIcon from '@/shared/components/material-icon'
import Tag from '@/shared/components/tag'
import { DropdownItem } from '@/shared/components/dropdown/dropdown-menu'
import { Spinner } from 'react-bootstrap'
import { Contact } from '../utils/types'
import OLFormLabel from '@/shared/components/ol/ol-form-label'
import OLSpinner from '@/shared/components/ol/ol-spinner'
export type ContactItem = {
email: string
@@ -167,15 +167,7 @@ export default function SelectCollaborators({
{/* eslint-disable-next-line jsx-a11y/label-has-for */}
<OLFormLabel className="small" {...getLabelProps()}>
{t('add_email_address')}
{loading && (
<Spinner
animation="border"
aria-hidden="true"
size="sm"
role="status"
className="ms-2"
/>
)}
{loading && <OLSpinner size="sm" className="ms-2" />}
</OLFormLabel>
<div className="host">

View File

@@ -11,7 +11,7 @@ import OLModal, {
} from '@/shared/components/ol/ol-modal'
import OLNotification from '@/shared/components/ol/ol-notification'
import OLButton from '@/shared/components/ol/ol-button'
import { Spinner } from 'react-bootstrap'
import OLSpinner from '@/shared/components/ol/ol-spinner'
const ReadOnlyTokenLink = lazy(() =>
import('./link-sharing').then(({ ReadOnlyTokenLink }) => ({
@@ -67,16 +67,7 @@ export default function ShareProjectModalContent({
</OLModalBody>
<OLModalFooter>
<div className="me-auto">
{inFlight && (
<Spinner
animation="border"
aria-hidden="true"
size="sm"
role="status"
/>
)}
</div>
<div className="me-auto">{inFlight && <OLSpinner size="sm" />}</div>
<ClickableElementEnhancer
onClick={cancel}

View File

@@ -11,8 +11,8 @@ import OLModal, {
} from '@/shared/components/ol/ol-modal'
import OLNotification from '@/shared/components/ol/ol-notification'
import OLButton from '@/shared/components/ol/ol-button'
import { Spinner } from 'react-bootstrap'
import { ProjectMember } from '@/shared/context/types/project-metadata'
import OLSpinner from '@/shared/components/ol/ol-spinner'
export default function TransferOwnershipModal({
member,
@@ -68,16 +68,7 @@ export default function TransferOwnershipModal({
)}
</OLModalBody>
<OLModalFooter>
<div className="me-auto">
{inflight && (
<Spinner
animation="border"
aria-hidden="true"
size="sm"
role="status"
/>
)}
</div>
<div className="me-auto">{inflight && <OLSpinner size="sm" />}</div>
<OLButton variant="secondary" onClick={cancel} disabled={inflight}>
{t('cancel')}
</OLButton>

View File

@@ -31,6 +31,7 @@ function ReactivateSubscription() {
disabled={isLoading || isSuccess}
onClick={handleReactivate}
isLoading={isLoading}
loadingLabel={t('reactivating')}
>
{t('reactivate_subscription')}
</OLButton>

View File

@@ -93,6 +93,7 @@ export function ConfirmUnpauseSubscriptionModal() {
variant="primary"
disabled={inflight}
isLoading={inflight}
loadingLabel={t('unpausing')}
onClick={handleConfirmUnpause}
>
{t('unpause_subscription')}

View File

@@ -1,6 +1,5 @@
import { FC, useEffect, useMemo, useState } from 'react'
import { WordCountData } from '@/features/word-count-modal/components/word-count-data'
import { WordCountLoading } from '@/features/word-count-modal/components/word-count-loading'
import { WordCountError } from '@/features/word-count-modal/components/word-count-error'
import { useProjectContext } from '@/shared/context/project-context'
import useAbortController from '@/shared/hooks/use-abort-controller'
@@ -14,6 +13,7 @@ import { isMainFile } from '@/features/pdf-preview/util/editor-files'
import { countWordsInFile } from '@/features/word-count-modal/utils/count-words-in-file'
import { createSegmenters } from '@/features/word-count-modal/utils/segmenters'
import { WordCountsClient } from './word-counts-client'
import LoadingSpinner from '@/shared/components/loading-spinner'
export const WordCountClient: FC = () => {
const [loading, setLoading] = useState(true)
@@ -107,7 +107,7 @@ export const WordCountClient: FC = () => {
return (
<>
{loading && !error && <WordCountLoading />}
{loading && !error && <LoadingSpinner />}
{error && <WordCountError />}
{data && <WordCountsClient data={data} />}
</>

View File

@@ -1,14 +0,0 @@
import { Spinner } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
export const WordCountLoading = () => {
const { t } = useTranslation()
return (
<div className="loading">
<Spinner animation="border" aria-hidden="true" size="sm" role="status" />
&nbsp;
{t('loading')}
</div>
)
}

View File

@@ -1,6 +1,5 @@
import { FC, useEffect, useState } from 'react'
import { ServerWordCountData } from '@/features/word-count-modal/components/word-count-data'
import { WordCountLoading } from '@/features/word-count-modal/components/word-count-loading'
import { WordCountError } from '@/features/word-count-modal/components/word-count-error'
import { useProjectContext } from '@/shared/context/project-context'
import { useLocalCompileContext } from '@/shared/context/local-compile-context'
@@ -8,6 +7,7 @@ import useAbortController from '@/shared/hooks/use-abort-controller'
import { getJSON } from '@/infrastructure/fetch-json'
import { debugConsole } from '@/utils/debugging'
import { WordCounts } from '@/features/word-count-modal/components/word-counts'
import LoadingSpinner from '@/shared/components/loading-spinner'
export const WordCountServer: FC = () => {
const { projectId } = useProjectContext()
@@ -40,7 +40,7 @@ export const WordCountServer: FC = () => {
return (
<>
{loading && !error && <WordCountLoading />}
{loading && !error && <LoadingSpinner />}
{error && <WordCountError />}
{data && <WordCounts data={data} />}
</>

View File

@@ -1,9 +1,10 @@
import { forwardRef } from 'react'
import { Button as BS5Button, Spinner } from 'react-bootstrap'
import { Button as BS5Button } from 'react-bootstrap'
import type { ButtonProps } from '@/shared/components/types/button-props'
import classNames from 'classnames'
import { useTranslation } from 'react-i18next'
import MaterialIcon from '@/shared/components/material-icon'
import OLSpinner from '../ol/ol-spinner'
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
@@ -56,13 +57,7 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(
>
{isLoading && (
<span className="spinner-container">
<Spinner
animation="border"
aria-hidden="true"
as="span"
className={loadingSpinnerClassName}
role="status"
/>
<OLSpinner size="sm" className={loadingSpinnerClassName} />
<span className="visually-hidden">
{loadingLabel ?? t('loading')}
</span>

View File

@@ -42,6 +42,7 @@ function LoadingSpinner({
return (
<div
role="status"
className={classNames(
'loading',
className,
@@ -49,7 +50,6 @@ function LoadingSpinner({
)}
>
<OLSpinner size={size} />
&nbsp;
{loadingText || `${t('loading')}`}
</div>
)

View File

@@ -2,13 +2,20 @@ import { Spinner } from 'react-bootstrap'
export type OLSpinnerSize = 'sm' | 'lg'
function OLSpinner({ size = 'sm' }: { size: OLSpinnerSize }) {
function OLSpinner({
size = 'sm',
className,
}: {
size?: OLSpinnerSize
className?: string
}) {
return (
<Spinner
size={size === 'sm' ? 'sm' : undefined}
animation="border"
aria-hidden="true"
role="status"
className={className}
data-testid="ol-spinner"
/>
)
}

View File

@@ -9,10 +9,11 @@ import {
import classNames from 'classnames'
import { useSelect } from 'downshift'
import { useTranslation } from 'react-i18next'
import { Form, Spinner } from 'react-bootstrap'
import { Form } from 'react-bootstrap'
import FormControl from '@/shared/components/form/form-control'
import MaterialIcon from '@/shared/components/material-icon'
import { DropdownItem } from '@/shared/components/dropdown/dropdown-menu'
import OLSpinner from './ol/ol-spinner'
export type SelectProps<T> = {
// The items rendered as dropdown options.
@@ -152,17 +153,7 @@ export const Select = <T,>({
{optionalLabel && (
<span className="fw-normal">({t('optional')})</span>
)}{' '}
{loading && (
<span data-testid="spinner">
<Spinner
animation="border"
aria-hidden="true"
as="span"
role="status"
size="sm"
/>
</span>
)}
{loading && <OLSpinner size="sm" />}
</Form.Label>
) : null}
<FormControl

View File

@@ -64,7 +64,6 @@ import type { ScriptLogType } from '../../../modules/admin-panel/frontend/js/fea
import { ActiveExperiment } from './labs-utils'
import { Subscription as AdminSubscription } from '../../../types/admin/subscription'
import { AdminCapability } from '../../../types/admin-capabilities'
import { GroupAuditLog } from '../../../modules/group-audit-log/frontend/js/components/logs'
import { AlgoliaConfig } from '../../../modules/algolia-search/frontend/js/types'
import { WritefullPublicEnv } from '@wf/domain/writefull-public-env'
@@ -188,7 +187,6 @@ export interface Meta {
'ol-legacyEditorThemes': string[]
'ol-licenseQuantity'?: number
'ol-loadingText': string
'ol-logsForRendering': GroupAuditLog[]
'ol-managedGroupSubscriptions': ManagedGroupSubscription[]
'ol-managedInstitutions': ManagedInstitution[]
'ol-managedPublishers': Publisher[]
@@ -257,6 +255,7 @@ export interface Meta {
'ol-showAiErrorAssistant': boolean
'ol-showBrlGeoBanner': boolean
'ol-showCouponField': boolean
'ol-showFilters': boolean
'ol-showGroupDiscount': boolean
'ol-showGroupsAndEnterpriseBanner': boolean
'ol-showInrGeoBanner': boolean

View File

@@ -0,0 +1,52 @@
import type { Meta, StoryObj } from '@storybook/react'
import LoadingSpinner, {
FullSizeLoadingSpinner,
} from '@/shared/components/loading-spinner'
type Story = StoryObj<typeof LoadingSpinner>
export const Default: Story = {
args: {
loadingText: 'Loading content...',
},
}
export const WithDelay: Story = {
args: {
delay: 500,
loadingText: 'This will appear after a 500ms delay...',
},
}
export const FullSize: StoryObj<typeof FullSizeLoadingSpinner> = {
render: args => <FullSizeLoadingSpinner {...args} />,
args: {
loadingText: 'Loading entire section...',
size: 'sm',
},
}
const meta: Meta<typeof LoadingSpinner> = {
title: 'Shared / Components / Loading Spinner',
component: LoadingSpinner,
parameters: {
layout: 'centered',
},
argTypes: {
delay: {
control: 'select',
options: [0, 500],
},
size: {
control: 'radio',
options: ['lg', 'sm'],
},
},
args: {
size: 'sm',
delay: 0,
},
render: args => <LoadingSpinner {...args} />,
}
export default meta

View File

@@ -5,6 +5,7 @@
.spinner-border {
// Ensure the thickness of the spinner is independent of the font size of its container
font-size: var(--font-size-03);
margin-right: var(--spacing-02);
}
// Adjust the small spinner to be 25% larger than Bootstrap's default in each dimension

View File

@@ -538,6 +538,5 @@
max-height: 39px;
font-size: var(--font-size-01);
line-height: var(--line-height-01);
margin-right: var(--spacing-04);
}

View File

@@ -345,6 +345,61 @@ $z-index-group-member-picker-list: 1;
}
}
.group-discount-banner {
border: 1px solid var(--neutral-20);
border-radius: var(--border-radius-base);
width: 50%;
max-width: 576px;
margin: auto;
margin-top: var(--spacing-06);
margin-bottom: var(--spacing-10);
display: flex;
gap: var(--spacing-06);
padding: var(--spacing-06);
@include media-breakpoint-down(md) {
width: 100%;
}
.group-icon {
flex-grow: 2;
}
.text-and-cta {
flex-grow: 8;
display: flex;
gap: var(--spacing-06);
@include media-breakpoint-down(lg) {
flex-direction: column;
}
.group-discount-text {
flex-grow: 6;
p {
margin-bottom: 0;
}
h3 {
margin-top: 0;
}
.group-discount-text-heading {
font-size: var(--font-size-03);
font-weight: 600;
line-height: var(--line-height-02);
margin-bottom: var(--spacing-02);
color: var(--neutral-90);
}
}
.group-discount-cta {
flex-grow: 3;
}
}
}
.plans-new-table {
width: 100%;

View File

@@ -150,6 +150,7 @@
"alignment": "Alignment",
"all": "All",
"all_borders": "All borders",
"all_events": "All events",
"all_features_in_group_standard_plus": "All features in Group Standard, plus:",
"all_logs": "All logs",
"all_premium_features": "All premium features",
@@ -176,6 +177,7 @@
"anyone_with_link_can_view": "Anyone with this link can view this project",
"app_on_x": "__appName__ on __social__",
"appearance": "Appearance",
"apply": "Apply ",
"apply_educational_discount": "Apply educational discount",
"apply_educational_discount_description": "40% discount for groups using __appName__ for teaching",
"apply_educational_discount_description_with_group_discount": "Get a total of 40% off for groups using __appName__ for teaching",
@@ -323,6 +325,7 @@
"characters": "Characters",
"chat": "Chat",
"chat_error": "Could not load chat messages, please try again.",
"chat_with_sales_team_50_or_more": "Chat with our sales team about larger discounts for groups of 50 or more.",
"check_error_logs": "Check error logs",
"check_your_email": "Check your email",
"checking": "Checking",
@@ -339,6 +342,7 @@
"cite_faster": "Cite faster",
"city": "City",
"clear_cached_files": "Clear cached files",
"clear_filters": "Clear filters",
"clear_search": "clear search",
"clear_sessions": "Clear sessions",
"clear_sessions_description": "This is a list of other sessions (logins) which are active on your account, not including your current session. Click the \"Clear sessions\" button below to log them out.",
@@ -693,6 +697,7 @@
"enables_real_time_syntax_checking_in_the_editor": "Enables real-time syntax checking in the editor",
"enabling": "Enabling",
"end_of_document": "End of document",
"end_time_utc": "End time (UTC)",
"ensure_recover_account": "This will ensure that it can be used to recover your __appName__ account in case you lose access to your primary email address.",
"enter_any_size_including_units_or_valid_latex_command": "Enter any size (including units) or valid LaTeX command",
"enter_the_code": "Enter the 6-digit code sent to __email__.",
@@ -714,6 +719,7 @@
"es": "Spanish",
"essential_cookies_only": "Essential cookies only",
"estimated_number_of_overleaf_users": "Estimated number of __appName__ users",
"event_type": "Event type",
"every": "per",
"everything_in_free_plus": "Everything in Free, plus…",
"everything_in_group_professional_plus": "Everything in Group Professional, plus…",
@@ -745,6 +751,7 @@
"fast": "Fast",
"fast_draft": "Fast [draft]",
"fastest": "Fastest",
"feature_enabled_or_disabled": "Feature enabled/disabled",
"feature_included": "Feature included",
"feature_not_included": "Feature not included",
"featured": "Featured",
@@ -844,6 +851,7 @@
"generate_from_text_or_image": "From text or image",
"generate_tables_and_equations": "Generate tables and equations from text and images. Try it for free in the Overleaf toolbar!",
"generate_token": "Generate token",
"generating": "Generating",
"generic_if_problem_continues_contact_us": "If the problem continues please contact us",
"generic_linked_file_compile_error": "This projects output files are not available because it failed to compile. Please open the project to see the compilation error details.",
"generic_something_went_wrong": "Sorry, something went wrong",
@@ -1179,6 +1187,7 @@
"labs_program_benefits": "By signing up for Overleaf Labs you can get your hands on in-development features and try them out as much as you like. All we ask in return is your honest feedback to help us develop and improve. Its important to note that features available in this program are still being tested and actively developed. This means they could change, be removed, or become part of a premium plan.",
"language": "Language",
"language_suggestions": "Language suggestions",
"larger_discounts_available": "Larger discounts available",
"last_active": "Last Active",
"last_active_description": "Last time a project was opened.",
"last_edit": "Last edit",
@@ -1811,6 +1820,7 @@
"reconfirm_account": "Reconfirm account",
"reconfirm_explained": "We need to reconfirm your account. Please request a password reset link via the form below to reconfirm your account. If you have any problems reconfirming your account, please contact us at",
"reconfirm_secondary_email": "To enhance the security of your __appName__ account, please reconfirm your secondary email address __emailAddress__.",
"reconfirming": "Reconfirming",
"reconnect": "Try again",
"reconnecting": "Reconnecting",
"reconnecting_in_x_secs": "Reconnecting in __seconds__ secs",
@@ -1923,6 +1933,7 @@
"resync_completed": "Resync completed!",
"resync_message": "Resyncing project history can take several minutes depending on the size of the project.",
"resync_project_history": "Resync Project History",
"resyncing": "Resyncing",
"retry_test": "Retry test",
"return_to_login_page": "Return to Login page",
"reverse_x_sort_order": "Reverse __x__ sort order",
@@ -2188,6 +2199,7 @@
"start_free_trial": "Start Free Trial!",
"start_free_trial_without_exclamation": "Start Free Trial",
"start_the_conversation_by_saying_hello_or_sharing_an_update": "Start the conversation by saying hello or sharing an update",
"start_time_utc": "Start time (UTC)",
"start_typing_find_your_company": " Start typing to find your company",
"start_typing_find_your_organization": "Start typing to find your organization",
"start_typing_find_your_university": "Start typing to find your university",
@@ -2505,7 +2517,6 @@
"uncategorized_projects": "Uncategorized Projects",
"unconfirmed": "Unconfirmed",
"undelete": "Undelete",
"undeleting": "Undeleting",
"understanding_labels": "Understanding labels",
"undo": "Undo",
"unfold_line": "Unfold line",
@@ -2539,6 +2550,7 @@
"unlinking": "Unlinking",
"unmerge_cells": "Unmerge cells",
"unpause_subscription": "Unpause subscription",
"unpausing": "Unpausing",
"unpublish": "Unpublish",
"unpublishing": "Unpublishing",
"unsubscribe": "Unsubscribe",

View File

@@ -78,7 +78,11 @@ function RegisterForm({
lg={4}
className="mt-3 mt-lg-0 d-flex align-items-center flex-column flex-lg-row"
>
<OLButton type="submit" isLoading={isLoading}>
<OLButton
type="submit"
isLoading={isLoading}
loadingLabel={t('registering')}
>
Register
</OLButton>
</OLCol>

View File

@@ -0,0 +1,9 @@
<svg width="37" height="40" viewBox="0 0 37 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M34.0038 27.9404C34.2069 27.3505 34.3111 26.7268 34.3111 26.0953C34.3111 23.8682 33.0273 21.9347 31.1602 20.9914C31.3372 20.7004 31.4909 20.3469 31.6028 19.9103C31.7461 19.3516 31.8034 18.6967 31.7591 18.164C31.7383 17.9093 31.7148 17.6702 31.6888 17.4442C31.4075 14.9338 30.0066 12.6859 27.8686 11.3346C26.4937 10.464 24.3245 9.72858 24.1553 8.82683C24.1474 8.79304 24.1422 8.75666 24.1344 8.72288C24.137 8.64492 24.1422 8.56436 24.1422 8.4864C24.1422 5.93446 22.5876 3.82171 20.3768 3.06808C19.882 2.87577 19.3378 2.76923 18.7414 2.76923C18.7128 2.76923 18.6815 2.76923 18.6529 2.76923C18.6242 2.76923 18.593 2.76923 18.5644 2.76923C17.968 2.76923 17.4238 2.87577 16.929 3.06808C14.7182 3.8243 13.1635 5.93446 13.1635 8.4864C13.1635 8.56696 13.1688 8.64492 13.1714 8.72288C13.1635 8.75666 13.1557 8.79304 13.1505 8.82683C12.9813 9.72858 10.8121 10.4666 9.43715 11.3346C7.29922 12.6859 5.89824 14.9338 5.617 17.4442C5.59096 17.6702 5.56752 17.9093 5.54669 18.164C5.49982 18.6967 5.55971 19.3516 5.70293 19.9103C5.81491 20.3469 5.96855 20.7029 6.14562 20.9914C4.27852 21.9347 2.99472 23.8682 2.99472 26.0953C2.99472 26.7268 3.10148 27.3505 3.302 27.9404C2.13278 29.1488 1.54166 30.8067 1.54166 32.8701V34.2241C1.54166 35.8846 2.89056 37.2308 4.55455 37.2308H32.7538C34.4178 37.2308 35.7667 35.8846 35.7667 34.2241V32.8701C35.7667 30.8067 35.1756 29.1488 34.0064 27.9404H34.0038Z" fill="#80CBA2"/>
<path d="M14.5721 17.5385H23.5023C23.8058 17.5385 24.05 17.2906 24.05 16.9827V15.1589C24.05 13.8048 23.5203 12.508 22.5792 11.5505C22.2475 11.2139 21.8721 10.9295 21.4684 10.7025C21.9467 10.0607 22.227 9.24661 22.227 8.3569C22.227 6.29306 20.7253 4.61539 18.6914 4.61539C16.6574 4.61539 15.0041 6.29046 15.0041 8.3569C15.0041 9.26227 15.3203 10.092 15.8475 10.739C15.4669 10.9608 15.1146 11.2322 14.7984 11.5505C13.8547 12.508 13.5667 13.8048 13.5667 15.1589V16.5183C13.5667 17.0819 14.0167 17.5385 14.5721 17.5385ZM18.6888 6.65574C19.6145 6.65574 20.2136 7.4176 20.2136 8.3569C20.2136 9.29619 19.6119 10.0581 18.6888 10.0581C17.7657 10.0581 17.0123 9.29619 17.0123 8.3569C17.0123 7.4176 17.7631 6.65574 18.6888 6.65574ZM15.5775 14.6371C15.5775 14.0527 15.7703 13.476 16.1663 13.0507C16.6523 12.5289 17.254 12.2993 17.9585 12.1636C18.1925 12.1193 18.4317 12.0984 18.6708 12.0984H19.2879C20.2804 12.1141 20.5916 12.4193 21.1573 12.9933C21.723 13.5674 22.0418 14.3475 22.0418 15.1589C22.0418 15.3468 21.8927 15.4981 21.7075 15.4981H15.9169C15.7317 15.4981 15.5826 15.3468 15.5826 15.1589V14.6397L15.5775 14.6371Z" fill="black"/>
<path d="M32.4027 29.0889C32.0612 28.7524 31.6748 28.468 31.2592 28.241C31.7515 27.5991 32.04 26.7851 32.04 25.8953C32.04 23.8315 30.4942 22.1538 28.4005 22.1538C26.3067 22.1538 24.6047 23.8289 24.6047 25.8953C24.6047 26.8007 24.9303 27.6304 25.4729 28.2775C25.0812 28.4993 24.7185 28.7706 24.393 29.0889C23.4215 30.0465 23.1251 31.3432 23.1251 32.6974V34.0567C23.1251 34.6203 23.5883 35.0769 24.16 35.0769H33.3529C33.6653 35.0769 33.9167 34.829 33.9167 34.5212V32.6974C33.9167 31.3432 33.3715 30.0465 32.4027 29.0889ZM28.3978 24.1942C29.3507 24.1942 29.9675 24.9561 29.9675 25.8953C29.9675 26.8346 29.3481 27.5965 28.3978 27.5965C27.4476 27.5965 26.672 26.8346 26.672 25.8953C26.672 24.9561 27.4449 24.1942 28.3978 24.1942ZM31.5027 33.0366H25.5417C25.3512 33.0366 25.1976 32.8852 25.1976 32.6974V32.1782C25.1976 31.5937 25.3962 31.0171 25.8038 30.5918C26.3041 30.07 26.9235 29.8404 27.6487 29.7047C27.8896 29.6603 28.1358 29.6395 28.3819 29.6395H29.0172C30.0389 29.6551 30.3592 29.9604 30.9416 30.5344C31.5239 31.1084 31.8521 31.8885 31.8521 32.7C31.8521 32.8878 31.6986 33.0392 31.508 33.0392L31.5027 33.0366Z" fill="black"/>
<path d="M11.8341 28.241C12.3265 27.5991 12.615 26.7851 12.615 25.8953C12.615 23.8315 11.0692 22.1538 8.97542 22.1538C6.88167 22.1538 5.17967 23.8289 5.17967 25.8953C5.17967 26.8007 5.50524 27.6304 6.04787 28.2775C5.65612 28.4993 5.29349 28.7706 4.96791 29.0889C3.99647 30.0465 3.70001 31.3432 3.70001 32.6974V34.0567C3.70001 34.6203 4.16323 35.0769 4.73498 35.0769H13.9279C14.2402 35.0769 14.4917 34.829 14.4917 34.5212V32.6974C14.4917 31.3432 13.9464 30.0465 12.9776 29.0889C12.6362 28.7524 12.2497 28.468 11.8341 28.241ZM8.97277 24.1968C9.92568 24.1968 10.5424 24.9587 10.5424 25.898C10.5424 26.8372 9.92303 27.5991 8.97277 27.5991C8.02251 27.5991 7.24695 26.8372 7.24695 25.898C7.24695 24.9587 8.01986 24.1968 8.97277 24.1968ZM12.0777 33.0392H6.11669C5.92611 33.0392 5.77259 32.8878 5.77259 32.7V32.1808C5.77259 31.5963 5.97111 31.0197 6.37874 30.5944C6.87902 30.0726 7.49841 29.843 8.22368 29.7073C8.46455 29.6629 8.71072 29.6421 8.95689 29.6421H9.59216C10.6139 29.6577 10.9342 29.963 11.5165 30.537C12.0988 31.111 12.4271 31.8912 12.4271 32.7026C12.4271 32.8905 12.2735 33.0418 12.083 33.0418L12.0777 33.0392Z" fill="black"/>
<path d="M27.8715 19.3063C27.8715 19.916 28.4282 20.4002 29.0682 20.2926C29.5677 20.2081 29.9424 19.7392 29.9059 19.2422C29.6354 15.4478 29.0838 14.5511 27.7128 12.7552C27.4552 12.417 27.0155 12.2351 26.5993 12.335C25.8604 12.5118 25.5794 13.3624 26.3937 14.3974C27.5437 15.9039 27.8715 17.2771 27.8715 19.3063Z" fill="black"/>
<path d="M21.9215 27.8889C21.1343 28.1636 20.8286 28.2714 20.291 28.3639C18.9969 28.5847 17.6671 28.446 16.4417 27.9813C16.2303 27.9017 16.0239 27.8221 15.8201 27.7502C15.303 27.5654 14.7323 27.8375 14.5489 28.3613C14.3655 28.8851 14.6381 29.4576 15.1552 29.6425C16.3627 30.3357 17.1295 30.4616 18.411 30.4616C19.6924 30.4616 20.5764 30.4616 22.4615 29.6425C22.9787 29.4576 23.2513 28.8825 23.0679 28.3613C22.8844 27.8401 22.4412 27.704 21.9215 27.8914V27.8889Z" fill="black"/>
<path d="M8.36483 20.2261C9.14108 20.5458 9.66675 19.8604 9.66675 19.3079C9.66675 17.2824 9.99904 15.9269 11.2029 14.4231C11.7885 13.6584 11.9111 12.7479 11.3745 12.5075C10.9033 12.1776 10.236 12.2645 9.88465 12.707C8.6481 14.7376 8.02437 14.9346 7.7166 18.3361C7.66757 18.8859 7.83099 20.0061 8.36756 20.2261H8.36483Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@@ -434,7 +434,7 @@ describe('<PdfSynctexControls/>', function () {
'not.be.disabled'
)
cy.findByRole('status', { hidden: true }).should('not.exist')
cy.findByTestId('ol-spinner').should('not.exist')
cy.wrap(null).then(() => {
testDetachChannel.postMessage({
@@ -448,7 +448,7 @@ describe('<PdfSynctexControls/>', function () {
'be.disabled'
)
cy.findByRole('status', { hidden: true }).should('have.length', 1)
cy.findByTestId('ol-spinner').should('have.length', 1)
cy.wrap(null).then(() => {
testDetachChannel.postMessage({

View File

@@ -63,7 +63,7 @@ describe('<Select />', function () {
describe('initial rendering', function () {
it('renders default text', function () {
render({ defaultText: 'Choose an item' })
cy.findByTestId('spinner').should('not.exist')
cy.findByTestId('ol-spinner').should('not.exist')
cy.findByRole('combobox').should('have.value', 'Choose an item')
})
@@ -102,7 +102,7 @@ describe('<Select />', function () {
label: 'test label',
loading: true,
})
cy.findByTestId('spinner')
cy.findByTestId('ol-spinner')
})
it('does not render a spinner while loading if there is no label', function () {
@@ -110,7 +110,7 @@ describe('<Select />', function () {
defaultText: 'Choose an item',
loading: true,
})
cy.findByTestId('spinner').should('not.exist')
cy.findByTestId('ol-spinner').should('not.exist')
})
})

View File

@@ -884,7 +884,7 @@ describe('<UserNotifications />', function () {
screen.getByRole('button', { name: 'Send confirmation code' })
)
await waitForElementToBeRemoved(() => screen.getByText(/loading/i))
await waitForElementToBeRemoved(() => screen.getByText(/sending/i))
screen.getByText(/Enter the 6-digit code sent to foo@overleaf.com/i)
expect(sendReconfirmationMock.callHistory.called()).to.be.true
fireEvent.click(

View File

@@ -220,7 +220,7 @@ describe('<EmailsSection />', function () {
await waitForElementToBeRemoved(() =>
screen.getByRole('button', {
name: 'Loading',
name: /adding/i,
})
)

View File

@@ -20,7 +20,8 @@ describe('ProjectListController', function () {
first_name: 'bjkdsjfk',
features: {},
emails: [{ email: 'test@overleaf.com' }],
lastActive: new Date(1),
lastActive: new Date(2),
signUpDate: new Date(1),
lastLoginIp: '111.111.111.112',
}
ctx.users = {