diff --git a/packages/desktop/app/javascripts/Main/FileBackups/FileBackupsManager.ts b/packages/desktop/app/javascripts/Main/FileBackups/FileBackupsManager.ts index 31d9656ff..311c75ad7 100644 --- a/packages/desktop/app/javascripts/Main/FileBackups/FileBackupsManager.ts +++ b/packages/desktop/app/javascripts/Main/FileBackups/FileBackupsManager.ts @@ -1,4 +1,4 @@ -import { FileBackupsDevice, FileBackupsMapping } from '@web/Application/Device/DesktopSnjsExports' +import { FileBackupRecord, FileBackupsDevice, FileBackupsMapping } from '@web/Application/Device/DesktopSnjsExports' import { AppState } from 'app/AppState' import { shell } from 'electron' import { StoreKeys } from '../Store/StoreKeys' @@ -120,6 +120,10 @@ export class FilesBackupManager implements FileBackupsDevice { return this.defaultMappingFileValue() } + for (const entry of Object.values(data.files)) { + entry.backedUpOn = new Date(entry.backedUpOn) + } + return data } @@ -129,6 +133,10 @@ export class FilesBackupManager implements FileBackupsDevice { void shell.openPath(location) } + async openFileBackup(record: FileBackupRecord): Promise { + void shell.openPath(record.absolutePath) + } + async saveFilesBackupsMappingFile(file: FileBackupsMapping): Promise<'success' | 'failed'> { await writeJSONFile(this.getMappingFileLocation(), file) diff --git a/packages/desktop/app/javascripts/Main/Remote/RemoteBridge.ts b/packages/desktop/app/javascripts/Main/Remote/RemoteBridge.ts index 1eb6cc9da..8a0e89eb6 100644 --- a/packages/desktop/app/javascripts/Main/Remote/RemoteBridge.ts +++ b/packages/desktop/app/javascripts/Main/Remote/RemoteBridge.ts @@ -5,7 +5,7 @@ import { StoreKeys } from '../Store/StoreKeys' const path = require('path') const rendererPath = path.join('file://', __dirname, '/renderer.js') -import { FileBackupsDevice, FileBackupsMapping } from '@web/Application/Device/DesktopSnjsExports' +import { FileBackupsDevice, FileBackupsMapping, FileBackupRecord } from '@web/Application/Device/DesktopSnjsExports' import { app, BrowserWindow } from 'electron' import { BackupsManagerInterface } from '../Backups/BackupsManagerInterface' import { KeychainInterface } from '../Keychain/KeychainInterface' @@ -64,6 +64,7 @@ export class RemoteBridge implements CrossProcessBridge { changeFilesBackupsLocation: this.changeFilesBackupsLocation.bind(this), getFilesBackupsLocation: this.getFilesBackupsLocation.bind(this), openFilesBackupsLocation: this.openFilesBackupsLocation.bind(this), + openFileBackup: this.openFileBackup.bind(this), } } @@ -202,4 +203,8 @@ export class RemoteBridge implements CrossProcessBridge { public openFilesBackupsLocation(): Promise { return this.fileBackups.openFilesBackupsLocation() } + + public openFileBackup(record: FileBackupRecord): Promise { + return this.fileBackups.openFileBackup(record) + } } diff --git a/packages/desktop/app/javascripts/Renderer/DesktopDevice.ts b/packages/desktop/app/javascripts/Renderer/DesktopDevice.ts index e86d701a1..0bca446c9 100644 --- a/packages/desktop/app/javascripts/Renderer/DesktopDevice.ts +++ b/packages/desktop/app/javascripts/Renderer/DesktopDevice.ts @@ -3,6 +3,7 @@ import { Environment, FileBackupsMapping, RawKeychainValue, + FileBackupRecord, } from '@web/Application/Device/DesktopSnjsExports' import { WebOrDesktopDevice } from '@web/Application/Device/WebOrDesktopDevice' import { Component } from '../Main/Packages/PackageManagerInterface' @@ -132,6 +133,10 @@ export class DesktopDevice extends WebOrDesktopDevice implements DesktopDeviceIn return this.remoteBridge.openFilesBackupsLocation() } + openFileBackup(record: FileBackupRecord): Promise { + return this.remoteBridge.openFileBackup(record) + } + async saveFilesBackupsFile( uuid: string, metaFile: string, diff --git a/packages/filepicker/package.json b/packages/filepicker/package.json index 4446d4259..eb7bd1b97 100644 --- a/packages/filepicker/package.json +++ b/packages/filepicker/package.json @@ -5,20 +5,13 @@ "node": ">=16.0.0 <17.0.0" }, "description": "Web filepicker for Standard Notes projects", - "main": "dist/index.js", + "main": "./src/index.ts", "author": "Standard Notes", - "types": "dist/index.d.ts", - "files": [ - "dist" - ], + "types": "./src/index.ts", "private": true, "license": "AGPL-3.0-or-later", "scripts": { - "clean": "rm -fr dist", - "prestart": "yarn clean", - "start": "tsc -p tsconfig.json --watch", - "prebuild": "yarn clean", - "build": "tsc -p tsconfig.json", + "build": "echo 'Empty build script required for yarn topological install'", "lint": "eslint src --ext .ts", "test": "jest" }, diff --git a/packages/filepicker/tsconfig.json b/packages/filepicker/tsconfig.json index 6b72a852c..2a8296516 100644 --- a/packages/filepicker/tsconfig.json +++ b/packages/filepicker/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../node_modules/@standardnotes/config/src/tsconfig.json", + "extends": "../../UILib.tsconfig.json", "compilerOptions": { "skipLibCheck": true, "rootDir": "./src", diff --git a/packages/files/package.json b/packages/files/package.json index 8ed83ff12..3169dc5e1 100644 --- a/packages/files/package.json +++ b/packages/files/package.json @@ -5,22 +5,18 @@ "node": ">=16.0.0 <17.0.0" }, "description": "Client-side files library", - "main": "dist/index.js", + "main": "./src/index.ts", "author": "Standard Notes", - "types": "dist/index.d.ts", + "types": "./src/index.ts", "files": [ "dist" ], "private": true, "license": "AGPL-3.0-or-later", "scripts": { - "clean": "rm -fr dist", - "prestart": "yarn clean", - "start": "tsc -p tsconfig.json --watch", - "prebuild": "yarn clean", - "build": "tsc -p tsconfig.json", "lint": "eslint src --ext .ts", - "test": "jest" + "test": "jest", + "build": "echo 'Empty build script required for yarn topological install'" }, "devDependencies": { "@types/jest": "^29.2.3", diff --git a/packages/files/src/Domain/Device/FileBackupsDevice.ts b/packages/files/src/Domain/Device/FileBackupsDevice.ts index 1b81f6353..2149205b5 100644 --- a/packages/files/src/Domain/Device/FileBackupsDevice.ts +++ b/packages/files/src/Domain/Device/FileBackupsDevice.ts @@ -1,5 +1,5 @@ import { Uuid } from '@standardnotes/common' -import { FileBackupsMapping } from './FileBackupsMapping' +import { FileBackupRecord, FileBackupsMapping } from './FileBackupsMapping' export interface FileBackupsDevice { getFilesBackupsMappingFile(): Promise @@ -18,4 +18,5 @@ export interface FileBackupsDevice { changeFilesBackupsLocation(): Promise getFilesBackupsLocation(): Promise openFilesBackupsLocation(): Promise + openFileBackup(record: FileBackupRecord): Promise } diff --git a/packages/files/src/Domain/Device/FileBackupsMapping.ts b/packages/files/src/Domain/Device/FileBackupsMapping.ts index eac2fc6ba..4af25e323 100644 --- a/packages/files/src/Domain/Device/FileBackupsMapping.ts +++ b/packages/files/src/Domain/Device/FileBackupsMapping.ts @@ -1,17 +1,16 @@ import { Uuid } from '@standardnotes/common' import { FileBackupsConstantsV1 } from './FileBackupsConstantsV1' +export type FileBackupRecord = { + backedUpOn: Date + absolutePath: string + relativePath: string + metadataFileName: typeof FileBackupsConstantsV1.MetadataFileName + binaryFileName: typeof FileBackupsConstantsV1.BinaryFileName + version: typeof FileBackupsConstantsV1.Version +} + export interface FileBackupsMapping { version: typeof FileBackupsConstantsV1.Version - files: Record< - Uuid, - { - backedUpOn: Date - absolutePath: string - relativePath: string - metadataFileName: typeof FileBackupsConstantsV1.MetadataFileName - binaryFileName: typeof FileBackupsConstantsV1.BinaryFileName - version: typeof FileBackupsConstantsV1.Version - } - > + files: Record } diff --git a/packages/files/tsconfig.json b/packages/files/tsconfig.json index f3dac14ef..6cbb6a3ee 100644 --- a/packages/files/tsconfig.json +++ b/packages/files/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../node_modules/@standardnotes/config/src/tsconfig.json", + "extends": "../../UILib.tsconfig.json", "compilerOptions": { "skipLibCheck": true, "rootDir": "./src", diff --git a/packages/services/src/Domain/Backups/BackupService.ts b/packages/services/src/Domain/Backups/BackupService.ts index 91c1a3495..f2e8a1fd5 100644 --- a/packages/services/src/Domain/Backups/BackupService.ts +++ b/packages/services/src/Domain/Backups/BackupService.ts @@ -2,7 +2,13 @@ import { ContentType, Uuid } from '@standardnotes/common' import { EncryptionProviderInterface } from '@standardnotes/encryption' import { PayloadEmitSource, FileItem, CreateEncryptedBackupFileContextPayload } from '@standardnotes/models' import { ClientDisplayableError } from '@standardnotes/responses' -import { FilesApiInterface, FileBackupMetadataFile, FileBackupsDevice, FileBackupsMapping } from '@standardnotes/files' +import { + FilesApiInterface, + FileBackupMetadataFile, + FileBackupsDevice, + FileBackupsMapping, + FileBackupRecord, +} from '@standardnotes/files' import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface' import { ItemManagerInterface } from '../Item/ItemManagerInterface' import { AbstractService } from '../Service/AbstractService' @@ -11,6 +17,7 @@ import { StatusServiceInterface } from '../Status/StatusServiceInterface' export class FilesBackupService extends AbstractService { private itemsObserverDisposer: () => void private pendingFiles = new Set() + private mappingCache?: FileBackupsMapping['files'] constructor( private items: ItemManagerInterface, @@ -75,8 +82,30 @@ export class FilesBackupService extends AbstractService { return this.device.openFilesBackupsLocation() } - private async getBackupsMapping(): Promise { - return (await this.device.getFilesBackupsMappingFile()).files + private async getBackupsMappingFromDisk(): Promise { + const result = (await this.device.getFilesBackupsMappingFile()).files + + this.mappingCache = result + + return result + } + + private invalidateMappingCache(): void { + this.mappingCache = undefined + } + + private async getBackupsMappingFromCache(): Promise { + return this.mappingCache ?? (await this.getBackupsMappingFromDisk()) + } + + public async getFileBackupInfo(file: FileItem): Promise { + const mapping = await this.getBackupsMappingFromCache() + const record = mapping[file.uuid] + return record + } + + public async openFileBackup(record: FileBackupRecord): Promise { + await this.device.openFileBackup(record) } private async handleChangedFiles(files: FileItem[]): Promise { @@ -88,7 +117,7 @@ export class FilesBackupService extends AbstractService { return } - const mapping = await this.getBackupsMapping() + const mapping = await this.getBackupsMappingFromDisk() for (const file of files) { if (this.pendingFiles.has(file.uuid)) { @@ -105,6 +134,8 @@ export class FilesBackupService extends AbstractService { this.pendingFiles.delete(file.uuid) } } + + this.invalidateMappingCache() } private async performBackupOperation(file: FileItem): Promise<'success' | 'failed' | 'aborted'> { diff --git a/packages/utils/package.json b/packages/utils/package.json index 7f6988e02..6820087f4 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -41,6 +41,7 @@ "eslint-plugin-prettier": "*", "jest": "^29.3.1", "jsdom": "^20.0.2", - "ts-jest": "^29.0.3" + "ts-jest": "^29.0.3", + "typescript": "*" } } diff --git a/packages/web/src/javascripts/Application/Application.ts b/packages/web/src/javascripts/Application/Application.ts index 267afe706..5cf17064e 100644 --- a/packages/web/src/javascripts/Application/Application.ts +++ b/packages/web/src/javascripts/Application/Application.ts @@ -30,6 +30,7 @@ import { ArchiveManager, AutolockService, KeyboardService, + PreferenceId, RouteService, RouteServiceInterface, ThemeManager, @@ -387,4 +388,11 @@ export class WebApplication extends SNApplication implements WebApplicationInter FeatureIdentifier.PlainEditor ) } + + openPreferences(pane?: PreferenceId): void { + this.getViewControllerManager().preferencesController.openPreferences() + if (pane) { + this.getViewControllerManager().preferencesController.setCurrentPane(pane) + } + } } diff --git a/packages/web/src/javascripts/Application/Device/DesktopSnjsExports.ts b/packages/web/src/javascripts/Application/Device/DesktopSnjsExports.ts index c6728d847..68bcd182a 100644 --- a/packages/web/src/javascripts/Application/Device/DesktopSnjsExports.ts +++ b/packages/web/src/javascripts/Application/Device/DesktopSnjsExports.ts @@ -6,4 +6,5 @@ export { DesktopClientRequiresWebMethods, FileBackupsMapping, FileBackupsDevice, + FileBackupRecord, } from '@standardnotes/snjs' diff --git a/packages/web/src/javascripts/Components/ContentListView/FileListItem.tsx b/packages/web/src/javascripts/Components/ContentListView/FileListItem.tsx index 7c6da1d65..04d541906 100644 --- a/packages/web/src/javascripts/Components/ContentListView/FileListItem.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/FileListItem.tsx @@ -1,6 +1,6 @@ -import { FileItem } from '@standardnotes/snjs' +import { FileItem, FileBackupRecord } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' -import { FunctionComponent, useCallback, useRef } from 'react' +import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react' import { getFileIconComponent } from '../FilePreview/getFileIconComponent' import ListItemConflictIndicator from './ListItemConflictIndicator' import ListItemTags from './ListItemTags' @@ -12,19 +12,28 @@ import { useContextMenuEvent } from '@/Hooks/useContextMenuEvent' import { classNames } from '@/Utils/ConcatenateClassNames' import { formatSizeToReadableString } from '@standardnotes/filepicker' import { getIconForFileType } from '@/Utils/Items/Icons/getIconForFileType' +import { useApplication } from '../ApplicationView/ApplicationProvider' +import Icon from '../Icon/Icon' const FileListItem: FunctionComponent> = ({ filesController, hideDate, hideIcon, hideTags, - item, + item: file, onSelect, selected, sortBy, tags, }) => { const { toggleAppPane } = useResponsiveAppPane() + const application = useApplication() + + const [backupInfo, setBackupInfo] = useState(undefined) + + useEffect(() => { + void application.fileBackups?.getFileBackupInfo(file).then(setBackupInfo) + }, [application, file]) const listItemRef = useRef(null) @@ -45,7 +54,7 @@ const FileListItem: FunctionComponent> = ({ let shouldOpenContextMenu = selected if (!selected) { - const { didSelect } = await onSelect(item) + const { didSelect } = await onSelect(file) if (didSelect) { shouldOpenContextMenu = true } @@ -55,18 +64,18 @@ const FileListItem: FunctionComponent> = ({ openFileContextMenu(posX, posY) } }, - [selected, onSelect, item, openFileContextMenu], + [selected, onSelect, file, openFileContextMenu], ) const onClick = useCallback(async () => { - const { didSelect } = await onSelect(item, true) + const { didSelect } = await onSelect(file, true) if (didSelect) { toggleAppPane(AppPaneId.Editor) } - }, [item, onSelect, toggleAppPane]) + }, [file, onSelect, toggleAppPane]) const IconComponent = () => - getFileIconComponent(getIconForFileType((item as FileItem).mimeType), 'w-10 h-10 flex-shrink-0') + getFileIconComponent(getIconForFileType((file as FileItem).mimeType), 'w-10 h-10 flex-shrink-0') useContextMenuEvent(listItemRef, openContextMenu) @@ -74,7 +83,7 @@ const FileListItem: FunctionComponent> = ({
> = ({ )}
-
{item.title}
+
{file.title}
- + - +
> = ({ selected ? 'bg-info text-info-contrast' : 'bg-passive-4 text-neutral', )} > - {formatSizeToReadableString(item.decryptedSize)} +
+ {formatSizeToReadableString(file.decryptedSize)} + {backupInfo && ( +
+ +
+ )} +
diff --git a/packages/web/src/javascripts/Components/FileContextMenu/FileContextMenuBackupOption.tsx b/packages/web/src/javascripts/Components/FileContextMenu/FileContextMenuBackupOption.tsx new file mode 100644 index 000000000..deb77798b --- /dev/null +++ b/packages/web/src/javascripts/Components/FileContextMenu/FileContextMenuBackupOption.tsx @@ -0,0 +1,57 @@ +import { FunctionComponent, useCallback, useEffect, useState } from 'react' +import MenuItem from '../Menu/MenuItem' +import { useApplication } from '../ApplicationView/ApplicationProvider' +import { FileBackupRecord, FileItem } from '@standardnotes/snjs' +import { dateToStringStyle1 } from '@/Utils/DateUtils' + +export const FileContextMenuBackupOption: FunctionComponent<{ file: FileItem }> = ({ file }) => { + const application = useApplication() + + const [backupInfo, setBackupInfo] = useState(undefined) + + useEffect(() => { + void application.fileBackups?.getFileBackupInfo(file).then(setBackupInfo) + }, [application, file]) + + const openFileBackup = useCallback(() => { + if (backupInfo) { + void application.fileBackups?.openFileBackup(backupInfo) + } + }, [backupInfo, application]) + + const configureFileBackups = useCallback(() => { + application.openPreferences('backups') + }, [application]) + + return ( + <> + {backupInfo && ( + +
+
Backed up on {dateToStringStyle1(backupInfo.backedUpOn)}
+
{backupInfo.absolutePath}
+
+
+ )} + + {!backupInfo && application.fileBackups && ( + +
+
Configure file backups
+
File not backed up locally
+
+
+ )} + + ) +} diff --git a/packages/web/src/javascripts/Components/FileContextMenu/FileMenuOptions.tsx b/packages/web/src/javascripts/Components/FileContextMenu/FileMenuOptions.tsx index 5c2c74fd0..0d3ee1526 100644 --- a/packages/web/src/javascripts/Components/FileContextMenu/FileMenuOptions.tsx +++ b/packages/web/src/javascripts/Components/FileContextMenu/FileMenuOptions.tsx @@ -10,6 +10,7 @@ import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider' import { AppPaneId } from '../ResponsivePane/AppPaneMetadata' import MenuItem from '../Menu/MenuItem' import { MenuItemType } from '../Menu/MenuItemType' +import { FileContextMenuBackupOption } from './FileContextMenuBackupOption' type Props = { closeMenu: () => void @@ -120,6 +121,9 @@ const FileMenuOptions: FunctionComponent = ({ Delete permanently + + +
{!hasSelectedMultipleFiles && ( diff --git a/packages/web/src/javascripts/Components/Icon/IconNameToSvgMapping.tsx b/packages/web/src/javascripts/Components/Icon/IconNameToSvgMapping.tsx index 028ab6322..2cc99f8ad 100644 --- a/packages/web/src/javascripts/Components/Icon/IconNameToSvgMapping.tsx +++ b/packages/web/src/javascripts/Components/Icon/IconNameToSvgMapping.tsx @@ -46,6 +46,7 @@ export const IconNameToSvgMapping = { 'star-circle-filled': icons.StarCircleFilled, 'star-filled': icons.StarFilledIcon, 'star-variant-filled': icons.StarVariantFilledIcon, + 'safe-square': icons.SafeSquareIcon, 'trash-filled': icons.TrashFilledIcon, 'trash-sweep': icons.TrashSweepIcon, 'user-add': icons.UserAddIcon, diff --git a/packages/web/src/javascripts/Components/LinkedItems/LinkedFileMenuOptions.tsx b/packages/web/src/javascripts/Components/LinkedItems/LinkedFileMenuOptions.tsx index 66e7a2baa..afa1fb538 100644 --- a/packages/web/src/javascripts/Components/LinkedItems/LinkedFileMenuOptions.tsx +++ b/packages/web/src/javascripts/Components/LinkedItems/LinkedFileMenuOptions.tsx @@ -3,6 +3,7 @@ import { FilesController } from '@/Controllers/FilesController' import { FileItem } from '@standardnotes/snjs' import { useState } from 'react' import { PopoverFileItemActionType } from '../AttachedFilesPopover/PopoverFileItemAction' +import { FileContextMenuBackupOption } from '../FileContextMenu/FileContextMenuBackupOption' import Icon from '../Icon/Icon' import HorizontalSeparator from '../Shared/HorizontalSeparator' import Switch from '../Switch/Switch' @@ -91,6 +92,8 @@ const LinkedFileMenuOptions = ({ file, closeMenu, handleFileAction, setIsRenamin Delete permanently + + ) } diff --git a/packages/web/src/javascripts/Components/Menu/MenuItem.tsx b/packages/web/src/javascripts/Components/Menu/MenuItem.tsx index 482444dfd..15fa53400 100644 --- a/packages/web/src/javascripts/Components/Menu/MenuItem.tsx +++ b/packages/web/src/javascripts/Components/Menu/MenuItem.tsx @@ -76,17 +76,20 @@ const MenuItem = forwardRef( role={type === MenuItemType.RadioButton ? 'menuitemradio' : 'menuitem'} tabIndex={typeof tabIndex === 'number' ? tabIndex : FOCUSABLE_BUT_NOT_TABBABLE} className={classNames( - 'flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-2 text-left md:py-1.5', + 'flex w-full cursor-pointer border-0 bg-transparent px-3 py-2 text-left md:py-1.5', 'text-mobile-menu-item text-text hover:bg-contrast hover:text-foreground', 'focus:bg-info-backdrop focus:shadow-none md:text-tablet-menu-item lg:text-menu-item', className, + className.includes('items-') ? '' : 'items-center', )} onClick={onClick} onBlur={onBlur} {...(type === MenuItemType.RadioButton ? { 'aria-checked': checked } : {})} > {shortcut && } - {type === MenuItemType.IconButton && icon ? : null} + {type === MenuItemType.IconButton && icon ? ( + + ) : null} {type === MenuItemType.RadioButton && typeof checked === 'boolean' ? ( ) : null} diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Backups/Files/FileBackupsCrossPlatform.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Backups/Files/FileBackupsCrossPlatform.tsx index 011b08d6e..ec5fa4256 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Backups/Files/FileBackupsCrossPlatform.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Backups/Files/FileBackupsCrossPlatform.tsx @@ -21,7 +21,7 @@ const FileBackupsCrossPlatform = ({ application }: Props) => { File Backups - Automatically save encrypted backups of files uploaded to any device to this computer. + Automatically save encrypted backups of files uploaded on any device to this computer. To enable file backups, use the Standard Notes desktop application. diff --git a/packages/web/src/javascripts/Utils/DateUtils.ts b/packages/web/src/javascripts/Utils/DateUtils.ts index 37bb88e85..32ca9cddf 100644 --- a/packages/web/src/javascripts/Utils/DateUtils.ts +++ b/packages/web/src/javascripts/Utils/DateUtils.ts @@ -29,6 +29,14 @@ export const formatDateAndTimeForNote = (date: Date, includeTime = true) => { } } +export const dateToStringStyle1 = (date: Date) => { + const dateString = `${date.toLocaleDateString()}` + + return `${dateString} at ${date.toLocaleTimeString(undefined, { + timeStyle: 'short', + })}` +} + export const dateToHoursAndMinutesTimeString = (date: Date) => { return date.toLocaleTimeString(undefined, { timeStyle: 'short', diff --git a/yarn.lock b/yarn.lock index 45ae0a801..c359b42da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6264,6 +6264,7 @@ __metadata: lodash: ^4.17.21 reflect-metadata: ^0.1.13 ts-jest: ^29.0.3 + typescript: "*" languageName: unknown linkType: soft