From 74ce1c1c1e85528cb52cf29baebd6105a44f1b8e Mon Sep 17 00:00:00 2001 From: Nick Grosenbacher Date: Fri, 8 Nov 2024 13:12:36 -0500 Subject: [PATCH 1/7] SWC-7064 - Hook to upload one or more files to Synapse / S3 - Add `useUploadFileEntities`, which uploads and tracks the entire process of uploading a file to a Synapse-managed bucket or directly uploading to an S3 bucket over HTTP - Add `useSynapseMultipartUpload`, a simple `useMutation` wrapper over our existing upload logic - Update `SynapseClient` upload logic to accept an AbortController in place of a callback to cancel the upload. Callback argument preserved to avoid a breaking change. - Update `SynapseClient.calculateMd5` to be memoized based on the Blob. Also simplified internal code without logical changes for modern browsers. - Add `useConfirmItems`, a utility hook that tracks a set of items in state that the user must individually or collectively confirm. - Updated React, ReactDOM types and @testing-library packages to fix lint warnings where `act` had type `any` - Added `p-limit` package to enable limiting concurrent promises (such as the number of concurrent file uploads) --- apps/SageAccountWeb/package.json | 4 +- apps/synapse-oauth-signin/package.json | 2 +- apps/synapse-portal-framework/package.json | 4 +- package.json | 3 +- packages/synapse-react-client/jest.config.cjs | 2 + packages/synapse-react-client/package.json | 7 +- .../src/synapse-client/SynapseClient.ts | 167 +- .../file/useSynapseMultipartUpload.ts | 46 + .../src/testutils/ReactQueryMockUtils.ts | 4 +- .../src/utils/hooks/useConfirmItems.test.ts | 78 + .../src/utils/hooks/useConfirmItems.ts | 104 ++ .../useTrackFileUploads.test.ts | 2 +- .../useTrackFileUploads.ts | 2 +- .../useUploadFileEntities.test.ts | 1380 +++++++++++++++++ .../useUploadFileEntities.ts | 398 +++++ .../src/File/UploadDestination.ts | 2 +- pnpm-lock.yaml | 775 ++++----- 17 files changed, 2450 insertions(+), 530 deletions(-) create mode 100644 packages/synapse-react-client/src/synapse-queries/file/useSynapseMultipartUpload.ts create mode 100644 packages/synapse-react-client/src/utils/hooks/useConfirmItems.test.ts create mode 100644 packages/synapse-react-client/src/utils/hooks/useConfirmItems.ts create mode 100644 packages/synapse-react-client/src/utils/hooks/useUploadFileEntity/useUploadFileEntities.test.ts create mode 100644 packages/synapse-react-client/src/utils/hooks/useUploadFileEntity/useUploadFileEntities.ts diff --git a/apps/SageAccountWeb/package.json b/apps/SageAccountWeb/package.json index 6a86b7083e..523e936df2 100644 --- a/apps/SageAccountWeb/package.json +++ b/apps/SageAccountWeb/package.json @@ -29,8 +29,8 @@ "universal-cookie": "^4.0.4" }, "devDependencies": { - "@testing-library/jest-dom": "^6.4.6", - "@testing-library/react": "^16.0.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.0.1", "@testing-library/user-event": "^14.5.2", "@types/katex": "^0.5.0", "@types/node": "^20.14.10", diff --git a/apps/synapse-oauth-signin/package.json b/apps/synapse-oauth-signin/package.json index 66bf79ee75..f0dcd14282 100644 --- a/apps/synapse-oauth-signin/package.json +++ b/apps/synapse-oauth-signin/package.json @@ -27,7 +27,7 @@ }, "devDependencies": { "@sage-bionetworks/synapse-types": "workspace:*", - "@testing-library/react": "16.0.0", + "@testing-library/react": "16.0.1", "@testing-library/user-event": "^14.5.2", "@types/isomorphic-fetch": "^0.0.36", "@types/jest": "^29.5.12", diff --git a/apps/synapse-portal-framework/package.json b/apps/synapse-portal-framework/package.json index af19fd4cfc..33d05b7e46 100644 --- a/apps/synapse-portal-framework/package.json +++ b/apps/synapse-portal-framework/package.json @@ -39,8 +39,8 @@ "type-check": "tsc --noEmit" }, "devDependencies": { - "@testing-library/jest-dom": "^6.4.6", - "@testing-library/react": "^16.0.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.0.1", "@testing-library/user-event": "^14.5.2", "@types/katex": "^0.5.0", "@types/lodash": "^4.17.0", diff --git a/package.json b/package.json index e3a1a9c127..cbe20e9b24 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,8 @@ "overrides": { "word-wrap": "^1.2.4", "semver": "^7.5.4", - "@types/react": "18.2.64", + "@types/react": "18.3.12", + "@types/react-dom": "18.3.1", "goober": "2.1.9", "react-hot-toast": "2.2.0", "postcss": "^8.4.31" diff --git a/packages/synapse-react-client/jest.config.cjs b/packages/synapse-react-client/jest.config.cjs index b381995ee6..cae1f2b5ac 100644 --- a/packages/synapse-react-client/jest.config.cjs +++ b/packages/synapse-react-client/jest.config.cjs @@ -4,6 +4,8 @@ const esModules = [ 'lodash-es', 'nanoid', 'mui-one-time-password-input', + 'p-limit', + 'yocto-queue', ] /** @type {import('jest').Config} */ diff --git a/packages/synapse-react-client/package.json b/packages/synapse-react-client/package.json index 5bc7d5e9c9..bd0e0a3869 100644 --- a/packages/synapse-react-client/package.json +++ b/packages/synapse-react-client/package.json @@ -157,9 +157,9 @@ "@storybook/testing-library": "^0.2.2", "@storybook/theming": "^8.2.4", "@svgr/plugin-jsx": "^8.1.0", - "@testing-library/dom": "^10.3.0", - "@testing-library/jest-dom": "^6.4.6", - "@testing-library/react": "^16.0.0", + "@testing-library/dom": "^10.4.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.0.1", "@testing-library/user-event": "^14.5.2", "@types/brainhubeu__react-carousel": "1.15.0", "@types/dagre": "^0.7.52", @@ -214,6 +214,7 @@ "memfs": "^3.5.3", "msw": "^1.3.2", "msw-storybook-addon": "^1.10.0", + "p-limit": "^6.1.0", "path-browserify": "^1.0.1", "postcss-normalize": "^10.0.1", "prettier": "^2.8.8", diff --git a/packages/synapse-react-client/src/synapse-client/SynapseClient.ts b/packages/synapse-react-client/src/synapse-client/SynapseClient.ts index df047409b0..1e7d5dff91 100644 --- a/packages/synapse-react-client/src/synapse-client/SynapseClient.ts +++ b/packages/synapse-react-client/src/synapse-client/SynapseClient.ts @@ -1,4 +1,5 @@ import { JSONSchema7 } from 'json-schema' +import { memoize } from 'lodash-es' import SparkMD5 from 'spark-md5' import UniversalCookies from 'universal-cookie' import { @@ -2074,6 +2075,8 @@ export const uploadFile = ( contentType: string = file.type, progressCallback?: (progress: ProgressCallback) => void, getIsCancelled?: () => boolean, + onMd5Computed?: () => void, + abortController?: AbortController, ) => { return new Promise( (fileUploadResolve, fileUploadReject) => { @@ -2089,6 +2092,9 @@ export const uploadFile = ( storageLocationId, } calculateMd5(file).then((md5: string) => { + if (onMd5Computed) { + onMd5Computed() + } request.contentMD5Hex = md5 startMultipartUpload( accessToken, @@ -2099,54 +2105,57 @@ export const uploadFile = ( fileUploadReject, progressCallback, getIsCancelled, + abortController, ) }) }, ) } -export const calculateMd5 = (fileBlob: File | Blob): Promise => { +/** + * Calculate the MD5 of the data in a Blob. This function is memoized, so if the same {@link Blob} object is + * passed after the MD5 is computed, no computation will occur. + */ +export const calculateMd5: (blob: Blob) => Promise = memoize(blob => { // code taken from md5 example from library return new Promise((resolve, reject) => { - const blobSlice = File.prototype.slice, - file = fileBlob, - chunkSize = 2097152, // Read in chunks of 2MB - chunks = Math.ceil(file.size / chunkSize), - spark = new SparkMD5.ArrayBuffer(), - fileReader = new FileReader() + const chunkSize = 2097152 // Read in chunks of 2M + const chunks = Math.ceil(blob.size / chunkSize) + const spark = new SparkMD5.ArrayBuffer() + const fileReader = new FileReader() let currentChunk = 0 fileReader.onload = function (e) { - console.log('read chunk nr', currentChunk + 1, 'of', chunks) + console.debug('read chunk nr', currentChunk + 1, 'of', chunks) spark.append(fileReader.result as ArrayBuffer) // Append array buffer currentChunk++ if (currentChunk < chunks) { loadNext() } else { - console.log('finished loading') + console.debug('finished loading') const md5: string = spark.end() - console.info('computed hash', md5) // Compute hash + console.debug('computed hash', md5) // Compute hash resolve(md5) } } fileReader.onerror = function () { - console.warn('oops, something went wrong.') + console.warn('oops, something went wrong.', fileReader.error) reject(fileReader.error) } const loadNext = () => { const start = currentChunk * chunkSize, - end = start + chunkSize >= file.size ? file.size : start + chunkSize + end = start + chunkSize >= blob.size ? blob.size : start + chunkSize - fileReader.readAsArrayBuffer(blobSlice.call(file, start, end)) + fileReader.readAsArrayBuffer(blob.slice(start, end)) } loadNext() }) -} +}) -const processFilePart = ( +const processFilePart = async ( partNumber: number, multipartUploadStatus: MultipartUploadStatus, clientSidePartsState: boolean[], @@ -2158,6 +2167,7 @@ const processFilePart = ( fileUploadReject: (reason: any) => void, updateProgress: () => void, getIsCancelled?: () => boolean, + abortController?: AbortController, ) => { if (clientSidePartsState![partNumber - 1]) { // no-op. this part has already been processed! @@ -2172,73 +2182,73 @@ const processFilePart = ( partNumbers: [partNumber], } const presignedUrlUrl = `/file/v1/file/multipart/${uploadId}/presigned/url/batch` - doPost( + + const presignedUrlResponse = await doPost( presignedUrlUrl, presignedUploadUrlRequest, accessToken, BackendDestinationEnum.REPO_ENDPOINT, - ).then(async (presignedUrlResponse: BatchPresignedUploadUrlResponse) => { - const presignedUrl = - presignedUrlResponse.partPresignedUrls[0].uploadPresignedUrl - // calculate the byte range - const startByte = (partNumber - 1) * request.partSizeBytes - let endByte = partNumber * request.partSizeBytes - 1 - if (endByte >= request.fileSizeBytes) { - endByte = request.fileSizeBytes - 1 - } - const fileSlice = file.slice( - startByte, - endByte + 1, - presignedUploadUrlRequest.contentType, + ) + const presignedUrl = + presignedUrlResponse.partPresignedUrls[0].uploadPresignedUrl + // calculate the byte range + const startByte = (partNumber - 1) * request.partSizeBytes + let endByte = partNumber * request.partSizeBytes - 1 + if (endByte >= request.fileSizeBytes) { + endByte = request.fileSizeBytes - 1 + } + const fileSlice = file.slice( + startByte, + endByte + 1, + presignedUploadUrlRequest.contentType, + ) + await uploadFilePart( + presignedUrl, + fileSlice, + presignedUploadUrlRequest.contentType, + getIsCancelled, + abortController, + ) + + // uploaded the part. calculate md5 of the part and add the part to the upload + const md5 = await calculateMd5(fileSlice) + const addPartUrl = `/file/v1/file/multipart/${uploadId}/add/${partNumber}?partMD5Hex=${md5}` + const addPartResponse = await doPut( + addPartUrl, + undefined, + accessToken, + BackendDestinationEnum.REPO_ENDPOINT, + ) + if (addPartResponse.addPartState === 'ADD_SUCCESS') { + // done with this part! + clientSidePartsState![partNumber - 1] = true + updateProgress() + checkUploadComplete( + multipartUploadStatus, + clientSidePartsState, + fileName, + accessToken, + fileUploadResolve, + fileUploadReject, ) - await uploadFilePart( - presignedUrl, - fileSlice, - presignedUploadUrlRequest.contentType, - getIsCancelled, + } else { + // retry after a brief delay + await delay(1000) + await processFilePart( + partNumber, + multipartUploadStatus, + clientSidePartsState, + accessToken, + fileName, + file, + request, + fileUploadResolve, + fileUploadReject, + updateProgress, ) - // uploaded the part. calculate md5 of the part and add the part to the upload - calculateMd5(fileSlice).then((md5: string) => { - const addPartUrl = `/file/v1/file/multipart/${uploadId}/add/${partNumber}?partMD5Hex=${md5}` - doPut( - addPartUrl, - undefined, - accessToken, - BackendDestinationEnum.REPO_ENDPOINT, - ).then((addPartResponse: AddPartResponse) => { - if (addPartResponse.addPartState === 'ADD_SUCCESS') { - // done with this part! - clientSidePartsState![partNumber - 1] = true - updateProgress() - checkUploadComplete( - multipartUploadStatus, - clientSidePartsState, - fileName, - accessToken, - fileUploadResolve, - fileUploadReject, - ) - } else { - // retry after a brief delay - delay(1000).then(() => { - processFilePart( - partNumber, - multipartUploadStatus, - clientSidePartsState, - accessToken, - fileName, - file, - request, - fileUploadResolve, - fileUploadReject, - updateProgress, - ) - }) - } - }) - }) - }) + } } + export const checkUploadComplete = ( status: MultipartUploadStatus, clientSidePartsState: boolean[], @@ -2277,14 +2287,15 @@ const uploadFilePart = async ( file: Blob, contentType: string, getIsCancelled?: () => boolean, + abortController?: AbortController, ) => { const controller = new AbortController() - const signal = controller.signal + const signal = abortController?.signal || controller.signal const checkIsCancelled = () => { if (getIsCancelled) { const isCancelled = getIsCancelled() - if (isCancelled) { + if (isCancelled && !controller.signal.aborted) { controller.abort() } } @@ -2314,6 +2325,7 @@ export const startMultipartUpload = ( fileUploadReject: (reason: any) => void, progressCallback?: (progress: ProgressCallback) => void, getIsCancelled?: () => boolean, + abortController?: AbortController, ) => { const url = '/file/v1/file/multipart' doPost( @@ -2353,6 +2365,7 @@ export const startMultipartUpload = ( fileUploadReject, updateProgress, getIsCancelled, + abortController, ) } else { updateProgress() diff --git a/packages/synapse-react-client/src/synapse-queries/file/useSynapseMultipartUpload.ts b/packages/synapse-react-client/src/synapse-queries/file/useSynapseMultipartUpload.ts new file mode 100644 index 0000000000..130b41a71a --- /dev/null +++ b/packages/synapse-react-client/src/synapse-queries/file/useSynapseMultipartUpload.ts @@ -0,0 +1,46 @@ +import { SynapseClientError } from '@sage-bionetworks/synapse-client' +import { FileUploadComplete } from '@sage-bionetworks/synapse-types' +import { useMutation, UseMutationOptions } from '@tanstack/react-query' +import { uploadFile } from '../../synapse-client/SynapseClient' +import { useSynapseContext } from '../../utils/index' +import { FileUploadArgs } from './FileUploadArgs' + +type UseSynapseMultipartUploadArgs = FileUploadArgs & { + readonly storageLocationId: number | undefined +} + +export function useSynapseMultipartUpload( + options?: UseMutationOptions< + FileUploadComplete, + SynapseClientError, + UseSynapseMultipartUploadArgs + >, +) { + const { accessToken } = useSynapseContext() + return useMutation({ + ...options, + mutationFn: (args: UseSynapseMultipartUploadArgs) => { + const { + blob, + fileName, + storageLocationId, + contentType, + progressCallback, + abortController, + onMd5Computed, + } = args + + return uploadFile( + accessToken, + fileName, + blob, + storageLocationId, + contentType, + progressCallback, + undefined, + onMd5Computed, + abortController, + ) + }, + }) +} diff --git a/packages/synapse-react-client/src/testutils/ReactQueryMockUtils.ts b/packages/synapse-react-client/src/testutils/ReactQueryMockUtils.ts index ef64dc50cd..7e416ea04e 100644 --- a/packages/synapse-react-client/src/testutils/ReactQueryMockUtils.ts +++ b/packages/synapse-react-client/src/testutils/ReactQueryMockUtils.ts @@ -99,7 +99,9 @@ export function getUseQueryErrorMock( } } -export function getUseMutationMock(data: TData) { +export function getUseMutationMock( + data?: TData, +) { return { context: undefined, data: undefined, diff --git a/packages/synapse-react-client/src/utils/hooks/useConfirmItems.test.ts b/packages/synapse-react-client/src/utils/hooks/useConfirmItems.test.ts new file mode 100644 index 0000000000..b7843a8f72 --- /dev/null +++ b/packages/synapse-react-client/src/utils/hooks/useConfirmItems.test.ts @@ -0,0 +1,78 @@ +import { renderHook, act } from '@testing-library/react' +import useConfirmItems from './useConfirmItems' + +describe('useConfirmItems', () => { + it('should initialize with empty pending and confirmed items', () => { + const { result } = renderHook(() => useConfirmItems()) + + expect(result.current.pendingItems).toEqual([]) + expect(result.current.confirmedItems).toEqual([]) + }) + + it('should add items to pending list', () => { + const { result } = renderHook(() => useConfirmItems()) + + act(() => { + result.current.addItemsPendingConfirmation('item1', 'item2') + }) + + expect(result.current.pendingItems).toEqual(['item1', 'item2']) + }) + + it('should confirm items and move them to confirmed list', () => { + const { result } = renderHook(() => useConfirmItems()) + + let confirmedItemsResult: { + confirmedItems: unknown[] + pendingItems: unknown[] + } | null = null + act(() => { + result.current.addItemsPendingConfirmation('item1', 'item2') + }) + act(() => { + confirmedItemsResult = result.current.confirmItem('item1') + }) + + expect(confirmedItemsResult).toEqual({ + pendingItems: ['item2'], + confirmedItems: ['item1'], + }) + expect(result.current.pendingItems).toEqual(['item2']) + expect(result.current.confirmedItems).toEqual(['item1']) + }) + + it('should remove pending items', () => { + const { result } = renderHook(() => useConfirmItems()) + + let removePendingItemsResult: { + confirmedItems: unknown[] + pendingItems: unknown[] + } | null = null + act(() => { + result.current.addItemsPendingConfirmation('item1', 'item2') + }) + act(() => { + removePendingItemsResult = result.current.removePendingItems('item1') + }) + + expect(removePendingItemsResult).toEqual({ + pendingItems: ['item2'], + confirmedItems: [], + }) + expect(result.current.pendingItems).toEqual(['item2']) + expect(result.current.confirmedItems).toEqual([]) + }) + + it('should clear all items', () => { + const { result } = renderHook(() => useConfirmItems()) + + act(() => { + result.current.addItemsPendingConfirmation('item1', 'item2') + result.current.confirmItem('item1') + result.current.clear() + }) + + expect(result.current.pendingItems).toEqual([]) + expect(result.current.confirmedItems).toEqual([]) + }) +}) diff --git a/packages/synapse-react-client/src/utils/hooks/useConfirmItems.ts b/packages/synapse-react-client/src/utils/hooks/useConfirmItems.ts new file mode 100644 index 0000000000..10cbe3af4a --- /dev/null +++ b/packages/synapse-react-client/src/utils/hooks/useConfirmItems.ts @@ -0,0 +1,104 @@ +import { useReducer } from 'react' + +type UseConfirmItemsReturn = { + /** The set of items that are awaiting confirmation */ + pendingItems: T[] + /** The set of items that have been confirmed */ + confirmedItems: T[] + /** Adds one or more items to the pending list */ + addItemsPendingConfirmation: (...items: T[]) => void + /** Confirms one or more items, adding them to the confirmed list and removing them from the pending list + * @returns the confirmed items */ + confirmItem: (...items: T[]) => { confirmedItems: T[]; pendingItems: T[] } + /** Remove one or more items that are pending from the list + * @returns the resulting state of confirmed items and pending items */ + removePendingItems: (...items: T[]) => { + confirmedItems: T[] + pendingItems: T[] + } + /** Clear all items from the confirmed and pending lists */ + clear: () => void +} + +function reducer( + state: { pendingItems: T[]; confirmedItems: T[] }, + action: + | { type: 'addItemsPendingConfirmation'; newItems: T[] } + | { type: 'confirmItem'; itemsToConfirm: T[] } + | { type: 'removePendingItems'; itemsToRemove: T[] } + | { type: 'clear' }, +) { + switch (action.type) { + case 'addItemsPendingConfirmation': + return { + ...state, + pendingItems: [...state.pendingItems, ...action.newItems], + } + case 'confirmItem': { + const newConfirmedItems = [ + ...state.confirmedItems, + ...action.itemsToConfirm, + ] + const newPendingItems = state.pendingItems.filter( + i => !action.itemsToConfirm.includes(i), + ) + return { + confirmedItems: newConfirmedItems, + pendingItems: newPendingItems, + } + } + case 'removePendingItems': { + const newPendingItems = state.pendingItems.filter( + i => !action.itemsToRemove.includes(i), + ) + return { + confirmedItems: state.confirmedItems, + pendingItems: newPendingItems, + } + } + case 'clear': + return { confirmedItems: [], pendingItems: [] } + default: + return state + } +} + +/** + * Stateful hook used to track items that need to be confirmed by the user. Methods are provided to support confirming + * individual items or all items, as well as skipping confirmation for individual items or all items. + */ +function useConfirmItems(): UseConfirmItemsReturn { + const [state, dispatch] = useReducer(reducer, { + pendingItems: [], + confirmedItems: [], + }) + + const addItemsPendingConfirmation = (...items: T[]) => { + dispatch({ type: 'addItemsPendingConfirmation', newItems: items }) + } + + const confirmItem = (...items: T[]) => { + dispatch({ type: 'confirmItem', itemsToConfirm: items }) + return reducer(state, { type: 'confirmItem', itemsToConfirm: items }) + } + + const removePendingItems = (...items: T[]) => { + dispatch({ type: 'removePendingItems', itemsToRemove: items }) + return reducer(state, { type: 'removePendingItems', itemsToRemove: items }) + } + + const clear = () => { + dispatch({ type: 'clear' }) + } + + return { + pendingItems: state.pendingItems, + confirmedItems: state.confirmedItems, + addItemsPendingConfirmation, + confirmItem, + removePendingItems, + clear, + } +} + +export default useConfirmItems diff --git a/packages/synapse-react-client/src/utils/hooks/useUploadFileEntity/useTrackFileUploads.test.ts b/packages/synapse-react-client/src/utils/hooks/useUploadFileEntity/useTrackFileUploads.test.ts index 313d622f18..10728f676c 100644 --- a/packages/synapse-react-client/src/utils/hooks/useUploadFileEntity/useTrackFileUploads.test.ts +++ b/packages/synapse-react-client/src/utils/hooks/useUploadFileEntity/useTrackFileUploads.test.ts @@ -2,7 +2,7 @@ import { act, renderHook as _renderHook } from '@testing-library/react' import { createWrapper } from '../../../testutils/TestingLibraryUtils' import { useTrackFileUploads } from './useTrackFileUploads' -describe('useTrackUploads', () => { +describe('useTrackFileUploads', () => { function renderHook() { return _renderHook(() => useTrackFileUploads(), { wrapper: createWrapper(), diff --git a/packages/synapse-react-client/src/utils/hooks/useUploadFileEntity/useTrackFileUploads.ts b/packages/synapse-react-client/src/utils/hooks/useUploadFileEntity/useTrackFileUploads.ts index b2727e27a0..f9a9fa453e 100644 --- a/packages/synapse-react-client/src/utils/hooks/useUploadFileEntity/useTrackFileUploads.ts +++ b/packages/synapse-react-client/src/utils/hooks/useUploadFileEntity/useTrackFileUploads.ts @@ -126,7 +126,7 @@ export function useTrackFileUploads() { function pauseUpload(file: File) { const entry = trackedUploadProgress.get(file) if (entry != null) { - entry.abortController.abort() + entry.abortController.abort('Paused by user') } setStatus(file, 'PAUSED') } diff --git a/packages/synapse-react-client/src/utils/hooks/useUploadFileEntity/useUploadFileEntities.test.ts b/packages/synapse-react-client/src/utils/hooks/useUploadFileEntity/useUploadFileEntities.test.ts new file mode 100644 index 0000000000..6f30de6ff9 --- /dev/null +++ b/packages/synapse-react-client/src/utils/hooks/useUploadFileEntity/useUploadFileEntities.test.ts @@ -0,0 +1,1380 @@ +import {} from '@sage-bionetworks/synapse-client' +import { + FileEntity, + UploadType, + UploadDestination, +} from '@sage-bionetworks/synapse-types' +import { act, renderHook as _renderHook, waitFor } from '@testing-library/react' +import { mockExternalObjectStoreUploadDestination } from '../../../mocks/mock_upload_destination' +import { MOCK_CONTEXT_VALUE } from '../../../mocks/MockSynapseContext' +import { SYNAPSE_STORAGE_LOCATION_ID } from '../../../synapse-client' +import { + useCreateEntity, + useUpdateEntity, +} from '../../../synapse-queries/entity/useEntity' +import { useDirectUploadToS3 } from '../../../synapse-queries/file/useDirectUploadToS3' +import { useSynapseMultipartUpload } from '../../../synapse-queries/file/useSynapseMultipartUpload' +import { useGetDefaultUploadDestination } from '../../../synapse-queries/file/useUploadDestination' +import { + getUseMutationMock, + getUseQuerySuccessMock, +} from '../../../testutils/ReactQueryMockUtils' +import { createWrapper } from '../../../testutils/TestingLibraryUtils' +import { + PrepareDirsForUploadReturn, + usePrepareFileEntityUpload, +} from './usePrepareFileEntityUpload' +import { useUploadFileEntities } from './useUploadFileEntities' + +jest.mock('../../../synapse-queries/file/useUploadDestination', () => { + return { + useGetDefaultUploadDestination: jest.fn(), + } +}) + +jest.mock('../../../synapse-queries/file/useSynapseMultipartUpload', () => { + return { + useSynapseMultipartUpload: jest.fn(), + } +}) + +jest.mock('../../../synapse-queries/file/useDirectUploadToS3', () => { + return { + useDirectUploadToS3: jest.fn(), + } +}) + +jest.mock('./usePrepareFileEntityUpload', () => { + return { + usePrepareFileEntityUpload: jest.fn(), + } +}) + +jest.mock('../../../synapse-queries/entity/useEntity', () => { + return { + useCreateEntity: jest.fn(), + useUpdateEntity: jest.fn(), + } +}) + +const mockGetEntityById = jest.spyOn( + MOCK_CONTEXT_VALUE.synapseClient.entityServicesClient, + 'getRepoV1EntityId', +) + +const mockUseGetDefaultUploadDestination = jest.mocked( + useGetDefaultUploadDestination, +) +const mockUsePrepareFileEntityUpload = jest.mocked(usePrepareFileEntityUpload) +const mockUseSynapseMultipartUpload = jest.mocked(useSynapseMultipartUpload) +const mockUseDirectS3Upload = jest.mocked(useDirectUploadToS3) +const mockUseCreateEntity = jest.mocked(useCreateEntity) +const mockUseUpdateEntity = jest.mocked(useUpdateEntity) + +const mockSynapseUploadDestination: UploadDestination = { + concreteType: 'org.sagebionetworks.repo.model.file.S3UploadDestination', + storageLocationId: SYNAPSE_STORAGE_LOCATION_ID, + uploadType: UploadType.S3, +} + +function setupUploadDestinationMock(uploadDestination: UploadDestination) { + mockUseGetDefaultUploadDestination.mockReturnValue( + getUseQuerySuccessMock(uploadDestination), + ) +} + +function setupPrepareDirsForUploadMock( + prepareDirsForUploadReturn: PrepareDirsForUploadReturn, +) { + const usePrepareFileEntityUploadMockReturn = + getUseMutationMock(prepareDirsForUploadReturn) + mockUsePrepareFileEntityUpload.mockReturnValue( + usePrepareFileEntityUploadMockReturn, + ) + return usePrepareFileEntityUploadMockReturn +} + +describe('useUploadFileEntities', () => { + const parentId = 'syn123' + const s3DirectAccessKey = 'fake-access-key' + const s3DirectSecretKey = 'fake-secret-key' + + const file1 = new File(['file contents'], 'file1.txt') + const file2 = new File(['file contents'], 'file2.txt') + + const createdFileHandleId1 = '147421' + const createdFileHandleId2 = '147422' + + const createdEntity1: FileEntity = { + id: 'syn456', + parentId, + name: file1.name, + concreteType: 'org.sagebionetworks.repo.model.FileEntity', + dataFileHandleId: createdFileHandleId1, + } + const createdEntity2: FileEntity = { + id: 'syn457', + parentId, + name: file2.name, + concreteType: 'org.sagebionetworks.repo.model.FileEntity', + dataFileHandleId: createdFileHandleId2, + } + + function renderHook() { + return _renderHook( + () => + useUploadFileEntities(parentId, s3DirectAccessKey, s3DirectSecretKey), + { + wrapper: createWrapper(), + }, + ) + } + + beforeEach(() => { + jest.clearAllMocks() + }) + + test('upload one file into Synapse Storage via Synapse multipart upload', async () => { + setupUploadDestinationMock(mockSynapseUploadDestination) + + const prepareDirsForUploadReturn: PrepareDirsForUploadReturn = { + newFileEntities: [ + { file: file1, parentId: 'syn123', existingEntityId: null }, + ], + updatedFileEntities: [], + } + const usePrepareFileEntityUploadMockReturn = setupPrepareDirsForUploadMock( + prepareDirsForUploadReturn, + ) + + const useSynapseMultipartUploadMockReturn = getUseMutationMock({ + fileHandleId: createdFileHandleId1, + fileName: file1.name, + }) + mockUseSynapseMultipartUpload.mockReturnValue( + useSynapseMultipartUploadMockReturn, + ) + + mockUseDirectS3Upload.mockReturnValue(getUseMutationMock()) + + const useCreateEntityReturn = getUseMutationMock(createdEntity1) + mockUseCreateEntity.mockReturnValue(useCreateEntityReturn) + + const useUpdateEntityReturn = getUseMutationMock() + mockUseUpdateEntity.mockReturnValue(useUpdateEntityReturn) + + const { result: hook } = renderHook() + + await waitFor(() => { + expect(hook.current.state).toBe('WAITING') + expect(hook.current.activePrompts.length).toBe(0) + expect(hook.current.initiateUpload).toBeDefined() + }) + + act(() => { + hook.current.initiateUpload([file1]) + }) + + expect(usePrepareFileEntityUploadMockReturn.mutate).toHaveBeenCalledTimes(1) + expect(usePrepareFileEntityUploadMockReturn.mutate).toHaveBeenCalledWith( + { + parentId, + files: [file1], + }, + { onSuccess: expect.any(Function) }, + ) + + // Invoke the `onSuccess` callback passed to usePrepareFileEntityUploadMockReturn.mutate + act(() => { + usePrepareFileEntityUploadMockReturn.mutate.mock.lastCall![1]!.onSuccess!( + prepareDirsForUploadReturn, + { + parentId, + files: [file1], + }, + null, + ) + }) + + await waitFor(() => { + expect( + useSynapseMultipartUploadMockReturn.mutateAsync, + ).toHaveBeenCalledTimes(1) + expect( + useSynapseMultipartUploadMockReturn.mutateAsync, + ).toHaveBeenCalledWith({ + fileName: file1.name, + blob: file1, + storageLocationId: SYNAPSE_STORAGE_LOCATION_ID, + contentType: 'text/plain', + progressCallback: expect.any(Function), + abortController: expect.any(AbortController), + onMd5Computed: expect.any(Function), + }) + + expect(useCreateEntityReturn.mutateAsync).toHaveBeenCalledTimes(1) + expect(useCreateEntityReturn.mutateAsync).toHaveBeenCalledWith({ + parentId, + name: file1.name, + concreteType: 'org.sagebionetworks.repo.model.FileEntity', + dataFileHandleId: createdFileHandleId1, + }) + expect(useUpdateEntityReturn.mutateAsync).not.toHaveBeenCalled() + }) + + expect(hook.current.state).toBe('COMPLETE') + }) + + test('upload multiple files into Synapse Storage via Synapse multipart upload', async () => { + const files = [file1, file2] + + setupUploadDestinationMock(mockSynapseUploadDestination) + const prepareDirsForUploadReturn: PrepareDirsForUploadReturn = { + newFileEntities: [ + { file: file1, parentId: 'syn123', existingEntityId: null }, + { file: file2, parentId: 'syn123', existingEntityId: null }, + ], + updatedFileEntities: [], + } + const usePrepareFileEntityUploadMockReturn = setupPrepareDirsForUploadMock( + prepareDirsForUploadReturn, + ) + + const useSynapseMultipartUploadMockReturn = getUseMutationMock() + useSynapseMultipartUploadMockReturn.mutateAsync.mockResolvedValueOnce({ + fileHandleId: createdFileHandleId1, + fileName: file1.name, + }) + useSynapseMultipartUploadMockReturn.mutateAsync.mockResolvedValueOnce({ + fileHandleId: createdFileHandleId2, + fileName: file2.name, + }) + mockUseSynapseMultipartUpload.mockReturnValue( + useSynapseMultipartUploadMockReturn, + ) + mockUseDirectS3Upload.mockReturnValue(getUseMutationMock()) + + const useCreateEntityReturn = getUseMutationMock() + + useCreateEntityReturn.mutateAsync + .mockResolvedValueOnce(createdEntity1) + .mockResolvedValueOnce(createdEntity2) + mockUseCreateEntity.mockReturnValue(useCreateEntityReturn) + + const useUpdateEntityReturn = getUseMutationMock() + mockUseUpdateEntity.mockReturnValue(useUpdateEntityReturn) + + const { result: hook } = renderHook() + + await waitFor(() => { + expect(hook.current.state).toBe('WAITING') + expect(hook.current.activePrompts.length).toBe(0) + expect(hook.current.initiateUpload).toBeDefined() + }) + + act(() => { + hook.current.initiateUpload(files) + }) + + expect(usePrepareFileEntityUploadMockReturn.mutate).toHaveBeenCalledTimes(1) + expect(usePrepareFileEntityUploadMockReturn.mutate).toHaveBeenCalledWith( + { + parentId, + files: files, + }, + { onSuccess: expect.any(Function) }, + ) + + // Invoke the `onSuccess` callback passed to usePrepareFileEntityUploadMockReturn.mutate + act(() => { + usePrepareFileEntityUploadMockReturn.mutate.mock.lastCall![1]!.onSuccess!( + prepareDirsForUploadReturn, + { + parentId, + files: files, + }, + null, + ) + }) + + await waitFor(() => { + expect( + useSynapseMultipartUploadMockReturn.mutateAsync, + ).toHaveBeenCalledTimes(2) + expect( + useSynapseMultipartUploadMockReturn.mutateAsync, + ).toHaveBeenCalledWith({ + fileName: file1.name, + blob: file1, + storageLocationId: SYNAPSE_STORAGE_LOCATION_ID, + contentType: 'text/plain', + progressCallback: expect.any(Function), + abortController: expect.any(AbortController), + onMd5Computed: expect.any(Function), + }) + expect( + useSynapseMultipartUploadMockReturn.mutateAsync, + ).toHaveBeenCalledWith({ + fileName: file2.name, + blob: file2, + storageLocationId: SYNAPSE_STORAGE_LOCATION_ID, + contentType: 'text/plain', + progressCallback: expect.any(Function), + abortController: expect.any(AbortController), + onMd5Computed: expect.any(Function), + }) + + expect(useCreateEntityReturn.mutateAsync).toHaveBeenCalledTimes(2) + expect(useCreateEntityReturn.mutateAsync).toHaveBeenCalledWith({ + parentId, + name: file1.name, + concreteType: 'org.sagebionetworks.repo.model.FileEntity', + dataFileHandleId: createdFileHandleId1, + }) + expect(useCreateEntityReturn.mutateAsync).toHaveBeenCalledWith({ + parentId, + name: file2.name, + concreteType: 'org.sagebionetworks.repo.model.FileEntity', + dataFileHandleId: createdFileHandleId2, + }) + + expect(useUpdateEntityReturn.mutateAsync).not.toHaveBeenCalled() + }) + + expect(hook.current.state).toBe('COMPLETE') + }) + + describe('prompt user to create new FileEntity version', () => { + test('confirm new version', async () => { + const files = [file1, file2] + + mockUseGetDefaultUploadDestination.mockReturnValue( + getUseQuerySuccessMock({ + concreteType: + 'org.sagebionetworks.repo.model.file.S3UploadDestination', + storageLocationId: SYNAPSE_STORAGE_LOCATION_ID, + uploadType: UploadType.S3, + }), + ) + + const prepareDirsForUploadReturn: PrepareDirsForUploadReturn = { + newFileEntities: [], + updatedFileEntities: [ + { file: file1, parentId: 'syn123', existingEntityId: 'syn456' }, + { file: file2, parentId: 'syn123', existingEntityId: 'syn457' }, + ], + } + const usePrepareFileEntityUploadMockReturn = + getUseMutationMock( + prepareDirsForUploadReturn, + ) + mockUsePrepareFileEntityUpload.mockReturnValue( + usePrepareFileEntityUploadMockReturn, + ) + + const useSynapseMultipartUploadMockReturn = getUseMutationMock() + useSynapseMultipartUploadMockReturn.mutateAsync.mockResolvedValueOnce({ + fileHandleId: createdFileHandleId1, + fileName: file1.name, + }) + useSynapseMultipartUploadMockReturn.mutateAsync.mockResolvedValueOnce({ + fileHandleId: createdFileHandleId2, + fileName: file2.name, + }) + mockUseSynapseMultipartUpload.mockReturnValue( + useSynapseMultipartUploadMockReturn, + ) + mockUseDirectS3Upload.mockReturnValue(getUseMutationMock()) + + const useCreateEntityReturn = getUseMutationMock() + mockUseCreateEntity.mockReturnValue(useCreateEntityReturn) + + mockGetEntityById + .mockResolvedValueOnce(createdEntity1) + .mockResolvedValueOnce(createdEntity2) + + const useUpdateEntityReturn = getUseMutationMock() + useUpdateEntityReturn.mutateAsync.mockResolvedValueOnce(createdEntity1) + useUpdateEntityReturn.mutateAsync.mockResolvedValueOnce(createdEntity2) + mockUseUpdateEntity.mockReturnValue(useUpdateEntityReturn) + + const { result: hook } = renderHook() + + await waitFor(() => { + expect(hook.current.state).toBe('WAITING') + expect(hook.current.activePrompts.length).toBe(0) + expect(hook.current.initiateUpload).toBeDefined() + }) + + act(() => { + hook.current.initiateUpload(files) + }) + + expect(usePrepareFileEntityUploadMockReturn.mutate).toHaveBeenCalledTimes( + 1, + ) + expect(usePrepareFileEntityUploadMockReturn.mutate).toHaveBeenCalledWith( + { + parentId, + files: files, + }, + { onSuccess: expect.any(Function) }, + ) + + // Invoke the `onSuccess` callback passed to usePrepareFileEntityUploadMockReturn.mutate + act(() => { + usePrepareFileEntityUploadMockReturn.mutate.mock.lastCall![1]! + .onSuccess!( + prepareDirsForUploadReturn, + { + parentId, + files: files, + }, + null, + ) + }) + + await waitFor(() => { + expect(hook.current.state).toBe('PROMPT_USER') + expect(hook.current.activePrompts.length).toBe(2) + expect(hook.current.activePrompts[0]).toEqual({ + title: 'Update existing file?', + message: `A file named "file1.txt" (syn456) already exists in this location. Do you want to update the existing file and create a new version?`, + onConfirm: expect.any(Function), + onSkip: expect.any(Function), + onConfirmAll: expect.any(Function), + onCancelAll: expect.any(Function), + }) + expect(hook.current.activePrompts[1]).toEqual({ + title: 'Update existing file?', + message: `A file named "file2.txt" (syn457) already exists in this location. Do you want to update the existing file and create a new version?`, + onConfirm: expect.any(Function), + onSkip: expect.any(Function), + onConfirmAll: expect.any(Function), + onCancelAll: expect.any(Function), + }) + }) + + act(() => { + hook.current.activePrompts[0].onConfirm() + }) + + await waitFor(() => { + expect(hook.current.state).toBe('PROMPT_USER') + expect(hook.current.activePrompts.length).toBe(1) + expect(hook.current.activePrompts[0]).toEqual({ + title: 'Update existing file?', + message: `A file named "file2.txt" (syn457) already exists in this location. Do you want to update the existing file and create a new version?`, + onConfirm: expect.any(Function), + onSkip: expect.any(Function), + onConfirmAll: expect.any(Function), + onCancelAll: expect.any(Function), + }) + + // Uploads should not start until all prompts have been addressed + expect( + useSynapseMultipartUploadMockReturn.mutateAsync, + ).not.toHaveBeenCalled() + }) + + act(() => { + hook.current.activePrompts[0].onConfirm() + }) + + await waitFor(() => { + expect( + useSynapseMultipartUploadMockReturn.mutateAsync, + ).toHaveBeenCalledTimes(2) + expect( + useSynapseMultipartUploadMockReturn.mutateAsync, + ).toHaveBeenCalledWith({ + fileName: file1.name, + blob: file1, + storageLocationId: SYNAPSE_STORAGE_LOCATION_ID, + contentType: 'text/plain', + progressCallback: expect.any(Function), + abortController: expect.any(AbortController), + onMd5Computed: expect.any(Function), + }) + expect( + useSynapseMultipartUploadMockReturn.mutateAsync, + ).toHaveBeenCalledWith({ + fileName: file2.name, + blob: file2, + storageLocationId: SYNAPSE_STORAGE_LOCATION_ID, + contentType: 'text/plain', + progressCallback: expect.any(Function), + abortController: expect.any(AbortController), + onMd5Computed: expect.any(Function), + }) + + expect(useUpdateEntityReturn.mutateAsync).toHaveBeenCalledTimes(2) + expect(useUpdateEntityReturn.mutateAsync).toHaveBeenCalledWith( + createdEntity1, + ) + expect(useUpdateEntityReturn.mutateAsync).toHaveBeenCalledWith( + createdEntity2, + ) + expect(useCreateEntityReturn.mutateAsync).not.toHaveBeenCalled() + }) + + expect(hook.current.state).toBe('COMPLETE') + }) + + test('confirm all versions', async () => { + const files = [file1, file2] + + mockUseGetDefaultUploadDestination.mockReturnValue( + getUseQuerySuccessMock({ + concreteType: + 'org.sagebionetworks.repo.model.file.S3UploadDestination', + storageLocationId: SYNAPSE_STORAGE_LOCATION_ID, + uploadType: UploadType.S3, + }), + ) + + const prepareDirsForUploadReturn: PrepareDirsForUploadReturn = { + newFileEntities: [], + updatedFileEntities: [ + { file: file1, parentId: 'syn123', existingEntityId: 'syn456' }, + { file: file2, parentId: 'syn123', existingEntityId: 'syn457' }, + ], + } + const usePrepareFileEntityUploadMockReturn = + getUseMutationMock( + prepareDirsForUploadReturn, + ) + mockUsePrepareFileEntityUpload.mockReturnValue( + usePrepareFileEntityUploadMockReturn, + ) + + const useSynapseMultipartUploadMockReturn = getUseMutationMock() + useSynapseMultipartUploadMockReturn.mutateAsync.mockResolvedValueOnce({ + fileHandleId: createdFileHandleId1, + fileName: file1.name, + }) + useSynapseMultipartUploadMockReturn.mutateAsync.mockResolvedValueOnce({ + fileHandleId: createdFileHandleId2, + fileName: file2.name, + }) + mockUseSynapseMultipartUpload.mockReturnValue( + useSynapseMultipartUploadMockReturn, + ) + mockUseDirectS3Upload.mockReturnValue(getUseMutationMock()) + + const useCreateEntityReturn = getUseMutationMock() + mockUseCreateEntity.mockReturnValue(useCreateEntityReturn) + + mockGetEntityById + .mockResolvedValueOnce(createdEntity1) + .mockResolvedValueOnce(createdEntity2) + + const useUpdateEntityReturn = getUseMutationMock() + useUpdateEntityReturn.mutateAsync.mockResolvedValueOnce(createdEntity1) + useUpdateEntityReturn.mutateAsync.mockResolvedValueOnce(createdEntity2) + mockUseUpdateEntity.mockReturnValue(useUpdateEntityReturn) + + const { result: hook } = renderHook() + + await waitFor(() => { + expect(hook.current.state).toBe('WAITING') + expect(hook.current.activePrompts.length).toBe(0) + expect(hook.current.initiateUpload).toBeDefined() + }) + + act(() => { + hook.current.initiateUpload(files) + }) + + expect(usePrepareFileEntityUploadMockReturn.mutate).toHaveBeenCalledTimes( + 1, + ) + expect(usePrepareFileEntityUploadMockReturn.mutate).toHaveBeenCalledWith( + { + parentId, + files: files, + }, + { onSuccess: expect.any(Function) }, + ) + + // Invoke the `onSuccess` callback passed to usePrepareFileEntityUploadMockReturn.mutate + act(() => { + usePrepareFileEntityUploadMockReturn.mutate.mock.lastCall![1]! + .onSuccess!( + prepareDirsForUploadReturn, + { + parentId, + files: files, + }, + null, + ) + }) + + await waitFor(() => { + expect(hook.current.state).toBe('PROMPT_USER') + expect(hook.current.activePrompts.length).toBe(2) + expect(hook.current.activePrompts[0]).toEqual({ + title: 'Update existing file?', + message: `A file named "file1.txt" (syn456) already exists in this location. Do you want to update the existing file and create a new version?`, + onConfirm: expect.any(Function), + onSkip: expect.any(Function), + onConfirmAll: expect.any(Function), + onCancelAll: expect.any(Function), + }) + expect(hook.current.activePrompts[1]).toEqual({ + title: 'Update existing file?', + message: `A file named "file2.txt" (syn457) already exists in this location. Do you want to update the existing file and create a new version?`, + onConfirm: expect.any(Function), + onSkip: expect.any(Function), + onConfirmAll: expect.any(Function), + onCancelAll: expect.any(Function), + }) + }) + + act(() => { + hook.current.activePrompts[0].onConfirmAll() + }) + + await waitFor(() => { + expect( + useSynapseMultipartUploadMockReturn.mutateAsync, + ).toHaveBeenCalledTimes(2) + expect( + useSynapseMultipartUploadMockReturn.mutateAsync, + ).toHaveBeenCalledWith({ + fileName: file1.name, + blob: file1, + storageLocationId: SYNAPSE_STORAGE_LOCATION_ID, + contentType: 'text/plain', + progressCallback: expect.any(Function), + abortController: expect.any(AbortController), + onMd5Computed: expect.any(Function), + }) + expect( + useSynapseMultipartUploadMockReturn.mutateAsync, + ).toHaveBeenCalledWith({ + fileName: file2.name, + blob: file2, + storageLocationId: SYNAPSE_STORAGE_LOCATION_ID, + contentType: 'text/plain', + progressCallback: expect.any(Function), + abortController: expect.any(AbortController), + onMd5Computed: expect.any(Function), + }) + + expect(useUpdateEntityReturn.mutateAsync).toHaveBeenCalledTimes(2) + expect(useUpdateEntityReturn.mutateAsync).toHaveBeenCalledWith( + createdEntity1, + ) + expect(useUpdateEntityReturn.mutateAsync).toHaveBeenCalledWith( + createdEntity2, + ) + expect(useCreateEntityReturn.mutateAsync).not.toHaveBeenCalled() + }) + + expect(hook.current.state).toBe('COMPLETE') + }) + + test('skip new version', async () => { + const files = [file1, file2] + + mockUseGetDefaultUploadDestination.mockReturnValue( + getUseQuerySuccessMock({ + concreteType: + 'org.sagebionetworks.repo.model.file.S3UploadDestination', + storageLocationId: SYNAPSE_STORAGE_LOCATION_ID, + uploadType: UploadType.S3, + }), + ) + + const prepareDirsForUploadReturn: PrepareDirsForUploadReturn = { + newFileEntities: [], + updatedFileEntities: [ + { file: file1, parentId: 'syn123', existingEntityId: 'syn456' }, + { file: file2, parentId: 'syn123', existingEntityId: 'syn457' }, + ], + } + const usePrepareFileEntityUploadMockReturn = + getUseMutationMock( + prepareDirsForUploadReturn, + ) + mockUsePrepareFileEntityUpload.mockReturnValue( + usePrepareFileEntityUploadMockReturn, + ) + + const useSynapseMultipartUploadMockReturn = getUseMutationMock() + useSynapseMultipartUploadMockReturn.mutateAsync.mockResolvedValueOnce({ + fileHandleId: createdFileHandleId2, + fileName: file2.name, + }) + mockUseSynapseMultipartUpload.mockReturnValue( + useSynapseMultipartUploadMockReturn, + ) + mockUseDirectS3Upload.mockReturnValue(getUseMutationMock()) + + const useCreateEntityReturn = getUseMutationMock() + mockUseCreateEntity.mockReturnValue(useCreateEntityReturn) + + mockGetEntityById.mockResolvedValueOnce(createdEntity2) + + const useUpdateEntityReturn = getUseMutationMock() + useUpdateEntityReturn.mutateAsync.mockResolvedValueOnce(createdEntity2) + mockUseUpdateEntity.mockReturnValue(useUpdateEntityReturn) + + const { result: hook } = renderHook() + + await waitFor(() => { + expect(hook.current.state).toBe('WAITING') + expect(hook.current.activePrompts.length).toBe(0) + expect(hook.current.initiateUpload).toBeDefined() + }) + + act(() => { + hook.current.initiateUpload(files) + }) + + expect(usePrepareFileEntityUploadMockReturn.mutate).toHaveBeenCalledTimes( + 1, + ) + expect(usePrepareFileEntityUploadMockReturn.mutate).toHaveBeenCalledWith( + { + parentId, + files: files, + }, + { onSuccess: expect.any(Function) }, + ) + + // Invoke the `onSuccess` callback passed to usePrepareFileEntityUploadMockReturn.mutate + act(() => { + usePrepareFileEntityUploadMockReturn.mutate.mock.lastCall![1]! + .onSuccess!( + prepareDirsForUploadReturn, + { + parentId, + files: files, + }, + null, + ) + }) + + await waitFor(() => { + expect(hook.current.state).toBe('PROMPT_USER') + expect(hook.current.activePrompts.length).toBe(2) + expect(hook.current.activePrompts[0]).toEqual({ + title: 'Update existing file?', + message: `A file named "file1.txt" (syn456) already exists in this location. Do you want to update the existing file and create a new version?`, + onConfirm: expect.any(Function), + onSkip: expect.any(Function), + onConfirmAll: expect.any(Function), + onCancelAll: expect.any(Function), + }) + expect(hook.current.activePrompts[1]).toEqual({ + title: 'Update existing file?', + message: `A file named "file2.txt" (syn457) already exists in this location. Do you want to update the existing file and create a new version?`, + onConfirm: expect.any(Function), + onSkip: expect.any(Function), + onConfirmAll: expect.any(Function), + onCancelAll: expect.any(Function), + }) + }) + + act(() => { + hook.current.activePrompts[0].onSkip() + }) + + await waitFor(() => { + expect(hook.current.state).toBe('PROMPT_USER') + expect(hook.current.activePrompts.length).toBe(1) + expect(hook.current.activePrompts[0]).toEqual({ + title: 'Update existing file?', + message: `A file named "file2.txt" (syn457) already exists in this location. Do you want to update the existing file and create a new version?`, + onConfirm: expect.any(Function), + onSkip: expect.any(Function), + onConfirmAll: expect.any(Function), + onCancelAll: expect.any(Function), + }) + + // Uploads should not start until all prompts have been addressed + expect( + useSynapseMultipartUploadMockReturn.mutateAsync, + ).not.toHaveBeenCalled() + }) + + act(() => { + hook.current.activePrompts[0].onConfirm() + }) + + await waitFor(() => { + expect( + useSynapseMultipartUploadMockReturn.mutateAsync, + ).toHaveBeenCalledTimes(1) + expect( + useSynapseMultipartUploadMockReturn.mutateAsync, + ).toHaveBeenCalledWith({ + fileName: file2.name, + blob: file2, + storageLocationId: SYNAPSE_STORAGE_LOCATION_ID, + contentType: 'text/plain', + progressCallback: expect.any(Function), + abortController: expect.any(AbortController), + onMd5Computed: expect.any(Function), + }) + + expect(useUpdateEntityReturn.mutateAsync).toHaveBeenCalledTimes(1) + expect(useUpdateEntityReturn.mutateAsync).toHaveBeenCalledWith( + createdEntity2, + ) + expect(useCreateEntityReturn.mutateAsync).not.toHaveBeenCalled() + }) + + expect(hook.current.state).toBe('COMPLETE') + }) + + test('cancel all uploads', async () => { + const files = [file1, file2] + + mockUseGetDefaultUploadDestination.mockReturnValue( + getUseQuerySuccessMock({ + concreteType: + 'org.sagebionetworks.repo.model.file.S3UploadDestination', + storageLocationId: SYNAPSE_STORAGE_LOCATION_ID, + uploadType: UploadType.S3, + }), + ) + + const prepareDirsForUploadReturn: PrepareDirsForUploadReturn = { + newFileEntities: [], + updatedFileEntities: [ + { file: file1, parentId: 'syn123', existingEntityId: 'syn456' }, + { file: file2, parentId: 'syn123', existingEntityId: 'syn457' }, + ], + } + const usePrepareFileEntityUploadMockReturn = + getUseMutationMock( + prepareDirsForUploadReturn, + ) + mockUsePrepareFileEntityUpload.mockReturnValue( + usePrepareFileEntityUploadMockReturn, + ) + + const useSynapseMultipartUploadMockReturn = getUseMutationMock() + useSynapseMultipartUploadMockReturn.mutateAsync.mockResolvedValueOnce({ + fileHandleId: createdFileHandleId1, + fileName: file1.name, + }) + useSynapseMultipartUploadMockReturn.mutateAsync.mockResolvedValueOnce({ + fileHandleId: createdFileHandleId2, + fileName: file2.name, + }) + mockUseSynapseMultipartUpload.mockReturnValue( + useSynapseMultipartUploadMockReturn, + ) + mockUseDirectS3Upload.mockReturnValue(getUseMutationMock()) + + const useCreateEntityReturn = getUseMutationMock() + mockUseCreateEntity.mockReturnValue(useCreateEntityReturn) + + mockGetEntityById + .mockResolvedValueOnce(createdEntity1) + .mockResolvedValueOnce(createdEntity2) + + const useUpdateEntityReturn = getUseMutationMock() + useUpdateEntityReturn.mutateAsync.mockResolvedValueOnce(createdEntity1) + useUpdateEntityReturn.mutateAsync.mockResolvedValueOnce(createdEntity2) + mockUseUpdateEntity.mockReturnValue(useUpdateEntityReturn) + + const { result: hook } = renderHook() + + await waitFor(() => { + expect(hook.current.state).toBe('WAITING') + expect(hook.current.activePrompts.length).toBe(0) + expect(hook.current.initiateUpload).toBeDefined() + }) + + act(() => { + hook.current.initiateUpload(files) + }) + + expect(usePrepareFileEntityUploadMockReturn.mutate).toHaveBeenCalledTimes( + 1, + ) + expect(usePrepareFileEntityUploadMockReturn.mutate).toHaveBeenCalledWith( + { + parentId, + files: files, + }, + { onSuccess: expect.any(Function) }, + ) + + // Invoke the `onSuccess` callback passed to usePrepareFileEntityUploadMockReturn.mutate + act(() => { + usePrepareFileEntityUploadMockReturn.mutate.mock.lastCall![1]! + .onSuccess!( + prepareDirsForUploadReturn, + { + parentId, + files: files, + }, + null, + ) + }) + + await waitFor(() => { + expect(hook.current.state).toBe('PROMPT_USER') + expect(hook.current.activePrompts.length).toBe(2) + expect(hook.current.activePrompts[0]).toEqual({ + title: 'Update existing file?', + message: `A file named "file1.txt" (syn456) already exists in this location. Do you want to update the existing file and create a new version?`, + onConfirm: expect.any(Function), + onSkip: expect.any(Function), + onConfirmAll: expect.any(Function), + onCancelAll: expect.any(Function), + }) + expect(hook.current.activePrompts[1]).toEqual({ + title: 'Update existing file?', + message: `A file named "file2.txt" (syn457) already exists in this location. Do you want to update the existing file and create a new version?`, + onConfirm: expect.any(Function), + onSkip: expect.any(Function), + onConfirmAll: expect.any(Function), + onCancelAll: expect.any(Function), + }) + }) + + act(() => { + hook.current.activePrompts[0].onCancelAll() + }) + + await waitFor(() => { + expect(hook.current.state).toBe('WAITING') + expect(hook.current.activePrompts.length).toBe(0) + expect(hook.current.uploadProgress.length).toBe(0) + }) + + expect( + useSynapseMultipartUploadMockReturn.mutateAsync, + ).not.toHaveBeenCalled() + expect(useUpdateEntityReturn.mutateAsync).not.toHaveBeenCalled() + expect(useCreateEntityReturn.mutateAsync).not.toHaveBeenCalled() + }) + }) + + test('pause and resume an upload', async () => { + setupUploadDestinationMock(mockSynapseUploadDestination) + const prepareDirsForUploadReturn: PrepareDirsForUploadReturn = { + newFileEntities: [ + { file: file1, parentId: 'syn123', existingEntityId: null }, + ], + updatedFileEntities: [], + } + const usePrepareFileEntityUploadMockReturn = setupPrepareDirsForUploadMock( + prepareDirsForUploadReturn, + ) + + const useSynapseMultipartUploadMockReturn = getUseMutationMock({ + fileHandleId: createdFileHandleId1, + fileName: file1.name, + }) + mockUseSynapseMultipartUpload.mockReturnValue( + useSynapseMultipartUploadMockReturn, + ) + useSynapseMultipartUploadMockReturn.mutateAsync.mockRejectedValueOnce( + new Error('the request was aborted.'), + ) + mockUseDirectS3Upload.mockReturnValue(getUseMutationMock()) + + const useCreateEntityReturn = getUseMutationMock(createdEntity1) + mockUseCreateEntity.mockReturnValue(useCreateEntityReturn) + + const useUpdateEntityReturn = getUseMutationMock() + mockUseUpdateEntity.mockReturnValue(useUpdateEntityReturn) + + const { result: hook } = renderHook() + + await waitFor(() => { + expect(hook.current.state).toBe('WAITING') + expect(hook.current.activePrompts.length).toBe(0) + expect(hook.current.initiateUpload).toBeDefined() + }) + + act(() => { + hook.current.initiateUpload([file1]) + }) + + expect(usePrepareFileEntityUploadMockReturn.mutate).toHaveBeenCalledTimes(1) + expect(usePrepareFileEntityUploadMockReturn.mutate).toHaveBeenCalledWith( + { + parentId, + files: [file1], + }, + { onSuccess: expect.any(Function) }, + ) + + // Invoke the `onSuccess` callback passed to usePrepareFileEntityUploadMockReturn.mutate + act(() => { + usePrepareFileEntityUploadMockReturn.mutate.mock.lastCall![1]!.onSuccess!( + prepareDirsForUploadReturn, + { + parentId, + files: [file1], + }, + null, + ) + }) + + expect(hook.current.state).toBe('UPLOADING') + + act(() => { + hook.current.uploadProgress[0].pause() + }) + + await waitFor(() => { + expect( + useSynapseMultipartUploadMockReturn.mutateAsync, + ).toHaveBeenCalledTimes(1) + expect( + useSynapseMultipartUploadMockReturn.mutateAsync, + ).toHaveBeenCalledWith({ + fileName: file1.name, + blob: file1, + storageLocationId: SYNAPSE_STORAGE_LOCATION_ID, + contentType: 'text/plain', + progressCallback: expect.any(Function), + abortController: expect.any(AbortController), + onMd5Computed: expect.any(Function), + }) + + expect(useCreateEntityReturn.mutateAsync).not.toHaveBeenCalled() + expect(useUpdateEntityReturn.mutateAsync).not.toHaveBeenCalled() + }) + + act(() => { + hook.current.uploadProgress[0].resume() + }) + + expect(usePrepareFileEntityUploadMockReturn.mutate).toHaveBeenCalledTimes(2) + + act(() => { + // Invoke the `onSuccess` callback once more + usePrepareFileEntityUploadMockReturn.mutate.mock.lastCall![1]!.onSuccess!( + prepareDirsForUploadReturn, + { + parentId, + files: [file1], + }, + null, + ) + }) + + await waitFor(() => { + expect( + useSynapseMultipartUploadMockReturn.mutateAsync, + ).toHaveBeenCalledTimes(2) + expect( + useSynapseMultipartUploadMockReturn.mutateAsync, + ).toHaveBeenNthCalledWith(2, { + fileName: file1.name, + blob: file1, + storageLocationId: SYNAPSE_STORAGE_LOCATION_ID, + contentType: 'text/plain', + progressCallback: expect.any(Function), + abortController: expect.any(AbortController), + onMd5Computed: expect.any(Function), + }) + + expect(useCreateEntityReturn.mutateAsync).toHaveBeenCalledTimes(1) + expect(useCreateEntityReturn.mutateAsync).toHaveBeenCalledWith({ + parentId, + name: file1.name, + concreteType: 'org.sagebionetworks.repo.model.FileEntity', + dataFileHandleId: createdFileHandleId1, + }) + expect(useUpdateEntityReturn.mutateAsync).not.toHaveBeenCalled() + }) + + expect(hook.current.state).toBe('COMPLETE') + }) + + test('cancel and remove an upload', async () => { + setupUploadDestinationMock(mockSynapseUploadDestination) + const prepareDirsForUploadReturn: PrepareDirsForUploadReturn = { + newFileEntities: [ + { file: file1, parentId: 'syn123', existingEntityId: null }, + ], + updatedFileEntities: [], + } + const usePrepareFileEntityUploadMockReturn = setupPrepareDirsForUploadMock( + prepareDirsForUploadReturn, + ) + + const useSynapseMultipartUploadMockReturn = getUseMutationMock({ + fileHandleId: createdFileHandleId1, + fileName: file1.name, + }) + mockUseSynapseMultipartUpload.mockReturnValue( + useSynapseMultipartUploadMockReturn, + ) + useSynapseMultipartUploadMockReturn.mutateAsync.mockRejectedValueOnce( + new Error('the request was aborted.'), + ) + mockUseDirectS3Upload.mockReturnValue(getUseMutationMock()) + + const useCreateEntityReturn = getUseMutationMock(createdEntity1) + mockUseCreateEntity.mockReturnValue(useCreateEntityReturn) + + const useUpdateEntityReturn = getUseMutationMock() + mockUseUpdateEntity.mockReturnValue(useUpdateEntityReturn) + + const { result: hook } = renderHook() + + await waitFor(() => { + expect(hook.current.state).toBe('WAITING') + expect(hook.current.activePrompts.length).toBe(0) + expect(hook.current.initiateUpload).toBeDefined() + }) + + act(() => { + hook.current.initiateUpload([file1]) + }) + + expect(usePrepareFileEntityUploadMockReturn.mutate).toHaveBeenCalledTimes(1) + expect(usePrepareFileEntityUploadMockReturn.mutate).toHaveBeenCalledWith( + { + parentId, + files: [file1], + }, + { onSuccess: expect.any(Function) }, + ) + + // Invoke the `onSuccess` callback passed to usePrepareFileEntityUploadMockReturn.mutate + act(() => { + usePrepareFileEntityUploadMockReturn.mutate.mock.lastCall![1]!.onSuccess!( + prepareDirsForUploadReturn, + { + parentId, + files: [file1], + }, + null, + ) + }) + + expect(hook.current.state).toBe('UPLOADING') + + act(() => { + hook.current.uploadProgress[0].cancel() + }) + + await waitFor(() => { + expect( + useSynapseMultipartUploadMockReturn.mutateAsync, + ).toHaveBeenCalledTimes(1) + expect( + useSynapseMultipartUploadMockReturn.mutateAsync, + ).toHaveBeenCalledWith({ + fileName: file1.name, + blob: file1, + storageLocationId: SYNAPSE_STORAGE_LOCATION_ID, + contentType: 'text/plain', + progressCallback: expect.any(Function), + abortController: expect.any(AbortController), + onMd5Computed: expect.any(Function), + }) + + expect(useCreateEntityReturn.mutateAsync).not.toHaveBeenCalled() + expect(useUpdateEntityReturn.mutateAsync).not.toHaveBeenCalled() + }) + + act(() => { + hook.current.uploadProgress[0].remove() + }) + + await waitFor(() => { + expect(hook.current.state).toBe('WAITING') + expect(hook.current.uploadProgress.length).toBe(0) + expect(usePrepareFileEntityUploadMockReturn.mutate).toHaveBeenCalledTimes( + 1, + ) + expect(useCreateEntityReturn.mutateAsync).not.toHaveBeenCalled() + expect(useUpdateEntityReturn.mutateAsync).not.toHaveBeenCalled() + }) + }) + + test('upload fails, and user removes it', async () => { + setupUploadDestinationMock(mockSynapseUploadDestination) + const prepareDirsForUploadReturn: PrepareDirsForUploadReturn = { + newFileEntities: [ + { file: file1, parentId: 'syn123', existingEntityId: null }, + ], + updatedFileEntities: [], + } + const usePrepareFileEntityUploadMockReturn = setupPrepareDirsForUploadMock( + prepareDirsForUploadReturn, + ) + + const useSynapseMultipartUploadMockReturn = getUseMutationMock({ + fileHandleId: createdFileHandleId1, + fileName: file1.name, + }) + mockUseSynapseMultipartUpload.mockReturnValue( + useSynapseMultipartUploadMockReturn, + ) + useSynapseMultipartUploadMockReturn.mutateAsync.mockRejectedValue( + new Error('The upload failed'), + ) + mockUseDirectS3Upload.mockReturnValue(getUseMutationMock()) + + const useCreateEntityReturn = getUseMutationMock() + mockUseCreateEntity.mockReturnValue(useCreateEntityReturn) + const useUpdateEntityReturn = getUseMutationMock() + mockUseUpdateEntity.mockReturnValue(useUpdateEntityReturn) + + const { result: hook } = renderHook() + + await waitFor(() => { + expect(hook.current.state).toBe('WAITING') + expect(hook.current.activePrompts.length).toBe(0) + expect(hook.current.initiateUpload).toBeDefined() + }) + + act(() => { + hook.current.initiateUpload([file1]) + }) + + expect(usePrepareFileEntityUploadMockReturn.mutate).toHaveBeenCalledTimes(1) + expect(usePrepareFileEntityUploadMockReturn.mutate).toHaveBeenCalledWith( + { + parentId, + files: [file1], + }, + { onSuccess: expect.any(Function) }, + ) + + // Invoke the `onSuccess` callback passed to usePrepareFileEntityUploadMockReturn.mutate + act(() => { + usePrepareFileEntityUploadMockReturn.mutate.mock.lastCall![1]!.onSuccess!( + prepareDirsForUploadReturn, + { + parentId, + files: [file1], + }, + null, + ) + }) + + await waitFor(() => { + expect(hook.current.state).toBe('WAITING') + expect(hook.current.activePrompts.length).toBe(0) + expect(hook.current.uploadProgress.length).toBe(1) + expect(hook.current.uploadProgress[0].status).toBe('FAILED') + expect(hook.current.uploadProgress[0].failureReason).toEqual( + 'The upload failed', + ) + expect( + useSynapseMultipartUploadMockReturn.mutateAsync, + ).toHaveBeenCalledTimes(1) + expect( + useSynapseMultipartUploadMockReturn.mutateAsync, + ).toHaveBeenCalledWith({ + fileName: file1.name, + blob: file1, + storageLocationId: SYNAPSE_STORAGE_LOCATION_ID, + contentType: 'text/plain', + progressCallback: expect.any(Function), + abortController: expect.any(AbortController), + onMd5Computed: expect.any(Function), + }) + + expect(useCreateEntityReturn.mutateAsync).not.toHaveBeenCalled() + expect(useUpdateEntityReturn.mutateAsync).not.toHaveBeenCalled() + }) + }) + + test('upload file via direct S3 upload', async () => { + setupUploadDestinationMock(mockExternalObjectStoreUploadDestination) + + const prepareDirsForUploadReturn: PrepareDirsForUploadReturn = { + newFileEntities: [ + { file: file1, parentId: 'syn123', existingEntityId: null }, + ], + updatedFileEntities: [], + } + const usePrepareFileEntityUploadMockReturn = setupPrepareDirsForUploadMock( + prepareDirsForUploadReturn, + ) + + mockUseSynapseMultipartUpload.mockReturnValue(getUseMutationMock()) + + const useDirectS3UploadReturn = getUseMutationMock({ + id: createdFileHandleId1, + }) + mockUseDirectS3Upload.mockReturnValue(useDirectS3UploadReturn) + + const useCreateEntityReturn = getUseMutationMock(createdEntity1) + mockUseCreateEntity.mockReturnValue(useCreateEntityReturn) + + const useUpdateEntityReturn = getUseMutationMock() + mockUseUpdateEntity.mockReturnValue(useUpdateEntityReturn) + + const { result: hook } = renderHook() + + await waitFor(() => { + expect(hook.current.state).toBe('WAITING') + expect(hook.current.activePrompts.length).toBe(0) + expect(hook.current.initiateUpload).toBeDefined() + }) + + act(() => { + hook.current.initiateUpload([file1]) + }) + + expect(usePrepareFileEntityUploadMockReturn.mutate).toHaveBeenCalledTimes(1) + expect(usePrepareFileEntityUploadMockReturn.mutate).toHaveBeenCalledWith( + { + parentId, + files: [file1], + }, + { onSuccess: expect.any(Function) }, + ) + + // Invoke the `onSuccess` callback passed to usePrepareFileEntityUploadMockReturn.mutate + act(() => { + usePrepareFileEntityUploadMockReturn.mutate.mock.lastCall![1]!.onSuccess!( + prepareDirsForUploadReturn, + { + parentId, + files: [file1], + }, + null, + ) + }) + + await waitFor(() => { + expect(useDirectS3UploadReturn.mutateAsync).toHaveBeenCalledTimes(1) + expect(useDirectS3UploadReturn.mutateAsync).toHaveBeenCalledWith({ + fileName: file1.name, + blob: file1, + storageLocationId: + mockExternalObjectStoreUploadDestination.storageLocationId, + contentType: 'text/plain', + progressCallback: expect.any(Function), + abortController: expect.any(AbortController), + onMd5Computed: expect.any(Function), + accessKey: s3DirectAccessKey, + secretKey: s3DirectSecretKey, + bucketName: mockExternalObjectStoreUploadDestination.bucket, + endpoint: mockExternalObjectStoreUploadDestination.endpointUrl, + keyPrefixUUID: mockExternalObjectStoreUploadDestination.keyPrefixUUID, + }) + + expect(useCreateEntityReturn.mutateAsync).toHaveBeenCalledTimes(1) + expect(useCreateEntityReturn.mutateAsync).toHaveBeenCalledWith({ + parentId, + name: file1.name, + concreteType: 'org.sagebionetworks.repo.model.FileEntity', + dataFileHandleId: createdFileHandleId1, + }) + expect(useUpdateEntityReturn.mutateAsync).not.toHaveBeenCalled() + }) + + expect(hook.current.state).toBe('COMPLETE') + }) +}) diff --git a/packages/synapse-react-client/src/utils/hooks/useUploadFileEntity/useUploadFileEntities.ts b/packages/synapse-react-client/src/utils/hooks/useUploadFileEntity/useUploadFileEntities.ts new file mode 100644 index 0000000000..bdadd9910f --- /dev/null +++ b/packages/synapse-react-client/src/utils/hooks/useUploadFileEntity/useUploadFileEntities.ts @@ -0,0 +1,398 @@ +import { instanceOfExternalObjectStoreUploadDestination } from '@sage-bionetworks/synapse-client' +import { FileEntity } from '@sage-bionetworks/synapse-types' +import pLimit from 'p-limit' +import { useCallback, useMemo } from 'react' +import { + ProgressCallback, + SYNAPSE_STORAGE_LOCATION_ID, +} from '../../../synapse-client/SynapseClient' +import { + useCreateEntity, + useGetDefaultUploadDestination, + useUpdateEntity, +} from '../../../synapse-queries' +import { FileUploadArgs } from '../../../synapse-queries/file/FileUploadArgs' +import { useDirectUploadToS3 } from '../../../synapse-queries/file/useDirectUploadToS3' +import { useSynapseMultipartUpload } from '../../../synapse-queries/file/useSynapseMultipartUpload' +import { fixDefaultContentType } from '../../ContentTypeUtils' +import { useSynapseContext } from '../../context/index' +import useConfirmItems from '../useConfirmItems' +import { + FilePreparedForUpload, + PrepareDirsForUploadReturn, + usePrepareFileEntityUpload, +} from './usePrepareFileEntityUpload' +import { + TrackedUploadProgress, + useTrackFileUploads, +} from './useTrackFileUploads' + +type UploadFileStatus = + | 'PREPARING' + | 'UPLOADING' + | 'PAUSED' + | 'CANCELED_BY_USER' + | 'FAILED' + | 'COMPLETE' + +type Prompt = { + title: string + message: string + onConfirmAll: () => void + onConfirm: () => void + onSkip: () => void + onCancelAll: () => void +} + +type UploaderState = + | 'LOADING' + | 'WAITING' + | 'PROMPT_USER' + | 'UPLOADING' + | 'COMPLETE' + | 'ERROR' + +type UseUploadFileEntitiesReturn = { + state: UploaderState + isPrecheckingUpload: boolean + activePrompts: Prompt[] + initiateUpload: (files: File[]) => void + activeUploadCount: number + uploadProgress: { + file: File + progress: ProgressCallback + status: UploadFileStatus + cancel: () => void + pause: () => void + resume: () => void + remove: () => void + failureReason?: string + }[] +} + +// Limit the number of concurrent uploads to avoid overwhelming the browser +// Note that within one upload operation, multiple parts could be uploaded in parallel +const limitConcurrentUploads = pLimit(8) + +export function useUploadFileEntities( + /** The ID of the parent entity to upload files to */ + parentId: string, + /** Optional accessKey for a direct S3 upload */ + accessKey = '', + /** Optional secretKey for a direct S3 upload */ + secretKey = '', +): UseUploadFileEntitiesReturn { + const { synapseClient } = useSynapseContext() + + const { data: uploadDestination, isLoading: isLoadingUploadDestination } = + useGetDefaultUploadDestination(parentId) + const storageLocationId = + uploadDestination?.storageLocationId || SYNAPSE_STORAGE_LOCATION_ID + + const { + pendingItems: filesToConfirmNewVersion, + confirmItem: confirmUploadFileWithNewVersion, + addItemsPendingConfirmation: addFileToConfirmUploadNewVersion, + removePendingItems: skipFileRequiringNewVersion, + clear: clearPendingFiles, + } = useConfirmItems<{ + file: File + parentId: string + existingEntityId: string | null + }>() + + const { + trackedUploadProgress, + setProgress, + setIsUploading, + trackNewFiles, + cancelUpload, + pauseUpload, + removeUpload, + setComplete, + setFailed, + isUploading, + isUploadComplete, + activeUploadCount, + } = useTrackFileUploads() + + const state: UploaderState = useMemo(() => { + if (isLoadingUploadDestination) { + return 'LOADING' + } + if (filesToConfirmNewVersion.length > 0) { + return 'PROMPT_USER' + } + if (isUploading) { + return 'UPLOADING' + } + if (isUploadComplete) { + return 'COMPLETE' + } + return 'WAITING' + }, [ + filesToConfirmNewVersion.length, + isLoadingUploadDestination, + isUploadComplete, + isUploading, + ]) + + const { mutateAsync: createEntityWithNewFile } = useCreateEntity() + const { mutateAsync: updateEntityWithNewFile } = useUpdateEntity() + const { mutateAsync: uploadFile } = useSynapseMultipartUpload() + const { mutateAsync: uploadFileDirectlyToS3 } = useDirectUploadToS3() + + const uploadPreparedFile = useCallback( + async function uploadPreparedFile( + newTrackedUploadProgress: Map, + preparedFile: FilePreparedForUpload, + ) { + try { + const trackedProgress = newTrackedUploadProgress.get(preparedFile.file)! + + const uploadArgs: FileUploadArgs = { + fileName: preparedFile.file.name, + blob: preparedFile.file, + storageLocationId, + contentType: fixDefaultContentType( + preparedFile.file.type, + preparedFile.file.name, + ), + progressCallback: progress => { + if (progress) { + setProgress(preparedFile.file, progress) + } + }, + abortController: trackedProgress.abortController, + onMd5Computed: () => { + setIsUploading(preparedFile.file) + }, + } + + let newFileHandleId: string + if ( + uploadDestination && + instanceOfExternalObjectStoreUploadDestination(uploadDestination) && + uploadDestination.endpointUrl != null + ) { + const fileHandle = await uploadFileDirectlyToS3({ + ...uploadArgs, + bucketName: uploadDestination.bucket!, + endpoint: uploadDestination.endpointUrl, + keyPrefixUUID: uploadDestination.keyPrefixUUID!, + accessKey, + secretKey, + }) + newFileHandleId = fileHandle.id! + } else { + const fileUploadComplete = await uploadFile(uploadArgs) + + newFileHandleId = fileUploadComplete.fileHandleId + } + + if (preparedFile.existingEntityId) { + // Update the existing entity + const entity = + await synapseClient.entityServicesClient.getRepoV1EntityId({ + id: preparedFile.existingEntityId, + }) + await updateEntityWithNewFile({ + ...entity, + dataFileHandleId: newFileHandleId, + } as FileEntity) + // Mark file as done! + setComplete(preparedFile.file) + } else { + // else, it's a new file entity + const newFileEntity: FileEntity = { + parentId: preparedFile.parentId, + name: preparedFile.file.name, + concreteType: 'org.sagebionetworks.repo.model.FileEntity', + dataFileHandleId: newFileHandleId, + } + await createEntityWithNewFile(newFileEntity) + // Mark file as done! + setComplete(preparedFile.file) + } + } catch (e) { + console.error('File upload failed: ', e) + setFailed(preparedFile.file, e.message) + } + }, + [ + storageLocationId, + uploadDestination, + setProgress, + setIsUploading, + uploadFileDirectlyToS3, + accessKey, + secretKey, + uploadFile, + synapseClient.entityServicesClient, + updateEntityWithNewFile, + setComplete, + createEntityWithNewFile, + setFailed, + ], + ) + + const startUpload = useCallback( + (...preparedFiles: FilePreparedForUpload[]) => { + clearPendingFiles() + + const newTrackedUploadProgress = trackNewFiles(...preparedFiles) + + // TODO: We already got the storage location from the root, but what if one of the sub-folders already existed + // with a different storage location? should we use that storage location? + preparedFiles.map(preparedFile => + limitConcurrentUploads(() => + uploadPreparedFile(newTrackedUploadProgress, preparedFile), + ), + ) + }, + [clearPendingFiles, trackNewFiles, uploadPreparedFile], + ) + + const postPrepareUpload = useCallback( + ( + preparedFiles: PrepareDirsForUploadReturn, + /** If true, skip checking upload limits and prompting the user before creating a new version. + * This is useful when resuming an upload that has already been pre-checked. */ + skipChecks = false, + ) => { + const { newFileEntities, updatedFileEntities } = preparedFiles + if (newFileEntities.length == 0 && updatedFileEntities.length == 0) { + // Is this possible? + throw new Error('No files were provided to upload.') + } + + if (skipChecks) { + startUpload(...newFileEntities, ...updatedFileEntities) + } else { + if (newFileEntities.length > 0 && updatedFileEntities.length == 0) { + // No need to prompt the user, just go ahead and upload! + startUpload(...newFileEntities) + } + + if (updatedFileEntities.length > 0) { + // Set up state to ask the user to confirm that they want to upload new versions. + confirmUploadFileWithNewVersion(...newFileEntities) + addFileToConfirmUploadNewVersion(...updatedFileEntities) + } + } + }, + [ + addFileToConfirmUploadNewVersion, + confirmUploadFileWithNewVersion, + startUpload, + ], + ) + + const { mutate: prepareUpload, isPending: isPrecheckingUpload } = + usePrepareFileEntityUpload() + + const initiateUpload = useCallback( + (files: File[]) => { + prepareUpload( + { files, parentId }, + { + onSuccess: result => { + postPrepareUpload(result, false) + }, + }, + ) + }, + [parentId, postPrepareUpload, prepareUpload], + ) + + return useMemo(() => { + let activePrompts: Prompt[] = [] + if (filesToConfirmNewVersion.length > 0) { + activePrompts = filesToConfirmNewVersion.map(fileToPrompt => { + return { + title: 'Update existing file?', + message: `A file named "${ + fileToPrompt.file.name + }" (${fileToPrompt.existingEntityId!}) already exists in this location. Do you want to update the existing file and create a new version?`, + onConfirm: () => { + const { confirmedItems, pendingItems } = + confirmUploadFileWithNewVersion(fileToPrompt) + + if (confirmedItems.length > 0 && pendingItems.length == 0) { + void startUpload(...confirmedItems) + } + }, + onConfirmAll: () => { + const { confirmedItems } = confirmUploadFileWithNewVersion( + ...filesToConfirmNewVersion, + ) + + void startUpload(...confirmedItems) + }, + onSkip: () => { + const { confirmedItems, pendingItems } = + skipFileRequiringNewVersion(fileToPrompt) + + if (confirmedItems.length > 0 && pendingItems.length == 0) { + void startUpload(...confirmedItems) + } + }, + onCancelAll: () => { + clearPendingFiles() + }, + } + }) + } + + return { + state, + isPrecheckingUpload, + activeUploadCount, + initiateUpload: initiateUpload, + activePrompts: activePrompts, + uploadProgress: [...trackedUploadProgress].map(([file, progress]) => { + return { + file: file, + progress: progress.progress, + status: progress.status, + failureReason: progress.failureReason, + cancel: () => { + cancelUpload(file) + }, + pause: () => { + pauseUpload(file) + }, + resume: () => { + prepareUpload( + { files: [file], parentId: progress.parentId }, + { + onSuccess: result => { + postPrepareUpload(result, true) + }, + }, + ) + }, + remove() { + removeUpload(file) + }, + } + }), + } + }, [ + activeUploadCount, + cancelUpload, + clearPendingFiles, + confirmUploadFileWithNewVersion, + filesToConfirmNewVersion, + initiateUpload, + pauseUpload, + postPrepareUpload, + prepareUpload, + removeUpload, + skipFileRequiringNewVersion, + startUpload, + state, + trackedUploadProgress, + isPrecheckingUpload, + ]) +} diff --git a/packages/synapse-types/src/File/UploadDestination.ts b/packages/synapse-types/src/File/UploadDestination.ts index e7956d39d1..009e679eca 100644 --- a/packages/synapse-types/src/File/UploadDestination.ts +++ b/packages/synapse-types/src/File/UploadDestination.ts @@ -14,7 +14,7 @@ export interface UploadDestination { /* The enumeration of possible upload types. */ uploadType: UploadType /* If set, the client should show this banner every time an upload is initiated */ - banner: string + banner?: string } /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 737dfaea9f..0c835ce3ce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,7 +7,8 @@ settings: overrides: word-wrap: ^1.2.4 semver: ^7.5.4 - '@types/react': 18.2.64 + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 goober: 2.1.9 react-hot-toast: 2.2.0 postcss: ^8.4.31 @@ -47,7 +48,7 @@ importers: version: 28.6.0(@typescript-eslint/eslint-plugin@7.14.1(@typescript-eslint/parser@7.14.1(eslint@9.5.0)(typescript@5.5.2))(eslint@9.5.0)(typescript@5.5.2))(eslint@9.5.0)(jest@29.7.0)(typescript@5.5.2) eslint-plugin-jest-dom: specifier: ^5.4.0 - version: 5.4.0(@testing-library/dom@10.3.1)(eslint@9.5.0) + version: 5.4.0(@testing-library/dom@10.4.0)(eslint@9.5.0) eslint-plugin-react: specifier: ^7.34.3 version: 7.34.3(eslint@9.5.0) @@ -83,22 +84,22 @@ importers: version: 11.0.0 '@emotion/react': specifier: ^11.11.4 - version: 11.11.4(@types/react@18.2.64)(react@18.2.0) + version: 11.11.4(@types/react@18.3.12)(react@18.2.0) '@emotion/styled': specifier: ^11.11.0 - version: 11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0) + version: 11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0) '@mui/icons-material': specifier: ^5.15.13 - version: 5.15.13(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.2.64)(react@18.2.0) + version: 5.15.13(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.3.12)(react@18.2.0) '@mui/material': specifier: ^5.15.13 - version: 5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@mui/system': specifier: ^5.15.13 - version: 5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0) + version: 5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0) '@mui/utils': specifier: ^5.15.13 - version: 5.15.13(@types/react@18.2.64)(react@18.2.0) + version: 5.15.13(@types/react@18.3.12)(react@18.2.0) '@react-hookz/web': specifier: ^23.1.0 version: 23.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -149,14 +150,14 @@ importers: version: 4.0.4 devDependencies: '@testing-library/jest-dom': - specifier: ^6.4.6 - version: 6.4.6(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.14.10))(vitest@1.6.0(@types/node@20.14.10)(@vitest/ui@1.6.0)(jsdom@21.1.2)(sass@1.77.6)(terser@5.31.2)) + specifier: ^6.6.3 + version: 6.6.3 '@testing-library/react': - specifier: ^16.0.0 - version: 16.0.0(@testing-library/dom@10.3.1)(@types/react-dom@18.0.6)(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + specifier: ^16.0.1 + version: 16.0.1(@testing-library/dom@10.4.0)(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@testing-library/user-event': specifier: ^14.5.2 - version: 14.5.2(@testing-library/dom@10.3.1) + version: 14.5.2(@testing-library/dom@10.4.0) '@types/katex': specifier: ^0.5.0 version: 0.5.0 @@ -167,11 +168,11 @@ importers: specifier: ^2.29.2 version: 2.29.2 '@types/react': - specifier: 18.2.64 - version: 18.2.64 + specifier: 18.3.12 + version: 18.3.12 '@types/react-dom': - specifier: 18.0.6 - version: 18.0.6 + specifier: 18.3.1 + version: 18.3.1 '@types/react-easy-crop': specifier: ^2.0.0 version: 2.0.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -309,11 +310,11 @@ importers: version: 5.4.3(@types/node@20.14.10)(sass@1.77.6)(terser@5.31.2) devDependencies: '@types/react': - specifier: 18.2.64 - version: 18.2.64 + specifier: 18.3.12 + version: 18.3.12 '@types/react-dom': - specifier: 18.0.6 - version: 18.0.6 + specifier: 18.3.1 + version: 18.3.1 sass: specifier: ^1.72.0 version: 1.77.6 @@ -358,11 +359,11 @@ importers: version: 5.4.3(@types/node@20.14.10)(sass@1.77.6)(terser@5.31.2) devDependencies: '@types/react': - specifier: 18.2.64 - version: 18.2.64 + specifier: 18.3.12 + version: 18.3.12 '@types/react-dom': - specifier: 18.0.6 - version: 18.0.6 + specifier: 18.3.1 + version: 18.3.1 sass: specifier: ^1.72.0 version: 1.77.6 @@ -407,11 +408,11 @@ importers: version: 5.4.3(@types/node@20.14.10)(sass@1.77.6)(terser@5.31.2) devDependencies: '@types/react': - specifier: 18.2.64 - version: 18.2.64 + specifier: 18.3.12 + version: 18.3.12 '@types/react-dom': - specifier: 18.0.6 - version: 18.0.6 + specifier: 18.3.1 + version: 18.3.1 sass: specifier: ^1.72.0 version: 1.77.6 @@ -456,11 +457,11 @@ importers: version: 5.4.3(@types/node@20.14.10)(sass@1.77.6)(terser@5.31.2) devDependencies: '@types/react': - specifier: 18.2.64 - version: 18.2.64 + specifier: 18.3.12 + version: 18.3.12 '@types/react-dom': - specifier: 18.0.6 - version: 18.0.6 + specifier: 18.3.1 + version: 18.3.1 sass: specifier: ^1.72.0 version: 1.77.6 @@ -481,7 +482,7 @@ importers: dependencies: '@mui/material': specifier: ^5.15.13 - version: 5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@sage-bionetworks/synapse-portal-framework': specifier: workspace:* version: link:../../synapse-portal-framework @@ -508,11 +509,11 @@ importers: version: 5.4.3(@types/node@20.14.10)(sass@1.77.6)(terser@5.31.2) devDependencies: '@types/react': - specifier: 18.2.64 - version: 18.2.64 + specifier: 18.3.12 + version: 18.3.12 '@types/react-dom': - specifier: 18.0.6 - version: 18.0.6 + specifier: 18.3.1 + version: 18.3.1 sass: specifier: ^1.72.0 version: 1.77.6 @@ -560,11 +561,11 @@ importers: version: 5.4.3(@types/node@20.14.10)(sass@1.77.6)(terser@5.31.2) devDependencies: '@types/react': - specifier: 18.2.64 - version: 18.2.64 + specifier: 18.3.12 + version: 18.3.12 '@types/react-dom': - specifier: 18.0.6 - version: 18.0.6 + specifier: 18.3.1 + version: 18.3.1 sass: specifier: ^1.72.0 version: 1.77.6 @@ -585,10 +586,10 @@ importers: dependencies: '@mui/icons-material': specifier: ^5.15.13 - version: 5.15.13(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.2.64)(react@18.2.0) + version: 5.15.13(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.3.12)(react@18.2.0) '@mui/material': specifier: ^5.15.13 - version: 5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@sage-bionetworks/synapse-portal-framework': specifier: workspace:* version: link:../../synapse-portal-framework @@ -618,11 +619,11 @@ importers: version: 5.4.3(@types/node@20.14.10)(sass@1.77.6)(terser@5.31.2) devDependencies: '@types/react': - specifier: 18.2.64 - version: 18.2.64 + specifier: 18.3.12 + version: 18.3.12 '@types/react-dom': - specifier: 18.0.6 - version: 18.0.6 + specifier: 18.3.1 + version: 18.3.1 sass: specifier: ^1.72.0 version: 1.77.6 @@ -667,11 +668,11 @@ importers: version: 5.4.3(@types/node@20.14.10)(sass@1.77.6)(terser@5.31.2) devDependencies: '@types/react': - specifier: 18.2.64 - version: 18.2.64 + specifier: 18.3.12 + version: 18.3.12 '@types/react-dom': - specifier: 18.0.6 - version: 18.0.6 + specifier: 18.3.1 + version: 18.3.1 sass: specifier: ^1.72.0 version: 1.77.6 @@ -692,10 +693,10 @@ importers: dependencies: '@mui/icons-material': specifier: ^5.15.13 - version: 5.15.13(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.2.64)(react@18.2.0) + version: 5.15.13(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.3.12)(react@18.2.0) '@mui/material': specifier: ^5.15.13 - version: 5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@sage-bionetworks/synapse-portal-framework': specifier: workspace:* version: link:../../synapse-portal-framework @@ -722,11 +723,11 @@ importers: version: 5.4.3(@types/node@20.14.10)(sass@1.77.6)(terser@5.31.2) devDependencies: '@types/react': - specifier: 18.2.64 - version: 18.2.64 + specifier: 18.3.12 + version: 18.3.12 '@types/react-dom': - specifier: 18.0.6 - version: 18.0.6 + specifier: 18.3.1 + version: 18.3.1 sass: specifier: ^1.72.0 version: 1.77.6 @@ -774,11 +775,11 @@ importers: version: 5.4.3(@types/node@20.14.10)(sass@1.77.6)(terser@5.31.2) devDependencies: '@types/react': - specifier: 18.2.64 - version: 18.2.64 + specifier: 18.3.12 + version: 18.3.12 '@types/react-dom': - specifier: 18.0.6 - version: 18.0.6 + specifier: 18.3.1 + version: 18.3.1 sass: specifier: ^1.72.0 version: 1.77.6 @@ -799,16 +800,16 @@ importers: dependencies: '@emotion/react': specifier: ^11.11.4 - version: 11.11.4(@types/react@18.2.64)(react@18.2.0) + version: 11.11.4(@types/react@18.3.12)(react@18.2.0) '@emotion/styled': specifier: ^11.11.0 - version: 11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0) + version: 11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0) '@mui/icons-material': specifier: ^5.15.13 - version: 5.15.13(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.2.64)(react@18.2.0) + version: 5.15.13(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.3.12)(react@18.2.0) '@mui/material': specifier: ^5.15.13 - version: 5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@react-hookz/web': specifier: ^23.1.0 version: 23.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -862,11 +863,11 @@ importers: specifier: workspace:* version: link:../../packages/synapse-types '@testing-library/react': - specifier: 16.0.0 - version: 16.0.0(@testing-library/dom@10.3.1)(@types/react-dom@18.2.22)(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + specifier: 16.0.1 + version: 16.0.1(@testing-library/dom@10.4.0)(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@testing-library/user-event': specifier: ^14.5.2 - version: 14.5.2(@testing-library/dom@10.3.1) + version: 14.5.2(@testing-library/dom@10.4.0) '@types/isomorphic-fetch': specifier: ^0.0.36 version: 0.0.36 @@ -883,11 +884,11 @@ importers: specifier: ^2.29.2 version: 2.29.2 '@types/react': - specifier: 18.2.64 - version: 18.2.64 + specifier: 18.3.12 + version: 18.3.12 '@types/react-dom': - specifier: ^18.2.22 - version: 18.2.22 + specifier: 18.3.1 + version: 18.3.1 '@types/react-plotly.js': specifier: ^2.6.3 version: 2.6.3 @@ -989,16 +990,16 @@ importers: dependencies: '@emotion/react': specifier: ^11.11.4 - version: 11.11.4(@types/react@18.2.64)(react@18.2.0) + version: 11.11.4(@types/react@18.3.12)(react@18.2.0) '@emotion/styled': specifier: ^11.11.0 - version: 11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0) + version: 11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0) '@mui/icons-material': specifier: ^5.15.13 - version: 5.15.13(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.2.64)(react@18.2.0) + version: 5.15.13(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.3.12)(react@18.2.0) '@mui/material': specifier: ^5.15.13 - version: 5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@react-hookz/web': specifier: ^23.1.0 version: 23.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -1055,14 +1056,14 @@ importers: version: link:../../packages/synapse-react-client devDependencies: '@testing-library/jest-dom': - specifier: ^6.4.6 - version: 6.4.6(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.14.10))(vitest@1.6.0(@types/node@20.14.10)(@vitest/ui@1.6.0)(jsdom@21.1.2)(sass@1.77.6)(terser@5.31.2)) + specifier: ^6.6.3 + version: 6.6.3 '@testing-library/react': - specifier: ^16.0.0 - version: 16.0.0(@testing-library/dom@10.3.1)(@types/react-dom@18.2.22)(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + specifier: ^16.0.1 + version: 16.0.1(@testing-library/dom@10.4.0)(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@testing-library/user-event': specifier: ^14.5.2 - version: 14.5.2(@testing-library/dom@10.3.1) + version: 14.5.2(@testing-library/dom@10.4.0) '@types/katex': specifier: ^0.5.0 version: 0.5.0 @@ -1076,8 +1077,8 @@ importers: specifier: ^2.29.2 version: 2.29.2 '@types/react': - specifier: 18.2.64 - version: 18.2.64 + specifier: 18.3.12 + version: 18.3.12 '@types/react-plotly.js': specifier: ^2.6.3 version: 2.6.3 @@ -1329,28 +1330,28 @@ importers: version: 1.19.26(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@emotion/react': specifier: ^11.11.4 - version: 11.11.4(@types/react@18.2.64)(react@18.2.0) + version: 11.11.4(@types/react@18.3.12)(react@18.2.0) '@emotion/styled': specifier: ^11.11.0 - version: 11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0) + version: 11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0) '@mui/icons-material': specifier: ^5.15.13 - version: 5.15.13(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.2.64)(react@18.2.0) + version: 5.15.13(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.3.12)(react@18.2.0) '@mui/material': specifier: ^5.15.13 - version: 5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@mui/system': specifier: ^5.15.13 - version: 5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0) + version: 5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0) '@mui/utils': specifier: ^5.15.13 - version: 5.15.13(@types/react@18.2.64)(react@18.2.0) + version: 5.15.13(@types/react@18.3.12)(react@18.2.0) '@mui/x-data-grid': specifier: ^6.19.6 - version: 6.19.6(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@mui/system@5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 6.19.6(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@mui/system@5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@mui/x-date-pickers': specifier: ^6.19.6 - version: 6.19.6(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@mui/system@5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(date-fns@2.30.0)(dayjs@1.11.10)(moment@2.30.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 6.19.6(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@mui/system@5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(date-fns@2.30.0)(dayjs@1.11.10)(moment@2.30.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@popperjs/core': specifier: ^2.11.8 version: 2.11.8 @@ -1368,7 +1369,7 @@ importers: version: 5.17.1(@rjsf/utils@5.17.1(react@18.2.0))(react@18.2.0) '@rjsf/mui': specifier: 5.17.1 - version: 5.17.1(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@mui/icons-material@5.15.13(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@rjsf/core@5.17.1(@rjsf/utils@5.17.1(react@18.2.0))(react@18.2.0))(@rjsf/utils@5.17.1(react@18.2.0))(react@18.2.0) + version: 5.17.1(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@mui/icons-material@5.15.13(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@rjsf/core@5.17.1(@rjsf/utils@5.17.1(react@18.2.0))(react@18.2.0))(@rjsf/utils@5.17.1(react@18.2.0))(react@18.2.0) '@rjsf/utils': specifier: 5.17.1 version: 5.17.1(react@18.2.0) @@ -1428,7 +1429,7 @@ importers: version: 4.1.0 jotai: specifier: ^2.7.0 - version: 2.7.0(@types/react@18.2.64)(react@18.2.0) + version: 2.7.0(@types/react@18.3.12)(react@18.2.0) json-rules-engine: specifier: ^4.1.0 version: 4.1.0 @@ -1473,7 +1474,7 @@ importers: version: link:../markdown-it-synapse-table mui-one-time-password-input: specifier: ^2.0.2 - version: 2.0.2(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 2.0.2(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) papaparse: specifier: ^5.4.1 version: 5.4.1 @@ -1536,7 +1537,7 @@ importers: version: 5.3.4(react@18.2.0) react-select: specifier: ^5.8.0 - version: 5.8.0(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 5.8.0(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-sizeme: specifier: ^3.0.2 version: 3.0.2 @@ -1560,7 +1561,7 @@ importers: version: 1.8.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0) reactflow: specifier: ^11.11.2 - version: 11.11.2(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 11.11.2(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) rss-parser: specifier: ^3.13.0 version: 3.13.0 @@ -1686,17 +1687,17 @@ importers: specifier: ^8.1.0 version: 8.1.0(@svgr/core@8.1.0(typescript@5.5.3)) '@testing-library/dom': - specifier: ^10.3.0 - version: 10.3.0 + specifier: ^10.4.0 + version: 10.4.0 '@testing-library/jest-dom': - specifier: ^6.4.6 - version: 6.4.6(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.14.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.10)(typescript@5.5.3)))(vitest@1.6.0(@types/node@20.14.10)(jsdom@21.1.2)(sass@1.77.6)(terser@5.31.2)) + specifier: ^6.6.3 + version: 6.6.3 '@testing-library/react': - specifier: ^16.0.0 - version: 16.0.0(@testing-library/dom@10.3.0)(@types/react-dom@18.2.22)(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + specifier: ^16.0.1 + version: 16.0.1(@testing-library/dom@10.4.0)(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@testing-library/user-event': specifier: ^14.5.2 - version: 14.5.2(@testing-library/dom@10.3.0) + version: 14.5.2(@testing-library/dom@10.4.0) '@types/brainhubeu__react-carousel': specifier: 1.15.0 version: 1.15.0 @@ -1749,14 +1750,14 @@ importers: specifier: ^1.5.5 version: 1.5.5 '@types/react': - specifier: 18.2.64 - version: 18.2.64 + specifier: 18.3.12 + version: 18.3.12 '@types/react-addons-css-transition-group': specifier: ^15.0.10 version: 15.0.10 '@types/react-dom': - specifier: ^18.2.22 - version: 18.2.22 + specifier: 18.3.1 + version: 18.3.1 '@types/react-mailchimp-subscribe': specifier: ^2.1.4 version: 2.1.4 @@ -1843,7 +1844,7 @@ importers: version: 3.6.0(jest@29.7.0(@types/node@20.14.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.10)(typescript@5.5.3))) jotai-devtools: specifier: ^0.6.3 - version: 0.6.3(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(redux@5.0.1) + version: 0.6.3(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(redux@5.0.1) jsdom: specifier: ^21.1.2 version: 21.1.2 @@ -1856,6 +1857,9 @@ importers: msw-storybook-addon: specifier: ^1.10.0 version: 1.10.0(msw@1.3.2(encoding@0.1.13)(typescript@5.5.3)) + p-limit: + specifier: ^6.1.0 + version: 6.1.0 path-browserify: specifier: ^1.0.1 version: 1.0.1 @@ -4116,7 +4120,7 @@ packages: '@mdx-js/react@3.0.1': resolution: {integrity: sha512-9ZrPIU4MGf6et1m1ov3zKf+q9+deetI51zprKB1D/z3NOb+rUxxtEl3mCjW5wTGh6VhRdwPueh1oRzi6ezkA8A==} peerDependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 react: '>=16' '@microsoft/api-extractor-model@7.29.6': @@ -4156,7 +4160,7 @@ packages: resolution: {integrity: sha512-puyUptF7VJ+9/dMIRLF+DLR21cWfvejsA6OnatfJfqFp8aMhya7xQtvYLEfCch6ahvFZvNC9FFEGGR+qkgFjUg==} engines: {node: '>=12.0.0'} peerDependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 react: ^17.0.0 || ^18.0.0 react-dom: ^17.0.0 || ^18.0.0 peerDependenciesMeta: @@ -4171,7 +4175,7 @@ packages: engines: {node: '>=12.0.0'} peerDependencies: '@mui/material': ^5.0.0 - '@types/react': 18.2.64 + '@types/react': 18.3.12 react: ^17.0.0 || ^18.0.0 peerDependenciesMeta: '@types/react': @@ -4183,7 +4187,7 @@ packages: peerDependencies: '@emotion/react': ^11.5.0 '@emotion/styled': ^11.3.0 - '@types/react': 18.2.64 + '@types/react': 18.3.12 react: ^17.0.0 || ^18.0.0 react-dom: ^17.0.0 || ^18.0.0 peerDependenciesMeta: @@ -4198,7 +4202,7 @@ packages: resolution: {integrity: sha512-j5Z2pRi6talCunIRIzpQERSaHwLd5EPdHMwIKDVCszro1RAzRZl7WmH68IMCgQmJMeglr+FalqNuq048qptGAg==} engines: {node: '>=12.0.0'} peerDependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 react: ^17.0.0 || ^18.0.0 peerDependenciesMeta: '@types/react': @@ -4223,7 +4227,7 @@ packages: peerDependencies: '@emotion/react': ^11.5.0 '@emotion/styled': ^11.3.0 - '@types/react': 18.2.64 + '@types/react': 18.3.12 react: ^17.0.0 || ^18.0.0 peerDependenciesMeta: '@emotion/react': @@ -4236,7 +4240,7 @@ packages: '@mui/types@7.2.13': resolution: {integrity: sha512-qP9OgacN62s+l8rdDhSFRe05HWtLLJ5TGclC9I1+tQngbssu0m2dmFZs+Px53AcOs9fD7TbYd4gc9AXzVqO/+g==} peerDependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 peerDependenciesMeta: '@types/react': optional: true @@ -4245,7 +4249,7 @@ packages: resolution: {integrity: sha512-qNlR9FLEhORC4zVZ3fzF48213EhP/92N71AcFbhHN73lPJjAbq9lUv+71P7uEdRHdrrOlm8+1zE8/OBy6MUqdg==} engines: {node: '>=12.0.0'} peerDependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 react: ^17.0.0 || ^18.0.0 peerDependenciesMeta: '@types/react': @@ -5417,12 +5421,8 @@ packages: resolution: {integrity: sha512-wdsYKy5zupPyLCW2Je5DLHSxSfbIp6h80WoHOQc+RPtmPGA52O9x5MJEkv92Sjonpq+poOAtUKhh1kBGAXBrNA==} engines: {node: '>=18'} - '@testing-library/dom@10.3.0': - resolution: {integrity: sha512-pT/TYB2+IyMYkkB6lqpkzD7VFbsR0JBJtflK3cS68sCNWxmOhWwRm1XvVHlseNEorsNcxkYsb4sRDV3aNIpttg==} - engines: {node: '>=18'} - - '@testing-library/dom@10.3.1': - resolution: {integrity: sha512-q/WL+vlXMpC0uXDyfsMtc1rmotzLV8Y0gq6q1gfrrDjQeHoeLrqHbxdPvPNAh1i+xuJl7+BezywcXArz7vLqKQ==} + '@testing-library/dom@10.4.0': + resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} engines: {node: '>=18'} '@testing-library/dom@9.3.4': @@ -5450,34 +5450,17 @@ packages: vitest: optional: true - '@testing-library/jest-dom@6.4.6': - resolution: {integrity: sha512-8qpnGVincVDLEcQXWaHOf6zmlbwTKc6Us6PPu4CRnPXCzo2OGBS5cwgMMOWdxDpEz1mkbvXHpEy99M5Yvt682w==} + '@testing-library/jest-dom@6.6.3': + resolution: {integrity: sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} - peerDependencies: - '@jest/globals': '>= 28' - '@types/bun': latest - '@types/jest': '>= 28' - jest: '>= 28' - vitest: '>= 0.32' - peerDependenciesMeta: - '@jest/globals': - optional: true - '@types/bun': - optional: true - '@types/jest': - optional: true - jest: - optional: true - vitest: - optional: true - '@testing-library/react@16.0.0': - resolution: {integrity: sha512-guuxUKRWQ+FgNX0h0NS0FIq3Q3uLtWVpBzcLOggmfMoUpgBnzBzvLLd4fbm6yS8ydJd94cIfY4yP9qUQjM2KwQ==} + '@testing-library/react@16.0.1': + resolution: {integrity: sha512-dSmwJVtJXmku+iocRhWOUFbrERC76TX2Mnf0ATODz8brzAZrMBbzLwQixlBSanZxR6LddK3eiwpSFZgDET1URg==} engines: {node: '>=18'} peerDependencies: '@testing-library/dom': ^10.0.0 - '@types/react': 18.2.64 - '@types/react-dom': ^18.0.0 + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 react: ^18.0.0 react-dom: ^18.0.0 peerDependenciesMeta: @@ -5849,11 +5832,8 @@ packages: '@types/react-addons-transition-group@15.0.10': resolution: {integrity: sha512-nV0vlq1ClUicGuDP5ocAHxsqWxjlxB+sBTF/+UMrafIJ/whkvmDrDGq/i0IcbnwptEXXmZKEQDYJ1F+0Vuao+A==} - '@types/react-dom@18.0.6': - resolution: {integrity: sha512-/5OFZgfIPSwy+YuIBP/FgJnQnsxhZhjjrnxudMddeblOouIodEQ75X14Rr4wGSG/bknL+Omy9iWlLo1u/9GzAA==} - - '@types/react-dom@18.2.22': - resolution: {integrity: sha512-fHkBXPeNtfvri6gdsMYyW+dW7RXFo6Ad09nLFK0VQWR7yGLai/Cyvyj696gbwYvBnhGtevUG9cET0pmUbMtoPQ==} + '@types/react-dom@18.3.1': + resolution: {integrity: sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==} '@types/react-easy-crop@2.0.0': resolution: {integrity: sha512-dUkoNMW3OrXQBYgozC43d3vYPQD23uJFt3ZmM4vcVrj7UjmIc2WxCnO9Zl5fs6bp7StnCtYb3Kp7h7s5T2Xu4w==} @@ -5887,8 +5867,8 @@ packages: '@types/react-window@1.8.8': resolution: {integrity: sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==} - '@types/react@18.2.64': - resolution: {integrity: sha512-MlmPvHgjj2p3vZaxbQgFUQFvD8QiZwACfGqEdDSWou5yISWxDQ4/74nCAwsUiX7UFLKZz3BbVSPj+YxeoGGCfg==} + '@types/react@18.3.12': + resolution: {integrity: sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==} '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} @@ -5896,9 +5876,6 @@ packages: '@types/resolve@1.20.6': resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} - '@types/scheduler@0.16.8': - resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==} - '@types/semver@7.5.8': resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} @@ -8995,7 +8972,7 @@ packages: resolution: {integrity: sha512-4qsyFKu4MprI39rj2uoItyhu24NoCHzkOV7z70PQr65SpzV6CSyhQvVIfbNlNqOIOspNMdf5OK+kTXLvqe63Jw==} engines: {node: '>=12.20.0'} peerDependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 react: '>=17.0.0' peerDependenciesMeta: '@types/react': @@ -9101,6 +9078,7 @@ packages: resolution: {integrity: sha512-Quz3MvAwHxVYNXsOByL7xI5EB2WYOeFswqaHIA3qOK3isRWTxiplBEocmmru6XmxDB2L7jDNYtYA4FyimoAFEw==} engines: {node: '>=8.17.0'} hasBin: true + bundledDependencies: [] jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} @@ -9597,7 +9575,7 @@ packages: '@emotion/react': ^11.5.0 '@emotion/styled': ^11.3.0 '@mui/material': ^5.0.0 - '@types/react': 18.2.64 + '@types/react': 18.3.12 react: ^18.0.0 react-dom: ^18.0.0 peerDependenciesMeta: @@ -9851,6 +9829,10 @@ packages: resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} engines: {node: '>=18'} + p-limit@6.1.0: + resolution: {integrity: sha512-H0jc0q1vOzlEk0TqAKXKZxdl7kX3OFUzCnNVUnq5Pc3DGo0kpeaMuPqxQn235HibwBEb0/pm9dgKTjXy66fBkg==} + engines: {node: '>=18'} + p-locate@3.0.0: resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} engines: {node: '>=6'} @@ -10346,7 +10328,7 @@ packages: react-json-tree@0.18.0: resolution: {integrity: sha512-Qe6HKSXrr++n9Y31nkRJ3XvQMATISpqigH1vEKhLwB56+nk5thTP0ITThpjxY6ZG/ubpVq/aEHIcyLP/OPHxeA==} peerDependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-lifecycles-compat@3.0.4: @@ -10399,7 +10381,7 @@ packages: engines: {node: '>=10'} deprecated: please update to the following version as this contains a bug (https://github.com/theKashey/react-remove-scroll-bar/issues/57) peerDependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 react: ^16.8.0 || ^17.0.0 || ^18.0.0 peerDependenciesMeta: '@types/react': @@ -10409,7 +10391,7 @@ packages: resolution: {integrity: sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==} engines: {node: '>=10'} peerDependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 react: ^16.8.0 || ^17.0.0 || ^18.0.0 peerDependenciesMeta: '@types/react': @@ -10458,7 +10440,7 @@ packages: resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} engines: {node: '>=10'} peerDependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 react: ^16.8.0 || ^17.0.0 || ^18.0.0 peerDependenciesMeta: '@types/react': @@ -11624,7 +11606,7 @@ packages: resolution: {integrity: sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ==} engines: {node: '>=10'} peerDependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 react: ^16.8.0 || ^17.0.0 || ^18.0.0 peerDependenciesMeta: '@types/react': @@ -11663,7 +11645,7 @@ packages: resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} engines: {node: '>=10'} peerDependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 react: ^16.8.0 || ^17.0.0 || ^18.0.0 peerDependenciesMeta: '@types/react': @@ -12080,6 +12062,10 @@ packages: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} engines: {node: '>=12.20'} + yocto-queue@1.1.1: + resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==} + engines: {node: '>=12.20'} + zod@3.22.4: resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} @@ -12087,7 +12073,7 @@ packages: resolution: {integrity: sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==} engines: {node: '>=12.7.0'} peerDependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 immer: '>=9.0.6' react: '>=16.8' peerDependenciesMeta: @@ -14434,7 +14420,7 @@ snapshots: '@emotion/memoize@0.8.1': {} - '@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0)': + '@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0)': dependencies: '@babel/runtime': 7.24.7 '@emotion/babel-plugin': 11.11.0 @@ -14446,7 +14432,7 @@ snapshots: hoist-non-react-statics: 3.3.2 react: 18.2.0 optionalDependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 transitivePeerDependencies: - supports-color @@ -14460,18 +14446,18 @@ snapshots: '@emotion/sheet@1.2.2': {} - '@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0)': + '@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0)': dependencies: '@babel/runtime': 7.24.7 '@emotion/babel-plugin': 11.11.0 '@emotion/is-prop-valid': 1.2.2 - '@emotion/react': 11.11.4(@types/react@18.2.64)(react@18.2.0) + '@emotion/react': 11.11.4(@types/react@18.3.12)(react@18.2.0) '@emotion/serialize': 1.1.3 '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) '@emotion/utils': 1.2.1 react: 18.2.0 optionalDependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 transitivePeerDependencies: - supports-color @@ -15084,17 +15070,17 @@ snapshots: '@lukeed/csprng@1.1.0': {} - '@mantine/core@6.0.21(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@mantine/hooks@6.0.21(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + '@mantine/core@6.0.21(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@mantine/hooks@6.0.21(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@floating-ui/react': 0.19.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@mantine/hooks': 6.0.21(react@18.2.0) - '@mantine/styles': 6.0.21(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@mantine/styles': 6.0.21(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@mantine/utils': 6.0.21(react@18.2.0) '@radix-ui/react-scroll-area': 1.0.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-remove-scroll: 2.5.7(@types/react@18.2.64)(react@18.2.0) - react-textarea-autosize: 8.3.4(@types/react@18.2.64)(react@18.2.0) + react-remove-scroll: 2.5.7(@types/react@18.3.12)(react@18.2.0) + react-textarea-autosize: 8.3.4(@types/react@18.3.12)(react@18.2.0) transitivePeerDependencies: - '@emotion/react' - '@types/react' @@ -15103,18 +15089,18 @@ snapshots: dependencies: react: 18.2.0 - '@mantine/prism@6.0.21(@mantine/core@6.0.21(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@mantine/hooks@6.0.21(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@mantine/hooks@6.0.21(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + '@mantine/prism@6.0.21(@mantine/core@6.0.21(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@mantine/hooks@6.0.21(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@mantine/hooks@6.0.21(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@mantine/core': 6.0.21(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@mantine/hooks@6.0.21(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@mantine/core': 6.0.21(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@mantine/hooks@6.0.21(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@mantine/hooks': 6.0.21(react@18.2.0) '@mantine/utils': 6.0.21(react@18.2.0) prism-react-renderer: 1.3.5(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - '@mantine/styles@6.0.21(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + '@mantine/styles@6.0.21(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@emotion/react': 11.11.4(@types/react@18.2.64)(react@18.2.0) + '@emotion/react': 11.11.4(@types/react@18.3.12)(react@18.2.0) clsx: 1.1.1 csstype: 3.0.9 react: 18.2.0 @@ -15149,10 +15135,10 @@ snapshots: '@mapbox/whoots-js@3.1.0': {} - '@mdx-js/react@3.0.1(@types/react@18.2.64)(react@18.2.0)': + '@mdx-js/react@3.0.1(@types/react@18.3.12)(react@18.2.0)': dependencies: '@types/mdx': 2.0.11 - '@types/react': 18.2.64 + '@types/react': 18.3.12 react: 18.2.0 '@microsoft/api-extractor-model@7.29.6(@types/node@20.14.10)': @@ -15228,38 +15214,38 @@ snapshots: outvariant: 1.4.3 strict-event-emitter: 0.5.1 - '@mui/base@5.0.0-beta.39(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + '@mui/base@5.0.0-beta.39(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.24.7 '@floating-ui/react-dom': 2.0.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - '@mui/types': 7.2.13(@types/react@18.2.64) - '@mui/utils': 5.15.13(@types/react@18.2.64)(react@18.2.0) + '@mui/types': 7.2.13(@types/react@18.3.12) + '@mui/utils': 5.15.13(@types/react@18.3.12)(react@18.2.0) '@popperjs/core': 2.11.8 clsx: 2.1.0 prop-types: 15.8.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) optionalDependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 '@mui/core-downloads-tracker@5.15.13': {} - '@mui/icons-material@5.15.13(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.2.64)(react@18.2.0)': + '@mui/icons-material@5.15.13(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.3.12)(react@18.2.0)': dependencies: '@babel/runtime': 7.24.7 - '@mui/material': 5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@mui/material': 5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 optionalDependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 - '@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + '@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.24.7 - '@mui/base': 5.0.0-beta.39(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@mui/base': 5.0.0-beta.39(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@mui/core-downloads-tracker': 5.15.13 - '@mui/system': 5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0) - '@mui/types': 7.2.13(@types/react@18.2.64) - '@mui/utils': 5.15.13(@types/react@18.2.64)(react@18.2.0) + '@mui/system': 5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0) + '@mui/types': 7.2.13(@types/react@18.3.12) + '@mui/utils': 5.15.13(@types/react@18.3.12)(react@18.2.0) '@types/react-transition-group': 4.4.10 clsx: 2.1.0 csstype: 3.1.3 @@ -15269,20 +15255,20 @@ snapshots: react-is: 18.2.0 react-transition-group: 4.4.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0) optionalDependencies: - '@emotion/react': 11.11.4(@types/react@18.2.64)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0) - '@types/react': 18.2.64 + '@emotion/react': 11.11.4(@types/react@18.3.12)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0) + '@types/react': 18.3.12 - '@mui/private-theming@5.15.13(@types/react@18.2.64)(react@18.2.0)': + '@mui/private-theming@5.15.13(@types/react@18.3.12)(react@18.2.0)': dependencies: '@babel/runtime': 7.24.7 - '@mui/utils': 5.15.13(@types/react@18.2.64)(react@18.2.0) + '@mui/utils': 5.15.13(@types/react@18.3.12)(react@18.2.0) prop-types: 15.8.1 react: 18.2.0 optionalDependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 - '@mui/styled-engine@5.15.11(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(react@18.2.0)': + '@mui/styled-engine@5.15.11(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.24.7 '@emotion/cache': 11.11.0 @@ -15290,30 +15276,30 @@ snapshots: prop-types: 15.8.1 react: 18.2.0 optionalDependencies: - '@emotion/react': 11.11.4(@types/react@18.2.64)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0) + '@emotion/react': 11.11.4(@types/react@18.3.12)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0) - '@mui/system@5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0)': + '@mui/system@5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0)': dependencies: '@babel/runtime': 7.24.7 - '@mui/private-theming': 5.15.13(@types/react@18.2.64)(react@18.2.0) - '@mui/styled-engine': 5.15.11(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(react@18.2.0) - '@mui/types': 7.2.13(@types/react@18.2.64) - '@mui/utils': 5.15.13(@types/react@18.2.64)(react@18.2.0) + '@mui/private-theming': 5.15.13(@types/react@18.3.12)(react@18.2.0) + '@mui/styled-engine': 5.15.11(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(react@18.2.0) + '@mui/types': 7.2.13(@types/react@18.3.12) + '@mui/utils': 5.15.13(@types/react@18.3.12)(react@18.2.0) clsx: 2.1.0 csstype: 3.1.3 prop-types: 15.8.1 react: 18.2.0 optionalDependencies: - '@emotion/react': 11.11.4(@types/react@18.2.64)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0) - '@types/react': 18.2.64 + '@emotion/react': 11.11.4(@types/react@18.3.12)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0) + '@types/react': 18.3.12 - '@mui/types@7.2.13(@types/react@18.2.64)': + '@mui/types@7.2.13(@types/react@18.3.12)': optionalDependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 - '@mui/utils@5.15.13(@types/react@18.2.64)(react@18.2.0)': + '@mui/utils@5.15.13(@types/react@18.3.12)(react@18.2.0)': dependencies: '@babel/runtime': 7.24.7 '@types/prop-types': 15.7.11 @@ -15321,14 +15307,14 @@ snapshots: react: 18.2.0 react-is: 18.2.0 optionalDependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 - '@mui/x-data-grid@6.19.6(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@mui/system@5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + '@mui/x-data-grid@6.19.6(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@mui/system@5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.24.7 - '@mui/material': 5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - '@mui/system': 5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0) - '@mui/utils': 5.15.13(@types/react@18.2.64)(react@18.2.0) + '@mui/material': 5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@mui/system': 5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0) + '@mui/utils': 5.15.13(@types/react@18.3.12)(react@18.2.0) clsx: 2.1.0 prop-types: 15.8.1 react: 18.2.0 @@ -15337,13 +15323,13 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@mui/x-date-pickers@6.19.6(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@mui/system@5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(date-fns@2.30.0)(dayjs@1.11.10)(moment@2.30.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + '@mui/x-date-pickers@6.19.6(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@mui/system@5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(date-fns@2.30.0)(dayjs@1.11.10)(moment@2.30.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.24.7 - '@mui/base': 5.0.0-beta.39(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - '@mui/material': 5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - '@mui/system': 5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0) - '@mui/utils': 5.15.13(@types/react@18.2.64)(react@18.2.0) + '@mui/base': 5.0.0-beta.39(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@mui/material': 5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@mui/system': 5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0) + '@mui/utils': 5.15.13(@types/react@18.3.12)(react@18.2.0) '@types/react-transition-group': 4.4.10 clsx: 2.1.0 prop-types: 15.8.1 @@ -15351,8 +15337,8 @@ snapshots: react-dom: 18.2.0(react@18.2.0) react-transition-group: 4.4.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0) optionalDependencies: - '@emotion/react': 11.11.4(@types/react@18.2.64)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0) + '@emotion/react': 11.11.4(@types/react@18.3.12)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0) date-fns: 2.30.0 dayjs: 1.11.10 moment: 2.30.1 @@ -15568,30 +15554,30 @@ snapshots: '@radix-ui/number@1.0.0': dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.24.8 '@radix-ui/primitive@1.0.0': dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.24.8 '@radix-ui/react-compose-refs@1.0.0(react@18.2.0)': dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.24.8 react: 18.2.0 '@radix-ui/react-context@1.0.0(react@18.2.0)': dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.24.8 react: 18.2.0 '@radix-ui/react-direction@1.0.0(react@18.2.0)': dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.24.8 react: 18.2.0 '@radix-ui/react-presence@1.0.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.24.8 '@radix-ui/react-compose-refs': 1.0.0(react@18.2.0) '@radix-ui/react-use-layout-effect': 1.0.0(react@18.2.0) react: 18.2.0 @@ -15599,14 +15585,14 @@ snapshots: '@radix-ui/react-primitive@1.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.24.8 '@radix-ui/react-slot': 1.0.1(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) '@radix-ui/react-scroll-area@1.0.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.24.8 '@radix-ui/number': 1.0.0 '@radix-ui/primitive': 1.0.0 '@radix-ui/react-compose-refs': 1.0.0(react@18.2.0) @@ -15621,18 +15607,18 @@ snapshots: '@radix-ui/react-slot@1.0.1(react@18.2.0)': dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.24.8 '@radix-ui/react-compose-refs': 1.0.0(react@18.2.0) react: 18.2.0 '@radix-ui/react-use-callback-ref@1.0.0(react@18.2.0)': dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.24.8 react: 18.2.0 '@radix-ui/react-use-layout-effect@1.0.0(react@18.2.0)': dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.24.8 react: 18.2.0 '@react-google-maps/api@2.19.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': @@ -15673,29 +15659,29 @@ snapshots: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - '@reactflow/background@11.3.12(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + '@reactflow/background@11.3.12(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@reactflow/core': 11.11.2(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@reactflow/core': 11.11.2(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) classcat: 5.0.4 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.2(@types/react@18.2.64)(react@18.2.0) + zustand: 4.5.2(@types/react@18.3.12)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/controls@11.2.12(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + '@reactflow/controls@11.2.12(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@reactflow/core': 11.11.2(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@reactflow/core': 11.11.2(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) classcat: 5.0.4 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.2(@types/react@18.2.64)(react@18.2.0) + zustand: 4.5.2(@types/react@18.3.12)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/core@11.11.2(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + '@reactflow/core@11.11.2(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@types/d3': 7.4.3 '@types/d3-drag': 3.0.7 @@ -15707,14 +15693,14 @@ snapshots: d3-zoom: 3.0.0 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.2(@types/react@18.2.64)(react@18.2.0) + zustand: 4.5.2(@types/react@18.3.12)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/minimap@11.7.12(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + '@reactflow/minimap@11.7.12(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@reactflow/core': 11.11.2(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@reactflow/core': 11.11.2(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@types/d3-selection': 3.0.10 '@types/d3-zoom': 3.0.8 classcat: 5.0.4 @@ -15722,31 +15708,31 @@ snapshots: d3-zoom: 3.0.0 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.2(@types/react@18.2.64)(react@18.2.0) + zustand: 4.5.2(@types/react@18.3.12)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/node-resizer@2.2.12(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + '@reactflow/node-resizer@2.2.12(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@reactflow/core': 11.11.2(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@reactflow/core': 11.11.2(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) classcat: 5.0.4 d3-drag: 3.0.0 d3-selection: 3.0.0 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.2(@types/react@18.2.64)(react@18.2.0) + zustand: 4.5.2(@types/react@18.3.12)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/node-toolbar@1.3.12(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + '@reactflow/node-toolbar@1.3.12(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@reactflow/core': 11.11.2(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@reactflow/core': 11.11.2(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) classcat: 5.0.4 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.2(@types/react@18.2.64)(react@18.2.0) + zustand: 4.5.2(@types/react@18.3.12)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer @@ -15776,12 +15762,12 @@ snapshots: prop-types: 15.8.1 react: 18.2.0 - '@rjsf/mui@5.17.1(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@mui/icons-material@5.15.13(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@rjsf/core@5.17.1(@rjsf/utils@5.17.1(react@18.2.0))(react@18.2.0))(@rjsf/utils@5.17.1(react@18.2.0))(react@18.2.0)': + '@rjsf/mui@5.17.1(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@mui/icons-material@5.15.13(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@rjsf/core@5.17.1(@rjsf/utils@5.17.1(react@18.2.0))(react@18.2.0))(@rjsf/utils@5.17.1(react@18.2.0))(react@18.2.0)': dependencies: - '@emotion/react': 11.11.4(@types/react@18.2.64)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0) - '@mui/icons-material': 5.15.13(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.2.64)(react@18.2.0) - '@mui/material': 5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@emotion/react': 11.11.4(@types/react@18.3.12)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0) + '@mui/icons-material': 5.15.13(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.3.12)(react@18.2.0) + '@mui/material': 5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@rjsf/core': 5.17.1(@rjsf/utils@5.17.1(react@18.2.0))(react@18.2.0) '@rjsf/utils': 5.17.1(react@18.2.0) react: 18.2.0 @@ -16390,12 +16376,12 @@ snapshots: '@storybook/addon-docs@8.2.4(storybook@8.2.4(@babel/preset-env@7.24.0(@babel/core@7.24.7)))': dependencies: '@babel/core': 7.24.8 - '@mdx-js/react': 3.0.1(@types/react@18.2.64)(react@18.2.0) + '@mdx-js/react': 3.0.1(@types/react@18.3.12)(react@18.2.0) '@storybook/blocks': 8.2.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(storybook@8.2.4(@babel/preset-env@7.24.0(@babel/core@7.24.7))) '@storybook/csf-plugin': 8.2.4(storybook@8.2.4(@babel/preset-env@7.24.0(@babel/core@7.24.7))) '@storybook/global': 5.0.0 '@storybook/react-dom-shim': 8.2.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(storybook@8.2.4(@babel/preset-env@7.24.0(@babel/core@7.24.7))) - '@types/react': 18.2.64 + '@types/react': 18.3.12 fs-extra: 11.2.0 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -16803,18 +16789,7 @@ snapshots: lz-string: 1.5.0 pretty-format: 27.5.1 - '@testing-library/dom@10.3.0': - dependencies: - '@babel/code-frame': 7.24.7 - '@babel/runtime': 7.24.7 - '@types/aria-query': 5.0.4 - aria-query: 5.3.0 - chalk: 4.1.2 - dom-accessibility-api: 0.5.16 - lz-string: 1.5.0 - pretty-format: 27.5.1 - - '@testing-library/dom@10.3.1': + '@testing-library/dom@10.4.0': dependencies: '@babel/code-frame': 7.24.7 '@babel/runtime': 7.24.8 @@ -16852,79 +16827,33 @@ snapshots: jest: 29.7.0(@types/node@20.14.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.10)(typescript@5.5.3)) vitest: 1.6.0(@types/node@20.14.10)(@vitest/ui@1.6.0)(jsdom@21.1.2)(sass@1.77.6)(terser@5.31.2) - '@testing-library/jest-dom@6.4.6(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.14.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.10)(typescript@5.5.3)))(vitest@1.6.0(@types/node@20.14.10)(jsdom@21.1.2)(sass@1.77.6)(terser@5.31.2))': - dependencies: - '@adobe/css-tools': 4.4.0 - '@babel/runtime': 7.24.7 - aria-query: 5.3.0 - chalk: 3.0.0 - css.escape: 1.5.1 - dom-accessibility-api: 0.6.3 - lodash: 4.17.21 - redent: 3.0.0 - optionalDependencies: - '@jest/globals': 29.7.0 - '@types/jest': 29.5.12 - jest: 29.7.0(@types/node@20.14.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.10)(typescript@5.5.3)) - vitest: 1.6.0(@types/node@20.14.10)(@vitest/ui@1.6.0)(jsdom@21.1.2)(sass@1.77.6)(terser@5.31.2) - - '@testing-library/jest-dom@6.4.6(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.14.10))(vitest@1.6.0(@types/node@20.14.10)(@vitest/ui@1.6.0)(jsdom@21.1.2)(sass@1.77.6)(terser@5.31.2))': + '@testing-library/jest-dom@6.6.3': dependencies: '@adobe/css-tools': 4.4.0 - '@babel/runtime': 7.24.7 aria-query: 5.3.0 chalk: 3.0.0 css.escape: 1.5.1 dom-accessibility-api: 0.6.3 lodash: 4.17.21 redent: 3.0.0 - optionalDependencies: - '@jest/globals': 29.7.0 - '@types/jest': 29.5.12 - jest: 29.7.0(@types/node@20.14.10) - vitest: 1.6.0(@types/node@20.14.10)(@vitest/ui@1.6.0)(jsdom@21.1.2)(sass@1.77.6)(terser@5.31.2) - '@testing-library/react@16.0.0(@testing-library/dom@10.3.0)(@types/react-dom@18.2.22)(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + '@testing-library/react@16.0.1(@testing-library/dom@10.4.0)(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@babel/runtime': 7.24.7 - '@testing-library/dom': 10.3.0 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - optionalDependencies: - '@types/react': 18.2.64 - '@types/react-dom': 18.2.22 - - '@testing-library/react@16.0.0(@testing-library/dom@10.3.1)(@types/react-dom@18.0.6)(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': - dependencies: - '@babel/runtime': 7.24.7 - '@testing-library/dom': 10.3.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - optionalDependencies: - '@types/react': 18.2.64 - '@types/react-dom': 18.0.6 - - '@testing-library/react@16.0.0(@testing-library/dom@10.3.1)(@types/react-dom@18.2.22)(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': - dependencies: - '@babel/runtime': 7.24.7 - '@testing-library/dom': 10.3.1 + '@babel/runtime': 7.24.8 + '@testing-library/dom': 10.4.0 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) optionalDependencies: - '@types/react': 18.2.64 - '@types/react-dom': 18.2.22 + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 '@testing-library/user-event@14.5.2(@testing-library/dom@10.1.0)': dependencies: '@testing-library/dom': 10.1.0 - '@testing-library/user-event@14.5.2(@testing-library/dom@10.3.0)': + '@testing-library/user-event@14.5.2(@testing-library/dom@10.4.0)': dependencies: - '@testing-library/dom': 10.3.0 - - '@testing-library/user-event@14.5.2(@testing-library/dom@10.3.1)': - dependencies: - '@testing-library/dom': 10.3.1 + '@testing-library/dom': 10.4.0 '@testing-library/user-event@14.5.2(@testing-library/dom@9.3.4)': dependencies: @@ -17005,7 +16934,7 @@ snapshots: '@types/brainhubeu__react-carousel@1.15.0': dependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 '@types/connect@3.4.38': dependencies: @@ -17203,7 +17132,7 @@ snapshots: '@types/hoist-non-react-statics@3.3.5': dependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 hoist-non-react-statics: 3.3.2 '@types/http-errors@2.0.4': {} @@ -17320,20 +17249,16 @@ snapshots: '@types/react-addons-css-transition-group@15.0.10': dependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 '@types/react-addons-transition-group': 15.0.10 '@types/react-addons-transition-group@15.0.10': dependencies: - '@types/react': 18.2.64 - - '@types/react-dom@18.0.6': - dependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 - '@types/react-dom@18.2.22': + '@types/react-dom@18.3.1': dependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 '@types/react-easy-crop@2.0.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: @@ -17344,27 +17269,27 @@ snapshots: '@types/react-mailchimp-subscribe@2.1.4': dependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 '@types/react-measure@2.0.12': dependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 '@types/react-plotly.js@2.6.3': dependencies: '@types/plotly.js': 2.29.2 - '@types/react': 18.2.64 + '@types/react': 18.3.12 '@types/react-router-dom@5.3.3': dependencies: '@types/history': 4.7.11 - '@types/react': 18.2.64 + '@types/react': 18.3.12 '@types/react-router': 5.1.20 '@types/react-router@5.1.20': dependencies: '@types/history': 4.7.11 - '@types/react': 18.2.64 + '@types/react': 18.3.12 '@types/react-tooltip@4.2.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: @@ -17375,28 +17300,25 @@ snapshots: '@types/react-transition-group@4.4.10': dependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 '@types/react-virtualized-auto-sizer@1.0.4': dependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 '@types/react-window@1.8.8': dependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 - '@types/react@18.2.64': + '@types/react@18.3.12': dependencies: '@types/prop-types': 15.7.11 - '@types/scheduler': 0.16.8 csstype: 3.1.3 '@types/resolve@1.20.2': {} '@types/resolve@1.20.6': {} - '@types/scheduler@0.16.8': {} - '@types/semver@7.5.8': {} '@types/send@0.17.4': @@ -17579,7 +17501,7 @@ snapshots: '@upsetjs/react@1.11.0(react@18.2.0)': dependencies: '@types/lz-string': 1.5.0 - '@types/react': 18.2.64 + '@types/react': 18.3.12 '@upsetjs/model': 1.11.0 lz-string: 1.5.0 react: 18.2.0 @@ -18139,7 +18061,7 @@ snapshots: babel-plugin-macros@3.1.0: dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.24.8 cosmiconfig: 7.1.0 resolve: 1.22.8 @@ -18853,7 +18775,7 @@ snapshots: safe-buffer: 5.2.1 sha.js: 2.4.11 - create-jest@29.7.0(@types/node@20.14.10): + create-jest@29.7.0: dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 @@ -19722,13 +19644,13 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-plugin-jest-dom@5.4.0(@testing-library/dom@10.3.1)(eslint@9.5.0): + eslint-plugin-jest-dom@5.4.0(@testing-library/dom@10.4.0)(eslint@9.5.0): dependencies: '@babel/runtime': 7.24.7 eslint: 9.5.0 requireindex: 1.2.0 optionalDependencies: - '@testing-library/dom': 10.3.1 + '@testing-library/dom': 10.4.0 eslint-plugin-jest@28.6.0(@typescript-eslint/eslint-plugin@7.14.1(@typescript-eslint/parser@7.14.1(eslint@9.5.0)(typescript@5.5.2))(eslint@9.5.0)(typescript@5.5.2))(eslint@9.5.0)(jest@29.7.0)(typescript@5.5.2): dependencies: @@ -20992,27 +20914,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.14.10) - exit: 0.1.2 - import-local: 3.1.0 - jest-config: 29.7.0(@types/node@20.14.10) - jest-util: 29.7.0 - jest-validate: 29.7.0 - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - optional: true - - jest-cli@29.7.0(@types/node@20.14.10): - dependencies: - '@jest/core': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.14.10) + create-jest: 29.7.0 exit: 0.1.2 import-local: 3.1.0 jest-config: 29.7.0(@types/node@20.14.10) @@ -21375,19 +21277,6 @@ snapshots: - ts-node optional: true - jest@29.7.0(@types/node@20.14.10): - dependencies: - '@jest/core': 29.7.0 - '@jest/types': 29.6.3 - import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@20.14.10) - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - optional: true - jest@29.7.0(@types/node@20.14.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.10)(typescript@5.5.3)): dependencies: '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.10)(typescript@5.5.3)) @@ -21402,27 +21291,27 @@ snapshots: jju@1.4.0: {} - jotai-devtools@0.6.3(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(redux@5.0.1): + jotai-devtools@0.6.3(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(redux@5.0.1): dependencies: - '@emotion/react': 11.11.4(@types/react@18.2.64)(react@18.2.0) - '@mantine/core': 6.0.21(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@mantine/hooks@6.0.21(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@emotion/react': 11.11.4(@types/react@18.3.12)(react@18.2.0) + '@mantine/core': 6.0.21(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@mantine/hooks@6.0.21(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@mantine/hooks': 6.0.21(react@18.2.0) - '@mantine/prism': 6.0.21(@mantine/core@6.0.21(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@mantine/hooks@6.0.21(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@mantine/hooks@6.0.21(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@mantine/prism': 6.0.21(@mantine/core@6.0.21(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@mantine/hooks@6.0.21(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@mantine/hooks@6.0.21(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@redux-devtools/extension': 3.3.0(redux@5.0.1) javascript-stringify: 2.1.0 jsondiffpatch: 0.5.0 react: 18.2.0 react-error-boundary: 4.0.13(react@18.2.0) - react-json-tree: 0.18.0(@types/react@18.2.64)(react@18.2.0) + react-json-tree: 0.18.0(@types/react@18.3.12)(react@18.2.0) react-resizable-panels: 0.0.54(react-dom@18.2.0(react@18.2.0))(react@18.2.0) transitivePeerDependencies: - '@types/react' - react-dom - redux - jotai@2.7.0(@types/react@18.2.64)(react@18.2.0): + jotai@2.7.0(@types/react@18.3.12)(react@18.2.0): optionalDependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 react: 18.2.0 joycon@3.1.1: {} @@ -22216,15 +22105,15 @@ snapshots: muggle-string@0.4.1: {} - mui-one-time-password-input@2.0.2(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + mui-one-time-password-input@2.0.2(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: - '@emotion/react': 11.11.4(@types/react@18.2.64)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0) - '@mui/material': 5.15.13(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react@18.2.0))(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@emotion/react': 11.11.4(@types/react@18.3.12)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0) + '@mui/material': 5.15.13(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) optionalDependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 mumath@3.3.4: dependencies: @@ -22552,6 +22441,10 @@ snapshots: dependencies: yocto-queue: 1.0.0 + p-limit@6.1.0: + dependencies: + yocto-queue: 1.1.1 + p-locate@3.0.0: dependencies: p-limit: 2.3.0 @@ -23035,7 +22928,7 @@ snapshots: react-base16-styling@0.9.1: dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.24.8 '@types/base16': 1.0.5 '@types/lodash': 4.17.7 base16: 1.0.0 @@ -23050,7 +22943,7 @@ snapshots: '@restart/hooks': 0.4.16(react@18.2.0) '@types/invariant': 2.2.37 '@types/prop-types': 15.7.11 - '@types/react': 18.2.64 + '@types/react': 18.3.12 '@types/react-transition-group': 4.4.10 '@types/warning': 3.0.3 classnames: 2.5.1 @@ -23161,11 +23054,11 @@ snapshots: react-is@18.2.0: {} - react-json-tree@0.18.0(@types/react@18.2.64)(react@18.2.0): + react-json-tree@0.18.0(@types/react@18.3.12)(react@18.2.0): dependencies: '@babel/runtime': 7.24.7 '@types/lodash': 4.17.7 - '@types/react': 18.2.64 + '@types/react': 18.3.12 react: 18.2.0 react-base16-styling: 0.9.1 @@ -23236,24 +23129,24 @@ snapshots: react-refresh@0.14.2: {} - react-remove-scroll-bar@2.3.5(@types/react@18.2.64)(react@18.2.0): + react-remove-scroll-bar@2.3.5(@types/react@18.3.12)(react@18.2.0): dependencies: react: 18.2.0 - react-style-singleton: 2.2.1(@types/react@18.2.64)(react@18.2.0) + react-style-singleton: 2.2.1(@types/react@18.3.12)(react@18.2.0) tslib: 2.7.0 optionalDependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 - react-remove-scroll@2.5.7(@types/react@18.2.64)(react@18.2.0): + react-remove-scroll@2.5.7(@types/react@18.3.12)(react@18.2.0): dependencies: react: 18.2.0 - react-remove-scroll-bar: 2.3.5(@types/react@18.2.64)(react@18.2.0) - react-style-singleton: 2.2.1(@types/react@18.2.64)(react@18.2.0) + react-remove-scroll-bar: 2.3.5(@types/react@18.3.12)(react@18.2.0) + react-style-singleton: 2.2.1(@types/react@18.3.12)(react@18.2.0) tslib: 2.7.0 - use-callback-ref: 1.3.1(@types/react@18.2.64)(react@18.2.0) - use-sidecar: 1.1.2(@types/react@18.2.64)(react@18.2.0) + use-callback-ref: 1.3.1(@types/react@18.3.12)(react@18.2.0) + use-sidecar: 1.1.2(@types/react@18.3.12)(react@18.2.0) optionalDependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 react-resizable-panels@0.0.54(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: @@ -23286,13 +23179,13 @@ snapshots: react-select-event@5.5.1: dependencies: - '@testing-library/dom': 10.3.0 + '@testing-library/dom': 10.4.0 - react-select@5.8.0(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + react-select@5.8.0(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.24.7 '@emotion/cache': 11.11.0 - '@emotion/react': 11.11.4(@types/react@18.2.64)(react@18.2.0) + '@emotion/react': 11.11.4(@types/react@18.3.12)(react@18.2.0) '@floating-ui/dom': 1.6.3 '@types/react-transition-group': 4.4.10 memoize-one: 6.0.0 @@ -23300,7 +23193,7 @@ snapshots: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) react-transition-group: 4.4.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - use-isomorphic-layout-effect: 1.1.2(@types/react@18.2.64)(react@18.2.0) + use-isomorphic-layout-effect: 1.1.2(@types/react@18.3.12)(react@18.2.0) transitivePeerDependencies: - '@types/react' - supports-color @@ -23323,14 +23216,14 @@ snapshots: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-style-singleton@2.2.1(@types/react@18.2.64)(react@18.2.0): + react-style-singleton@2.2.1(@types/react@18.3.12)(react@18.2.0): dependencies: get-nonce: 1.0.1 invariant: 2.2.4 react: 18.2.0 tslib: 2.7.0 optionalDependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 react-switch@7.0.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: @@ -23345,12 +23238,12 @@ snapshots: react-shallow-renderer: 16.15.0(react@18.2.0) scheduler: 0.23.0 - react-textarea-autosize@8.3.4(@types/react@18.2.64)(react@18.2.0): + react-textarea-autosize@8.3.4(@types/react@18.3.12)(react@18.2.0): dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.24.8 react: 18.2.0 use-composed-ref: 1.3.0(react@18.2.0) - use-latest: 1.2.1(@types/react@18.2.64)(react@18.2.0) + use-latest: 1.2.1(@types/react@18.3.12)(react@18.2.0) transitivePeerDependencies: - '@types/react' @@ -23400,14 +23293,14 @@ snapshots: dependencies: loose-envify: 1.4.0 - reactflow@11.11.2(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + reactflow@11.11.2(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: - '@reactflow/background': 11.3.12(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - '@reactflow/controls': 11.2.12(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - '@reactflow/core': 11.11.2(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - '@reactflow/minimap': 11.7.12(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - '@reactflow/node-resizer': 2.2.12(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - '@reactflow/node-toolbar': 1.3.12(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@reactflow/background': 11.3.12(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@reactflow/controls': 11.2.12(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@reactflow/core': 11.11.2(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@reactflow/minimap': 11.7.12(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@reactflow/node-resizer': 2.2.12(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@reactflow/node-toolbar': 1.3.12(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) transitivePeerDependencies: @@ -23480,7 +23373,7 @@ snapshots: regenerator-transform@0.15.2: dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.24.8 regexp.prototype.flags@1.5.2: dependencies: @@ -24626,7 +24519,7 @@ snapshots: uncontrollable@7.2.1(react@18.2.0): dependencies: '@babel/runtime': 7.24.7 - '@types/react': 18.2.64 + '@types/react': 18.3.12 invariant: 2.2.4 react: 18.2.0 react-lifecycles-compat: 3.0.4 @@ -24725,12 +24618,12 @@ snapshots: punycode: 1.4.1 qs: 6.12.0 - use-callback-ref@1.3.1(@types/react@18.2.64)(react@18.2.0): + use-callback-ref@1.3.1(@types/react@18.3.12)(react@18.2.0): dependencies: react: 18.2.0 tslib: 2.7.0 optionalDependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 use-composed-ref@1.3.0(react@18.2.0): dependencies: @@ -24742,26 +24635,26 @@ snapshots: dequal: 2.0.3 react: 18.2.0 - use-isomorphic-layout-effect@1.1.2(@types/react@18.2.64)(react@18.2.0): + use-isomorphic-layout-effect@1.1.2(@types/react@18.3.12)(react@18.2.0): dependencies: react: 18.2.0 optionalDependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 - use-latest@1.2.1(@types/react@18.2.64)(react@18.2.0): + use-latest@1.2.1(@types/react@18.3.12)(react@18.2.0): dependencies: react: 18.2.0 - use-isomorphic-layout-effect: 1.1.2(@types/react@18.2.64)(react@18.2.0) + use-isomorphic-layout-effect: 1.1.2(@types/react@18.3.12)(react@18.2.0) optionalDependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 - use-sidecar@1.1.2(@types/react@18.2.64)(react@18.2.0): + use-sidecar@1.1.2(@types/react@18.3.12)(react@18.2.0): dependencies: detect-node-es: 1.1.0 react: 18.2.0 tslib: 2.7.0 optionalDependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 use-sync-external-store@1.2.0(react@18.2.0): dependencies: @@ -25246,11 +25139,13 @@ snapshots: yocto-queue@1.0.0: {} + yocto-queue@1.1.1: {} + zod@3.22.4: {} - zustand@4.5.2(@types/react@18.2.64)(react@18.2.0): + zustand@4.5.2(@types/react@18.3.12)(react@18.2.0): dependencies: use-sync-external-store: 1.2.0(react@18.2.0) optionalDependencies: - '@types/react': 18.2.64 + '@types/react': 18.3.12 react: 18.2.0 From fc76b745367d4cbbff0b22b31772715caf443bff Mon Sep 17 00:00:00 2001 From: Nick Grosenbacher Date: Fri, 8 Nov 2024 13:22:19 -0500 Subject: [PATCH 2/7] `processFilePart`: Pass all arguments through on recursive retry --- .../synapse-react-client/src/synapse-client/SynapseClient.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/synapse-react-client/src/synapse-client/SynapseClient.ts b/packages/synapse-react-client/src/synapse-client/SynapseClient.ts index 1e7d5dff91..b10a6fa75c 100644 --- a/packages/synapse-react-client/src/synapse-client/SynapseClient.ts +++ b/packages/synapse-react-client/src/synapse-client/SynapseClient.ts @@ -2245,6 +2245,8 @@ const processFilePart = async ( fileUploadResolve, fileUploadReject, updateProgress, + getIsCancelled, + abortController, ) } } From 6961324ea07ba8c9e97dcaf41eab5990c3a02149 Mon Sep 17 00:00:00 2001 From: Nick Grosenbacher Date: Mon, 11 Nov 2024 09:26:51 -0500 Subject: [PATCH 3/7] extract type for individual file upload state --- .../useUploadFileEntities.ts | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/synapse-react-client/src/utils/hooks/useUploadFileEntity/useUploadFileEntities.ts b/packages/synapse-react-client/src/utils/hooks/useUploadFileEntity/useUploadFileEntities.ts index bdadd9910f..f8b8be685a 100644 --- a/packages/synapse-react-client/src/utils/hooks/useUploadFileEntity/useUploadFileEntities.ts +++ b/packages/synapse-react-client/src/utils/hooks/useUploadFileEntity/useUploadFileEntities.ts @@ -52,22 +52,24 @@ type UploaderState = | 'COMPLETE' | 'ERROR' +type FileUploadProgress = { + file: File + progress: ProgressCallback + status: UploadFileStatus + cancel: () => void + pause: () => void + resume: () => void + remove: () => void + failureReason?: string +} + type UseUploadFileEntitiesReturn = { state: UploaderState isPrecheckingUpload: boolean activePrompts: Prompt[] initiateUpload: (files: File[]) => void activeUploadCount: number - uploadProgress: { - file: File - progress: ProgressCallback - status: UploadFileStatus - cancel: () => void - pause: () => void - resume: () => void - remove: () => void - failureReason?: string - }[] + uploadProgress: FileUploadProgress[] } // Limit the number of concurrent uploads to avoid overwhelming the browser From 50260468a0bd4b4d31dc2cd28a8e14e8ff37d6e7 Mon Sep 17 00:00:00 2001 From: Nick Grosenbacher Date: Mon, 11 Nov 2024 09:45:26 -0500 Subject: [PATCH 4/7] break up useMemo --- .../useUploadFileEntities.ts | 187 ++++++++++-------- 1 file changed, 105 insertions(+), 82 deletions(-) diff --git a/packages/synapse-react-client/src/utils/hooks/useUploadFileEntity/useUploadFileEntities.ts b/packages/synapse-react-client/src/utils/hooks/useUploadFileEntity/useUploadFileEntities.ts index f8b8be685a..fca65ae5ed 100644 --- a/packages/synapse-react-client/src/utils/hooks/useUploadFileEntity/useUploadFileEntities.ts +++ b/packages/synapse-react-client/src/utils/hooks/useUploadFileEntity/useUploadFileEntities.ts @@ -65,6 +65,7 @@ type FileUploadProgress = { type UseUploadFileEntitiesReturn = { state: UploaderState + errorMessage?: string isPrecheckingUpload: boolean activePrompts: Prompt[] initiateUpload: (files: File[]) => void @@ -86,11 +87,16 @@ export function useUploadFileEntities( ): UseUploadFileEntitiesReturn { const { synapseClient } = useSynapseContext() - const { data: uploadDestination, isLoading: isLoadingUploadDestination } = - useGetDefaultUploadDestination(parentId) + const { + data: uploadDestination, + isLoading: isLoadingUploadDestination, + error: getUploadDestinationError, + } = useGetDefaultUploadDestination(parentId) const storageLocationId = uploadDestination?.storageLocationId || SYNAPSE_STORAGE_LOCATION_ID + const errorMessage = getUploadDestinationError?.message + const { pendingItems: filesToConfirmNewVersion, confirmItem: confirmUploadFileWithNewVersion, @@ -119,6 +125,9 @@ export function useUploadFileEntities( } = useTrackFileUploads() const state: UploaderState = useMemo(() => { + if (getUploadDestinationError) { + return 'ERROR' + } if (isLoadingUploadDestination) { return 'LOADING' } @@ -134,6 +143,7 @@ export function useUploadFileEntities( return 'WAITING' }, [ filesToConfirmNewVersion.length, + getUploadDestinationError, isLoadingUploadDestination, isUploadComplete, isUploading, @@ -238,6 +248,9 @@ export function useUploadFileEntities( ], ) + /** + * Upload the list of prepared files to Synapse. + */ const startUpload = useCallback( (...preparedFiles: FilePreparedForUpload[]) => { clearPendingFiles() @@ -255,18 +268,19 @@ export function useUploadFileEntities( [clearPendingFiles, trackNewFiles, uploadPreparedFile], ) + /** + * After all files have been prepared for upload, process them to determine if the upload can automatically continue. + * If all files can be uploaded without user intervention, start the upload. Otherwise, prompt the user to make required + * decisions to continue the upload. + */ const postPrepareUpload = useCallback( ( preparedFiles: PrepareDirsForUploadReturn, - /** If true, skip checking upload limits and prompting the user before creating a new version. + /** If true, skip checks such as prompting the user before creating a new version. * This is useful when resuming an upload that has already been pre-checked. */ skipChecks = false, ) => { const { newFileEntities, updatedFileEntities } = preparedFiles - if (newFileEntities.length == 0 && updatedFileEntities.length == 0) { - // Is this possible? - throw new Error('No files were provided to upload.') - } if (skipChecks) { startUpload(...newFileEntities, ...updatedFileEntities) @@ -307,94 +321,103 @@ export function useUploadFileEntities( [parentId, postPrepareUpload, prepareUpload], ) - return useMemo(() => { - let activePrompts: Prompt[] = [] - if (filesToConfirmNewVersion.length > 0) { - activePrompts = filesToConfirmNewVersion.map(fileToPrompt => { - return { - title: 'Update existing file?', - message: `A file named "${ - fileToPrompt.file.name - }" (${fileToPrompt.existingEntityId!}) already exists in this location. Do you want to update the existing file and create a new version?`, - onConfirm: () => { - const { confirmedItems, pendingItems } = - confirmUploadFileWithNewVersion(fileToPrompt) - - if (confirmedItems.length > 0 && pendingItems.length == 0) { - void startUpload(...confirmedItems) - } - }, - onConfirmAll: () => { - const { confirmedItems } = confirmUploadFileWithNewVersion( - ...filesToConfirmNewVersion, - ) + const activePrompts = useMemo(() => { + return filesToConfirmNewVersion.map(fileToPrompt => { + return { + title: 'Update existing file?', + message: `A file named "${ + fileToPrompt.file.name + }" (${fileToPrompt.existingEntityId!}) already exists in this location. Do you want to update the existing file and create a new version?`, + onConfirm: () => { + const { confirmedItems, pendingItems } = + confirmUploadFileWithNewVersion(fileToPrompt) + if (confirmedItems.length > 0 && pendingItems.length == 0) { void startUpload(...confirmedItems) - }, - onSkip: () => { - const { confirmedItems, pendingItems } = - skipFileRequiringNewVersion(fileToPrompt) + } + }, + onConfirmAll: () => { + const { confirmedItems } = confirmUploadFileWithNewVersion( + ...filesToConfirmNewVersion, + ) - if (confirmedItems.length > 0 && pendingItems.length == 0) { - void startUpload(...confirmedItems) - } - }, - onCancelAll: () => { - clearPendingFiles() - }, - } - }) - } + void startUpload(...confirmedItems) + }, + onSkip: () => { + const { confirmedItems, pendingItems } = + skipFileRequiringNewVersion(fileToPrompt) - return { - state, - isPrecheckingUpload, - activeUploadCount, - initiateUpload: initiateUpload, - activePrompts: activePrompts, - uploadProgress: [...trackedUploadProgress].map(([file, progress]) => { - return { - file: file, - progress: progress.progress, - status: progress.status, - failureReason: progress.failureReason, - cancel: () => { - cancelUpload(file) - }, - pause: () => { - pauseUpload(file) - }, - resume: () => { - prepareUpload( - { files: [file], parentId: progress.parentId }, - { - onSuccess: result => { - postPrepareUpload(result, true) - }, - }, - ) - }, - remove() { - removeUpload(file) - }, - } - }), - } + if (confirmedItems.length > 0 && pendingItems.length == 0) { + void startUpload(...confirmedItems) + } + }, + onCancelAll: () => { + clearPendingFiles() + }, + } + }) }, [ - activeUploadCount, - cancelUpload, clearPendingFiles, confirmUploadFileWithNewVersion, filesToConfirmNewVersion, - initiateUpload, + skipFileRequiringNewVersion, + startUpload, + ]) + + const uploadProgress: FileUploadProgress[] = useMemo(() => { + return [...trackedUploadProgress].map(([file, progress]) => { + return { + file: file, + progress: progress.progress, + status: progress.status, + failureReason: progress.failureReason, + cancel: () => { + cancelUpload(file) + }, + pause: () => { + pauseUpload(file) + }, + resume: () => { + prepareUpload( + { files: [file], parentId: progress.parentId }, + { + onSuccess: result => { + postPrepareUpload(result, true) + }, + }, + ) + }, + remove() { + removeUpload(file) + }, + } + }) + }, [ + cancelUpload, pauseUpload, postPrepareUpload, prepareUpload, removeUpload, - skipFileRequiringNewVersion, - startUpload, - state, trackedUploadProgress, + ]) + + return useMemo(() => { + return { + state, + errorMessage, + isPrecheckingUpload, + activeUploadCount, + initiateUpload: initiateUpload, + activePrompts: activePrompts, + uploadProgress: uploadProgress, + } + }, [ + state, + errorMessage, isPrecheckingUpload, + activeUploadCount, + initiateUpload, + activePrompts, + uploadProgress, ]) } From 4312dee25a5f9f19b96078124774c28db9346c9c Mon Sep 17 00:00:00 2001 From: Nick Grosenbacher Date: Mon, 11 Nov 2024 11:19:50 -0500 Subject: [PATCH 5/7] Update packages/synapse-react-client/src/utils/hooks/useUploadFileEntity/useUploadFileEntities.ts --- .../src/utils/hooks/useUploadFileEntity/useUploadFileEntities.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/synapse-react-client/src/utils/hooks/useUploadFileEntity/useUploadFileEntities.ts b/packages/synapse-react-client/src/utils/hooks/useUploadFileEntity/useUploadFileEntities.ts index fca65ae5ed..da2f024c28 100644 --- a/packages/synapse-react-client/src/utils/hooks/useUploadFileEntity/useUploadFileEntities.ts +++ b/packages/synapse-react-client/src/utils/hooks/useUploadFileEntity/useUploadFileEntities.ts @@ -382,6 +382,7 @@ export function useUploadFileEntities( { files: [file], parentId: progress.parentId }, { onSuccess: result => { + // the file was already checked before it was paused, so we can skipChecks postPrepareUpload(result, true) }, }, From 98a0e2756366c3934639edc5e14363ccdb7005df Mon Sep 17 00:00:00 2001 From: Nick Grosenbacher Date: Mon, 11 Nov 2024 14:39:08 -0500 Subject: [PATCH 6/7] fix reference to wrong AbortController --- .../synapse-react-client/src/synapse-client/SynapseClient.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/synapse-react-client/src/synapse-client/SynapseClient.ts b/packages/synapse-react-client/src/synapse-client/SynapseClient.ts index b10a6fa75c..2140b2bc5d 100644 --- a/packages/synapse-react-client/src/synapse-client/SynapseClient.ts +++ b/packages/synapse-react-client/src/synapse-client/SynapseClient.ts @@ -2291,8 +2291,8 @@ const uploadFilePart = async ( getIsCancelled?: () => boolean, abortController?: AbortController, ) => { - const controller = new AbortController() - const signal = abortController?.signal || controller.signal + const controller = abortController || new AbortController() + const signal = controller.signal const checkIsCancelled = () => { if (getIsCancelled) { From 1f072f9aa091ed4cf9580143f036900d89055b09 Mon Sep 17 00:00:00 2001 From: Nick Grosenbacher Date: Mon, 11 Nov 2024 15:30:46 -0500 Subject: [PATCH 7/7] Add suggested comment --- .../hooks/useUploadFileEntity/useUploadFileEntities.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/synapse-react-client/src/utils/hooks/useUploadFileEntity/useUploadFileEntities.ts b/packages/synapse-react-client/src/utils/hooks/useUploadFileEntity/useUploadFileEntities.ts index da2f024c28..fea02d8379 100644 --- a/packages/synapse-react-client/src/utils/hooks/useUploadFileEntity/useUploadFileEntities.ts +++ b/packages/synapse-react-client/src/utils/hooks/useUploadFileEntity/useUploadFileEntities.ts @@ -85,6 +85,16 @@ export function useUploadFileEntities( /** Optional secretKey for a direct S3 upload */ secretKey = '', ): UseUploadFileEntitiesReturn { + /** + * General flow for how the functions in this hook are used is + * + * 1. `initiateUpload(files)` - called by the caller of the hook + * 2. `prepareUpload(files)` - sets up the folder paths, checks for existing entities with the same name + * 3. `postPrepareUpload(files)` - may prompt the user to confirm uploading new versions of files, based on the results of `prepareUpload` + * 4. `startUpload(files)` - starts tracking file uploads, for each file, calls `uploadPreparedFile` + * 5. `uploadPreparedFile(file)` - uploads the file and creates/updates the entity + */ + const { synapseClient } = useSynapseContext() const {