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:
David
2025-10-15 08:05:57 +00:00
committed by Copybot
parent 2c7c0caf07
commit 584572bb40
43 changed files with 600 additions and 672 deletions
-1
View File
@@ -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()
}
}
}
@@ -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,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'
@@ -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
@@ -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 = (
@@ -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,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()
@@ -112,7 +112,7 @@ function WritefullGrantedAddOn({
)
}
export function WritefullManagedBundleAddOn() {
function WritefullManagedBundleAddOn() {
const { setModalIdShown } = useSubscriptionDashboardContext()
const handleManageOnWritefull = () => setModalIdShown('manage-on-writefull')
return (
@@ -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>
)
}
@@ -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)
+66
View File
@@ -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
-2
View File
@@ -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",
+4 -1
View File
@@ -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,