Merge pull request #28760 from overleaf/dp-hackathon-knip
Add Knip and remove a bunch of unused code GitOrigin-RevId: 42ab99fc65973c883d2361e0027e7181767e714e
This commit is contained in:
@@ -978,7 +978,6 @@ module.exports = {
|
||||
tprFileViewRefreshButton: [],
|
||||
tprFileViewNotOriginalImporter: [],
|
||||
contactUsModal: [],
|
||||
editorToolbarButtons: [],
|
||||
sourceEditorExtensions: [],
|
||||
sourceEditorComponents: [],
|
||||
pdfLogEntryHeaderActionComponents: [],
|
||||
|
||||
@@ -101,7 +101,6 @@
|
||||
"added_by_on": "",
|
||||
"adding": "",
|
||||
"additional_certificate": "",
|
||||
"additional_licenses": "",
|
||||
"address_line_1": "",
|
||||
"address_second_line_optional": "",
|
||||
"adjust_column_width": "",
|
||||
@@ -334,7 +333,6 @@
|
||||
"connection_lost_with_unsaved_changes": "",
|
||||
"contact_group_admin": "",
|
||||
"contact_sales": "",
|
||||
"contact_support_to_change_group_subscription": "",
|
||||
"contact_us": "",
|
||||
"contacting_the_sales_team": "",
|
||||
"continue": "",
|
||||
|
||||
@@ -58,17 +58,6 @@ prototype\
|
||||
|
||||
const MAX_PATH = 1024 // Maximum path length, in characters. This is fairly arbitrary.
|
||||
|
||||
export function clean(filename: string): string {
|
||||
filename = filename.replace(BADCHAR_RX, '_')
|
||||
// for BADFILE_RX replace any matches with an equal number of underscores
|
||||
filename = filename.replace(BADFILE_RX, match =>
|
||||
new Array(match.length + 1).join('_')
|
||||
)
|
||||
// replace blocked filenames 'prototype' with '@prototype'
|
||||
filename = filename.replace(BLOCKEDFILE_RX, '@$1')
|
||||
return filename
|
||||
}
|
||||
|
||||
export function isCleanFilename(filename: string): boolean {
|
||||
return (
|
||||
isAllowedLength(filename) &&
|
||||
@@ -81,30 +70,6 @@ export function isBlockedFilename(filename: string): boolean {
|
||||
return BLOCKEDFILE_RX.test(filename)
|
||||
}
|
||||
|
||||
// returns whether a full path is 'clean' - e.g. is a full or relative path
|
||||
// that points to a file, and each element passes the rules in 'isCleanFilename'
|
||||
export function isCleanPath(path: string): boolean {
|
||||
const elements = path.split('/')
|
||||
|
||||
const lastElementIsEmpty = elements[elements.length - 1].length === 0
|
||||
if (lastElementIsEmpty) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (const element of Array.from(elements)) {
|
||||
if (element.length > 0 && !isCleanFilename(element)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// check for a top-level reserved name
|
||||
if (BLOCKEDFILE_RX.test(path.replace(/^\/?/, ''))) {
|
||||
return false
|
||||
} // remove leading slash if present
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export function isAllowedLength(pathname: string): boolean {
|
||||
return pathname.length > 0 && pathname.length <= MAX_PATH
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
|
||||
export function waitFor<T>(
|
||||
testFunction: () => T,
|
||||
timeout: number,
|
||||
pollInterval = 500
|
||||
): Promise<T> {
|
||||
const iterationLimit = Math.floor(timeout / pollInterval)
|
||||
let iterations = 0
|
||||
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const tryIteration = () => {
|
||||
if (iterations > iterationLimit) {
|
||||
const err = new Error(
|
||||
`waiting too long, ${JSON.stringify({ timeout, pollInterval })}`
|
||||
)
|
||||
debugConsole.error(err)
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
|
||||
iterations += 1
|
||||
const result = testFunction()
|
||||
|
||||
if (result) {
|
||||
resolve(result)
|
||||
return
|
||||
}
|
||||
|
||||
setTimeout(tryIteration, pollInterval)
|
||||
}
|
||||
|
||||
tryIteration()
|
||||
})
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
|
||||
type EditorEvent = { type: string; meta: unknown; date: Date }
|
||||
|
||||
// Record events and then do nothing with them.
|
||||
export class EventLog {
|
||||
private recentEvents: EditorEvent[] = []
|
||||
|
||||
pushEvent = (type: string, meta: unknown = {}) => {
|
||||
debugConsole.log('event', type, meta)
|
||||
this.recentEvents.push({ type, meta, date: new Date() })
|
||||
if (this.recentEvents.length > 100) {
|
||||
return this.recentEvents.shift()
|
||||
}
|
||||
}
|
||||
}
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
import PdfLogsViewer from '@/features/pdf-preview/components/pdf-logs-viewer'
|
||||
import { PdfPreviewProvider } from '@/features/pdf-preview/components/pdf-preview-provider'
|
||||
|
||||
export default function OldErrorPane() {
|
||||
return (
|
||||
<PdfPreviewProvider>
|
||||
<PdfLogsViewer alwaysVisible />
|
||||
</PdfPreviewProvider>
|
||||
)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import classNames from 'classnames'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { Overlay, OverlayProps, Popover } from 'react-bootstrap'
|
||||
|
||||
/** @knipignore keep this file around even when there is no current promo using it */
|
||||
export default function TooltipPromotion({
|
||||
target,
|
||||
tutorialKey,
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { useTutorial } from '@/shared/hooks/promotions/use-tutorial'
|
||||
import useTutorial from '@/shared/hooks/promotions/use-tutorial'
|
||||
import { useEditorContext } from '@/shared/context/editor-context'
|
||||
import { useProjectContext } from '@/shared/context/project-context'
|
||||
import OLNotification from '@/shared/components/ol/ol-notification'
|
||||
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
type MenuItemButtonProps = {
|
||||
children: ReactNode
|
||||
onClick?: (e?: React.MouseEvent) => void
|
||||
className?: string
|
||||
afterNode?: React.ReactNode
|
||||
}
|
||||
|
||||
export default function MenuItemButton({
|
||||
children,
|
||||
onClick,
|
||||
className,
|
||||
afterNode,
|
||||
...buttonProps
|
||||
}: MenuItemButtonProps) {
|
||||
return (
|
||||
<li role="presentation" className={className}>
|
||||
<button
|
||||
className="menu-item-button"
|
||||
role="menuitem"
|
||||
onClick={onClick}
|
||||
{...buttonProps}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
{afterNode}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import OLFormSwitch from '@/shared/components/ol/ol-form-switch'
|
||||
|
||||
type TrackChangesToggleProps = {
|
||||
id: string
|
||||
description: string
|
||||
disabled: boolean
|
||||
handleToggle: () => void
|
||||
value: boolean
|
||||
}
|
||||
|
||||
function TrackChangesToggle({
|
||||
id,
|
||||
description,
|
||||
disabled,
|
||||
handleToggle,
|
||||
value,
|
||||
}: TrackChangesToggleProps) {
|
||||
return (
|
||||
<OLFormSwitch
|
||||
id={`input-switch-${id}`}
|
||||
disabled={disabled}
|
||||
onChange={handleToggle}
|
||||
checked={value}
|
||||
label={description}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default TrackChangesToggle
|
||||
@@ -1,79 +1,11 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { sendMB } from '@/infrastructure/event-tracking'
|
||||
import OLBadge from '@/shared/components/ol/ol-badge'
|
||||
import OLButton from '@/shared/components/ol/ol-button'
|
||||
|
||||
function trackUpgradeClick() {
|
||||
sendMB('settings-upgrade-click')
|
||||
}
|
||||
|
||||
type EnableWidgetProps = {
|
||||
logo: ReactNode
|
||||
title: string
|
||||
description: string
|
||||
helpPath: string
|
||||
helpTextOverride?: string
|
||||
hasFeature?: boolean
|
||||
isPremiumFeature?: boolean
|
||||
statusIndicator?: ReactNode
|
||||
children?: ReactNode
|
||||
linked?: boolean
|
||||
handleLinkClick: () => void
|
||||
handleUnlinkClick: () => void
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export function EnableWidget({
|
||||
logo,
|
||||
title,
|
||||
description,
|
||||
helpPath,
|
||||
helpTextOverride,
|
||||
hasFeature,
|
||||
isPremiumFeature,
|
||||
statusIndicator,
|
||||
linked,
|
||||
handleLinkClick,
|
||||
handleUnlinkClick,
|
||||
children,
|
||||
disabled,
|
||||
}: EnableWidgetProps) {
|
||||
const { t } = useTranslation()
|
||||
const helpText = helpTextOverride || t('learn_more')
|
||||
|
||||
return (
|
||||
<div className="settings-widget-container">
|
||||
<div>{logo}</div>
|
||||
<div className="description-container">
|
||||
<div className="title-row">
|
||||
<h4>{title}</h4>
|
||||
{!hasFeature && isPremiumFeature && (
|
||||
<OLBadge bg="info">{t('premium_feature')}</OLBadge>
|
||||
)}
|
||||
</div>
|
||||
<p className="small">
|
||||
{description}{' '}
|
||||
<a href={helpPath} target="_blank" rel="noreferrer">
|
||||
{helpText}
|
||||
</a>
|
||||
</p>
|
||||
{children}
|
||||
{hasFeature && statusIndicator}
|
||||
</div>
|
||||
<div>
|
||||
<ActionButton
|
||||
hasFeature={hasFeature}
|
||||
linked={linked}
|
||||
handleUnlinkClick={handleUnlinkClick}
|
||||
handleLinkClick={handleLinkClick}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type ActionButtonProps = {
|
||||
hasFeature?: boolean
|
||||
linked?: boolean
|
||||
@@ -128,5 +60,3 @@ export function ActionButton({
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default EnableWidget
|
||||
|
||||
-16
@@ -27,19 +27,3 @@ type AbsoluteWidth = {
|
||||
}
|
||||
|
||||
export type WidthSelection = PercentageWidth | CustomWidth | AbsoluteWidth
|
||||
|
||||
export const isPercentageWidth = (
|
||||
width: WidthSelection
|
||||
): width is PercentageWidth => {
|
||||
return width.unit === '%'
|
||||
}
|
||||
|
||||
export const isAbsoluteWidth = (
|
||||
width: WidthSelection
|
||||
): width is AbsoluteWidth => {
|
||||
return (ABSOLUTE_UNITS as readonly string[]).includes(width.unit)
|
||||
}
|
||||
|
||||
export const isCustomWidth = (width: WidthSelection): width is CustomWidth => {
|
||||
return width.unit === 'custom'
|
||||
}
|
||||
|
||||
@@ -21,10 +21,6 @@ export const addNewCommentRangeEffect = StateEffect.define<Range<Decoration>>()
|
||||
|
||||
export const removeNewCommentRangeEffect = StateEffect.define<string>()
|
||||
|
||||
export const textSelectedEffect = StateEffect.define<null>()
|
||||
|
||||
export const removeReviewPanelTooltipEffect = StateEffect.define()
|
||||
|
||||
const mouseDownEffect = StateEffect.define()
|
||||
const mouseUpEffect = StateEffect.define()
|
||||
const mouseDownStateField = StateField.define<boolean>({
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
import {
|
||||
Extension,
|
||||
Facet,
|
||||
StateEffect,
|
||||
StateField,
|
||||
TransactionSpec,
|
||||
} from '@codemirror/state'
|
||||
import { Extension, Facet, StateEffect, StateField } from '@codemirror/state'
|
||||
import {
|
||||
Decoration,
|
||||
EditorView,
|
||||
ViewPlugin,
|
||||
ViewUpdate,
|
||||
WidgetType,
|
||||
} from '@codemirror/view'
|
||||
|
||||
@@ -222,30 +215,3 @@ const topPaddingDecoration = EditorView.decorations.compute(
|
||||
])
|
||||
}
|
||||
)
|
||||
|
||||
export function setVerticalOverflow(padding: VerticalPadding): TransactionSpec {
|
||||
return {
|
||||
effects: [setOverflowPaddingEffect.of(padding)],
|
||||
}
|
||||
}
|
||||
|
||||
export function updateSetsVerticalOverflow(update: ViewUpdate): boolean {
|
||||
return update.transactions.some(tr => {
|
||||
return tr.effects.some(effect => effect.is(setOverflowPaddingEffect))
|
||||
})
|
||||
}
|
||||
|
||||
export function updateChangesTopPadding(update: ViewUpdate): boolean {
|
||||
return (
|
||||
update.state.field(overflowPaddingState).top !==
|
||||
update.startState.field(overflowPaddingState).top
|
||||
)
|
||||
}
|
||||
|
||||
export function editorVerticalTopPadding(view: EditorView): number {
|
||||
return view.state.field(overflowPaddingState, false)?.top ?? 0
|
||||
}
|
||||
|
||||
export function editorOverflowPadding(view: EditorView) {
|
||||
return view.state.field(overflowPaddingState, false)
|
||||
}
|
||||
|
||||
@@ -88,11 +88,6 @@ const inlineElements = new Set([
|
||||
export const isInlineElement = (node: Node): node is HTMLElement =>
|
||||
inlineElements.has(node.nodeName)
|
||||
|
||||
const codeElements = new Set(['CODE', 'PRE'])
|
||||
|
||||
export const isCodeElement = (node: Node): node is HTMLElement =>
|
||||
codeElements.has(node.nodeName)
|
||||
|
||||
const keepEmptyBlockElements = new Set(['TD', 'TH', 'CANVAS', 'DT', 'DD', 'HR'])
|
||||
|
||||
export const shouldRemoveEmptyBlockElement = (
|
||||
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import useCodeMirrorMeasurement from './use-codemirror-measurement'
|
||||
|
||||
// view.contentHeight, which is measured for us by CodeMirror and is what the
|
||||
// gutters use, is sometimes a pixel or so short of the full height of the
|
||||
// editor content, which leaves a small gap at the bottom, so use the DOM
|
||||
// scrollHeight property instead.
|
||||
const measureContentHeight = (view: EditorView) => view.contentDOM.scrollHeight
|
||||
|
||||
export default function useCodeMirrorContentHeight() {
|
||||
return useCodeMirrorMeasurement('content-height', measureContentHeight)
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useCodeMirrorViewContext } from '../components/codemirror-context'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import useEventListener from '../../../shared/hooks/use-event-listener'
|
||||
|
||||
export default function useCodeMirrorMeasurement(
|
||||
key: string,
|
||||
measure: (view: EditorView) => number
|
||||
) {
|
||||
const view = useCodeMirrorViewContext()
|
||||
const [measurement, setMeasurement] = useState(() => measure(view))
|
||||
|
||||
useEventListener(
|
||||
'editor:geometry-change',
|
||||
useCallback(() => {
|
||||
view.requestMeasure({
|
||||
key,
|
||||
read: () => measure(view),
|
||||
write(value) {
|
||||
// wrap the React state setter in a timeout so it doesn't run inside the CodeMirror update cycle
|
||||
window.setTimeout(() => {
|
||||
setMeasurement(value)
|
||||
})
|
||||
},
|
||||
})
|
||||
}, [view, measure, key])
|
||||
)
|
||||
|
||||
return measurement
|
||||
}
|
||||
@@ -44,7 +44,6 @@ function filterByType(type: 'file' | 'doc' | 'folder') {
|
||||
}
|
||||
|
||||
export const filterFiles = filterByType('file')
|
||||
export const filterDocs = filterByType('doc')
|
||||
export const filterFolders = filterByType('folder')
|
||||
|
||||
const IMAGE_FILE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'pdf']
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import { SubView } from '../../../../../types/review-panel/review-panel'
|
||||
|
||||
export const isCurrentFileView = (view: SubView) => view === 'cur_file'
|
||||
export const isOverviewView = (view: SubView) => view === 'overview'
|
||||
@@ -1,68 +1,8 @@
|
||||
import { ensureSyntaxTree, syntaxTree } from '@codemirror/language'
|
||||
import { syntaxTree } from '@codemirror/language'
|
||||
import { EditorSelection, EditorState, SelectionRange } from '@codemirror/state'
|
||||
import { SyntaxNode, Tree } from '@lezer/common'
|
||||
import { ListEnvironment } from '../../lezer-latex/latex.terms.mjs'
|
||||
|
||||
const HUNDRED_MS = 100
|
||||
|
||||
export type AncestorItem = {
|
||||
node: SyntaxNode
|
||||
label: string
|
||||
type?: string
|
||||
from: number
|
||||
to: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the stack of 'ancestor' nodes at the given position.
|
||||
* The first element is the most distant ancestor, while the last element
|
||||
* is the node at the position.
|
||||
*/
|
||||
export function getAncestorStack(
|
||||
state: EditorState,
|
||||
pos: number
|
||||
): AncestorItem[] | null {
|
||||
const tree = ensureSyntaxTree(state, pos, HUNDRED_MS)
|
||||
|
||||
if (!tree) {
|
||||
return null
|
||||
}
|
||||
|
||||
const stack: AncestorItem[] = []
|
||||
const selectedNode = tree.resolve(pos, 0)
|
||||
|
||||
let node: SyntaxNode | null = selectedNode
|
||||
while (node) {
|
||||
const name = node.type.name
|
||||
switch (name) {
|
||||
case 'Environment':
|
||||
{
|
||||
const data: AncestorItem = {
|
||||
node,
|
||||
label: name,
|
||||
from: node.from,
|
||||
to: node.to,
|
||||
}
|
||||
|
||||
const child = node.getChild('EnvNameGroup')
|
||||
if (child) {
|
||||
data.type = state.doc.sliceString(child.from + 1, child.to - 1)
|
||||
}
|
||||
stack.push(data)
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
stack.push({ node, label: name, from: node.from, to: node.to })
|
||||
break
|
||||
}
|
||||
|
||||
node = node.parent
|
||||
}
|
||||
|
||||
return stack.reverse()
|
||||
}
|
||||
|
||||
export const wrappedNodeOfType = (
|
||||
state: EditorState,
|
||||
range: SelectionRange,
|
||||
|
||||
@@ -1,29 +1,9 @@
|
||||
import { ensureSyntaxTree } from '@codemirror/language'
|
||||
import { EditorState } from '@codemirror/state'
|
||||
import { IterMode, SyntaxNode, SyntaxNodeRef, Tree } from '@lezer/common'
|
||||
import { SyntaxNode } from '@lezer/common'
|
||||
|
||||
const HUNDRED_MS = 100
|
||||
|
||||
export function iterateDescendantsOf(
|
||||
tree: Tree,
|
||||
ancestors: (string | number)[],
|
||||
spec: {
|
||||
enter(node: SyntaxNodeRef): boolean | void
|
||||
leave?(node: SyntaxNodeRef): void
|
||||
from?: number | undefined
|
||||
to?: number | undefined
|
||||
mode?: IterMode | undefined
|
||||
}
|
||||
) {
|
||||
const filteredEnter = (node: SyntaxNodeRef): boolean | void => {
|
||||
if (!ancestors.some(x => node.type.is(x))) {
|
||||
return false
|
||||
}
|
||||
return spec.enter(node)
|
||||
}
|
||||
tree.iterate({ ...spec, enter: filteredEnter })
|
||||
}
|
||||
|
||||
export const previousSiblingIs = (
|
||||
state: EditorState,
|
||||
pos: number,
|
||||
@@ -38,20 +18,6 @@ export const previousSiblingIs = (
|
||||
return previousNode?.type.name === expectedName
|
||||
}
|
||||
|
||||
export const nextSiblingIs = (
|
||||
state: EditorState,
|
||||
pos: number,
|
||||
expectedName: string
|
||||
): boolean | null => {
|
||||
const tree = ensureSyntaxTree(state, pos, HUNDRED_MS)
|
||||
if (!tree) {
|
||||
return null
|
||||
}
|
||||
const thisNode = tree.resolve(pos)
|
||||
const previousNode = thisNode?.nextSibling
|
||||
return previousNode?.type.name === expectedName
|
||||
}
|
||||
|
||||
export const getOptionalArgumentText = (
|
||||
state: EditorState,
|
||||
optionalArgumentNode: SyntaxNode
|
||||
|
||||
@@ -297,11 +297,3 @@ export function parseFigureData(
|
||||
graphicsCommandArguments,
|
||||
})
|
||||
}
|
||||
|
||||
export const getBeginEnvSuffix = (state: EditorState, node: SyntaxNode) => {
|
||||
const argumentNode = node
|
||||
.getChild('OptionalArgument')
|
||||
?.getChild('ShortOptionalArg')
|
||||
|
||||
return argumentNode && state.sliceDoc(argumentNode.from, argumentNode.to)
|
||||
}
|
||||
|
||||
@@ -6,11 +6,7 @@ export type {
|
||||
Outline,
|
||||
} from './tree-operations/outline'
|
||||
|
||||
export {
|
||||
iterateDescendantsOf,
|
||||
previousSiblingIs,
|
||||
nextSiblingIs,
|
||||
} from './tree-operations/common'
|
||||
export { previousSiblingIs } from './tree-operations/common'
|
||||
|
||||
export {
|
||||
cursorIsAtBeginEnvironment,
|
||||
@@ -19,7 +15,6 @@ export {
|
||||
} from './tree-operations/environments'
|
||||
|
||||
export {
|
||||
getAncestorStack,
|
||||
ancestorNodeOfType,
|
||||
ancestorOfNodeWithType,
|
||||
getBibkeyArgumentNode,
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import OLNotification from '@/shared/components/ol/ol-notification'
|
||||
|
||||
export function RedirectAlerts() {
|
||||
function RedirectAlerts() {
|
||||
const queryParams = new URLSearchParams(window.location.search)
|
||||
const redirectReason = queryParams.get('redirect-reason')
|
||||
const { t } = useTranslation()
|
||||
|
||||
+1
-1
@@ -112,7 +112,7 @@ function WritefullGrantedAddOn({
|
||||
)
|
||||
}
|
||||
|
||||
export function WritefullManagedBundleAddOn() {
|
||||
function WritefullManagedBundleAddOn() {
|
||||
const { setModalIdShown } = useSubscriptionDashboardContext()
|
||||
const handleManageOnWritefull = () => setModalIdShown('manage-on-writefull')
|
||||
return (
|
||||
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
import { Trans } from 'react-i18next'
|
||||
|
||||
export function ContactSupportToChangeGroupPlan() {
|
||||
return (
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey="contact_support_to_change_group_subscription"
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key, jsx-a11y/anchor-has-content
|
||||
<a href="/contact" />,
|
||||
]}
|
||||
/>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
import { Trans } from 'react-i18next'
|
||||
|
||||
export function PendingAdditionalLicenses({
|
||||
additionalLicenses,
|
||||
totalLicenses,
|
||||
}: {
|
||||
additionalLicenses: number
|
||||
totalLicenses: number
|
||||
}) {
|
||||
return (
|
||||
<Trans
|
||||
i18nKey="additional_licenses"
|
||||
values={{
|
||||
additionalLicenses,
|
||||
totalLicenses,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<strong />,
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<strong />,
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
export function swapModal(selectorBefore, selectorAfter) {
|
||||
const modalBefore = $(selectorBefore)
|
||||
const modalAfter = $(selectorAfter)
|
||||
|
||||
// Disable the fade-out + fade-in animation when swapping the forms.
|
||||
modalBefore.removeClass('fade')
|
||||
modalAfter.removeClass('fade')
|
||||
modalAfter.modal()
|
||||
modalBefore.modal('hide')
|
||||
modalBefore.addClass('fade')
|
||||
modalAfter.addClass('fade')
|
||||
}
|
||||
@@ -19,6 +19,7 @@ type IntegrationLinkingWidgetProps = {
|
||||
setOptedIn: (optedIn: boolean) => void
|
||||
}
|
||||
|
||||
/** @knipignore */
|
||||
export function LabsExperimentWidget({
|
||||
logo,
|
||||
title,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { postJSON } from '@/infrastructure/fetch-json'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import { useEditorContext } from '@/shared/context/editor-context'
|
||||
|
||||
export const useTutorial = (
|
||||
const useTutorial = (
|
||||
tutorialKey: string,
|
||||
eventData: Record<string, any> = {}
|
||||
) => {
|
||||
|
||||
@@ -121,5 +121,4 @@ function useAsync<T = any, E extends Error | FetchError = Error>(
|
||||
}
|
||||
|
||||
export default useAsync
|
||||
export type UseAsyncReturnType = ReturnType<typeof useAsync>
|
||||
export { useAsync, abortError }
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import { useState, useCallback, useEffect } from 'react'
|
||||
import customLocalStorage from '@/infrastructure/local-storage'
|
||||
import usePersistedState from '@/shared/hooks/use-persisted-state'
|
||||
|
||||
/**
|
||||
* @typedef {Object} RemindMeLater
|
||||
* @property {boolean} stillDissmissed - whether the user has dismissed the notification, or if the notification is still withing the 1 day reminder period
|
||||
* @property {function} remindThemLater - saves that the user has dismissed the notification for 1 day in local storage
|
||||
* @property {function} saveDismissed - saves that the user has dismissed the notification in local storage
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} key the unique key used to keep track of what popup is currently being shown (usually the component name)
|
||||
* @param {string} notificationLocation what page the notification originates from (eg, the editor page, project page, etc)
|
||||
* @returns {RemindMeLater} an object containing whether the notification is still dismissed, and functions to remind the user later or save that they have dismissed the notification
|
||||
*/
|
||||
export default function useRemindMeLater(
|
||||
key: string,
|
||||
notificationLocation: string = 'editor'
|
||||
) {
|
||||
const [dismissedUntil, setDismissedUntil] = usePersistedState<
|
||||
Date | undefined
|
||||
>(`${notificationLocation}.has_dismissed_${key}_until`)
|
||||
|
||||
const [stillDissmissed, setStillDismissed] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const alertDismissed = customLocalStorage.getItem(
|
||||
`${notificationLocation}.has_dismissed_${key}`
|
||||
)
|
||||
|
||||
const isStillDismissed = Boolean(
|
||||
dismissedUntil && new Date(dismissedUntil) > new Date()
|
||||
)
|
||||
|
||||
setStillDismissed(alertDismissed || isStillDismissed)
|
||||
}, [setStillDismissed, dismissedUntil, key, notificationLocation])
|
||||
|
||||
const remindThemLater = useCallback(() => {
|
||||
const until = new Date()
|
||||
until.setDate(until.getDate() + 1) // 1 day
|
||||
setDismissedUntil(until)
|
||||
}, [setDismissedUntil])
|
||||
|
||||
const saveDismissed = useCallback(() => {
|
||||
customLocalStorage.setItem(
|
||||
`${notificationLocation}.has_dismissed_${key}`,
|
||||
true
|
||||
)
|
||||
}, [key, notificationLocation])
|
||||
|
||||
return { stillDissmissed, remindThemLater, saveDismissed }
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -22,10 +22,6 @@ export const isInsertChange = (
|
||||
change: Change<EditOperation>
|
||||
): change is Change<InsertOperation> => isInsertOperation(change.op)
|
||||
|
||||
export const isCommentChange = (
|
||||
change: Change<CommentOperation>
|
||||
): change is Change<CommentOperation> => isCommentOperation(change.op)
|
||||
|
||||
export const isDeleteChange = (
|
||||
change: Change<EditOperation>
|
||||
): change is Change<DeleteOperation> => isDeleteOperation(change.op)
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
export function cleanupServiceWorker() {
|
||||
try {
|
||||
navigator.serviceWorker
|
||||
.getRegistrations()
|
||||
.catch(() => {
|
||||
// fail silently if permission not given (e.g. SecurityError)
|
||||
return []
|
||||
})
|
||||
.then(registrations => {
|
||||
registrations.forEach(worker => {
|
||||
worker.unregister()
|
||||
})
|
||||
})
|
||||
} catch (e) {
|
||||
// fail silently if service worker are not available (on the navigator)
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ export function getSplitTestVariant(name: string, fallback?: string) {
|
||||
return getMeta('ol-splitTestVariants')?.[name] || fallback
|
||||
}
|
||||
|
||||
/** @knipignore */
|
||||
export function parseIntFromSplitTest(name: string, defaultValue: number) {
|
||||
const v = getMeta('ol-splitTestVariants')?.[name]
|
||||
const n = parseInt(v, 10)
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Configuration for knip, a tool to find unused exports in a project.
|
||||
*
|
||||
* To run knip, use the command: bin/npm -w services/web run knip
|
||||
*
|
||||
* It currently only runs in the frontend web service, but could be
|
||||
* adapted to run in the backend as well.
|
||||
*
|
||||
* This is an initial version built in a hackathon. It is useful but not yet complete.
|
||||
* In future we should add it to our CI checks but we need to make it more robust first.
|
||||
*/
|
||||
|
||||
import type { KnipConfig } from 'knip'
|
||||
import Path from 'path'
|
||||
import settings from './config/settings.webpack'
|
||||
|
||||
const moduleEntryPoints = Object.values(settings.overleafModuleImports).flatMap(
|
||||
paths =>
|
||||
paths.map(path => {
|
||||
const cleanedPath = path.replace(Path.join(__dirname, '/'), '')
|
||||
if (cleanedPath.match(/\.(js|jsx|ts|tsx?)$/)) {
|
||||
return cleanedPath
|
||||
}
|
||||
|
||||
return `${cleanedPath}.{js,jsx,ts,tsx}`
|
||||
})
|
||||
)
|
||||
|
||||
const knipConfig: KnipConfig = {
|
||||
$schema: 'https://unpkg.com/knip@5/schema.json',
|
||||
entry: [
|
||||
'frontend/js/pages/**/*.{js,jsx,ts,tsx}',
|
||||
'frontend/stories/**/*.{js,jsx,ts,tsx}',
|
||||
'test/frontend/**/*.{js,jsx,ts,tsx}',
|
||||
'modules/**/test/**/*.{js,jsx,ts,tsx}',
|
||||
'modules/**/*.test.{js,jsx,ts,tsx}',
|
||||
'modules/**/stories/**/*.{js,jsx,ts,tsx}',
|
||||
// TODO: update this when I work out how writefull entry points work
|
||||
'modules/writefull/**/*.{js,jsx,ts,tsx}',
|
||||
'types/window.ts',
|
||||
// Workers are loaded dynamically so we explicitly include all workers
|
||||
// here until we work out a way to follow the dynamic imports
|
||||
'modules/**/*.worker*',
|
||||
'frontend/js/**/*.worker*',
|
||||
...moduleEntryPoints,
|
||||
],
|
||||
project: [
|
||||
'frontend/js/**/*.{js,jsx,ts,tsx}',
|
||||
'modules/**/frontend/**/*.{js,jsx,ts,tsx}',
|
||||
'!frontend/js/.storybook/**/*.{js,jsx,ts,tsx}',
|
||||
],
|
||||
ignore: [
|
||||
// TODO: Files that we should keep around even when unused.
|
||||
// I would like to do this in the file itself but I can't seem
|
||||
// to work out a way to do that (@knipignore only works for
|
||||
// individual exports rather than whole files)
|
||||
'frontend/js/shared/components/labs/labs-experiments-widget.tsx',
|
||||
'frontend/js/features/ide-redesign/components/tooltip-promo.tsx',
|
||||
],
|
||||
ignoreExportsUsedInFile: true,
|
||||
ignoreBinaries: ['.*'],
|
||||
ignoreDependencies: ['.*'],
|
||||
tags: ['-knipignore'],
|
||||
}
|
||||
|
||||
export default knipConfig
|
||||
@@ -117,7 +117,6 @@
|
||||
"added_by_on": "Added by __name__ on __date__",
|
||||
"adding": "Adding",
|
||||
"additional_certificate": "Additional certificate",
|
||||
"additional_licenses": "Your subscription includes <0>__additionalLicenses__</0> additional license(s) for a total of <1>__totalLicenses__</1> licenses.",
|
||||
"address": "Address",
|
||||
"address_line_1": "Address",
|
||||
"address_second_line_optional": "Address second line (optional)",
|
||||
@@ -432,7 +431,6 @@
|
||||
"contact_message_label": "Message",
|
||||
"contact_sales": "Contact sales",
|
||||
"contact_support": "Contact Support",
|
||||
"contact_support_to_change_group_subscription": "Please <0>contact Support</0> if you wish to change your group subscription.",
|
||||
"contact_us": "Contact us",
|
||||
"contact_us_lowercase": "Contact us",
|
||||
"contacting_the_sales_team": "Contacting the Sales team",
|
||||
|
||||
@@ -71,7 +71,8 @@
|
||||
"local:test:acceptance": "npm run local:test:acceptance:app && npm run local:test:acceptance:modules",
|
||||
"local:test:unit": "npm run test:unit:all",
|
||||
"local:test:frontend": "npm run test:frontend",
|
||||
"local:test": "npm run local:test:unit && npm run local:test:frontend && npm run local:test:acceptance"
|
||||
"local:test": "npm run local:test:unit && npm run local:test:frontend && npm run local:test:acceptance",
|
||||
"knip": "knip --config knip.ts"
|
||||
},
|
||||
"browserslist": [
|
||||
"defaults and supports woff2",
|
||||
@@ -255,6 +256,7 @@
|
||||
"@types/express": "^4.17.23",
|
||||
"@types/mocha": "^9.1.0",
|
||||
"@types/mocha-each": "^2.0.4",
|
||||
"@types/node": "^24.5.2",
|
||||
"@types/react": "^18.3.20",
|
||||
"@types/react-color": "^3.0.13",
|
||||
"@types/react-dom": "^18.3.6",
|
||||
@@ -338,6 +340,7 @@
|
||||
"jscodeshift": "^17.0.0",
|
||||
"jsdom": "^19.0.0",
|
||||
"jsdom-global": "^3.0.2",
|
||||
"knip": "^5.64.1",
|
||||
"match-sorter": "^6.2.0",
|
||||
"mathjax": "^3.2.2",
|
||||
"mediatr-ts": "^2.0.1",
|
||||
|
||||
@@ -94,9 +94,9 @@ export const trashedAndNotOwnedProject = {
|
||||
owner: users.picard,
|
||||
} as Project
|
||||
|
||||
export const sharedProject = archiveableProject
|
||||
export const sharedProject = { ...archiveableProject }
|
||||
|
||||
export const ownedProject = copyableProject
|
||||
export const ownedProject = { ...copyableProject }
|
||||
|
||||
export const projectsData: Array<Project> = [
|
||||
copyableProject,
|
||||
|
||||
Reference in New Issue
Block a user