synapse-react-client 4.0.9 → 4.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (543) hide show
  1. package/dist/SWC.index.d.ts +1 -0
  2. package/dist/SWC.index.d.ts.map +1 -1
  3. package/dist/SWC.index.js +2 -1
  4. package/dist/SWC.index.js.map +1 -1
  5. package/dist/aridhia-queries/aridhiaTokenExchange.js.map +1 -1
  6. package/dist/aridhia-queries/useGetAridhiaRequests.js.map +1 -1
  7. package/dist/assets/icons/TasksIcon.d.ts.map +1 -1
  8. package/dist/assets/icons/TasksIcon.js +6 -10
  9. package/dist/assets/icons/TasksIcon.js.map +1 -1
  10. package/dist/components/AccessRequirementAclEditor/AccessRequirementAclEditor.d.ts.map +1 -1
  11. package/dist/components/AccessRequirementAclEditor/AccessRequirementAclEditor.js +69 -63
  12. package/dist/components/AccessRequirementAclEditor/AccessRequirementAclEditor.js.map +1 -1
  13. package/dist/components/AccessRequirementList/AccessApprovalCheckMark.js.map +1 -1
  14. package/dist/components/AccessRequirementList/AccessRequirementList.js.map +1 -1
  15. package/dist/components/AccessRequirementList/AccessRequirementListUtils.js.map +1 -1
  16. package/dist/components/AccessRequirementList/ManagedACTAccessRequirementRequestFlow/DataAccessRequestAccessorsEditor.js.map +1 -1
  17. package/dist/components/AccessRequirementList/RequirementItem/SelfSignAccessRequirementItem.js.map +1 -1
  18. package/dist/components/AccessRequirementRelatedProjectsList/AccessRequirementRelatedProjectsList.js.map +1 -1
  19. package/dist/components/AccessTokenPage/AccessTokenCard/AccessTokenCard.js.map +1 -1
  20. package/dist/components/AcknowledgementsPage/StudyAcknowledgements.js.map +1 -1
  21. package/dist/components/AclEditor/PermissionLevelMenu.js.map +1 -1
  22. package/dist/components/AclEditor/ResourceAccessAndUserGroupHeader.js.map +1 -1
  23. package/dist/components/AclEditor/useSortResourceAccessList.js.map +1 -1
  24. package/dist/components/AclEditor/useUpdateAcl.js.map +1 -1
  25. package/dist/components/Aridhia/AridhiaAccessStatus.js.map +1 -1
  26. package/dist/components/Authentication/AuthenticationMethodSelection.d.ts.map +1 -1
  27. package/dist/components/Authentication/AuthenticationMethodSelection.js +38 -37
  28. package/dist/components/Authentication/AuthenticationMethodSelection.js.map +1 -1
  29. package/dist/components/Authentication/Constants.d.ts +1 -0
  30. package/dist/components/Authentication/Constants.d.ts.map +1 -1
  31. package/dist/components/Authentication/Constants.js +2 -2
  32. package/dist/components/Authentication/Constants.js.map +1 -1
  33. package/dist/components/Authentication/LastLoginInfo.js.map +1 -1
  34. package/dist/components/Authentication/RecoveryCodeForm.js.map +1 -1
  35. package/dist/components/Authentication/RecoveryCodeGrid.js.map +1 -1
  36. package/dist/components/Authentication/RegenerateBackupCodesWarning.js.map +1 -1
  37. package/dist/components/Authentication/Reset2FAWarning.js.map +1 -1
  38. package/dist/components/Authentication/StandaloneLoginForm.js +1 -1
  39. package/dist/components/Authentication/TwoFactorBackupCodes.js.map +1 -1
  40. package/dist/components/Authentication/TwoFactorEnrollmentForm.d.ts.map +1 -1
  41. package/dist/components/Authentication/TwoFactorEnrollmentForm.js +2 -1
  42. package/dist/components/Authentication/TwoFactorEnrollmentForm.js.map +1 -1
  43. package/dist/components/BasePortalCard/ColorfulPortalCardWithChips/ColorfulPortalCardWithChips.js.map +1 -1
  44. package/dist/components/CardContainer/CardContainer.js.map +1 -1
  45. package/dist/components/CardDeck/CardDeck.Mobile.js.map +1 -1
  46. package/dist/components/CardDeck/TableQueryCardDeck.js.map +1 -1
  47. package/dist/components/CertificationQuiz/CertificationQuiz.js.map +1 -1
  48. package/dist/components/ChallengeDataDownload/ChallengeDataDownload.js.map +1 -1
  49. package/dist/components/ChallengeSubmission/ChallengeSubmission.js.map +1 -1
  50. package/dist/components/ChallengeSubmission/ChallengeSubmissionStepper.js.map +1 -1
  51. package/dist/components/ChallengeSubmission/EvaluationQueueCurrentRoundInfo.js.map +1 -1
  52. package/dist/components/ChallengeSubmission/EvaluationQueueList.js.map +1 -1
  53. package/dist/components/ChallengeSubmission/SubmissionDirectoryList.js.map +1 -1
  54. package/dist/components/ChallengeTeamWizard/ChallengeTeamWizard.js.map +1 -1
  55. package/dist/components/ChallengeTeamWizard/CreateChallengeTeam.js.map +1 -1
  56. package/dist/components/ChangePassword/ChangePassword.js.map +1 -1
  57. package/dist/components/ChangePassword/ChangePasswordWithToken.js.map +1 -1
  58. package/dist/components/ChangePassword/useChangePasswordFormState.js +1 -1
  59. package/dist/components/ChangePassword/useChangePasswordFormState.js.map +1 -1
  60. package/dist/components/CitationPopover/CitationPopoverContent.js.map +1 -1
  61. package/dist/components/ColumnFilter/ColumnFilter.js.map +1 -1
  62. package/dist/components/ComponentCollapse.js.map +1 -1
  63. package/dist/components/CookiesNotification/CookiesNotification.js.map +1 -1
  64. package/dist/components/CreateProjectModal/CreateProjectModal.js.map +1 -1
  65. package/dist/components/CreateTableViewWizard/CreateTableViewWizardUtils.js.map +1 -1
  66. package/dist/components/DataGrid/DataGrid.d.ts +0 -1
  67. package/dist/components/DataGrid/DataGrid.d.ts.map +1 -1
  68. package/dist/components/DataGrid/DataGrid.js +72 -72
  69. package/dist/components/DataGrid/DataGrid.js.map +1 -1
  70. package/dist/components/DataGrid/DataGridWebSocket.d.ts +4 -0
  71. package/dist/components/DataGrid/DataGridWebSocket.d.ts.map +1 -1
  72. package/dist/components/DataGrid/DataGridWebSocket.js +9 -8
  73. package/dist/components/DataGrid/DataGridWebSocket.js.map +1 -1
  74. package/dist/components/DataGrid/SynapseGrid.d.ts.map +1 -1
  75. package/dist/components/DataGrid/SynapseGrid.js +326 -268
  76. package/dist/components/DataGrid/SynapseGrid.js.map +1 -1
  77. package/dist/components/DataGrid/columns/AutocompleteColumn.d.ts +2 -0
  78. package/dist/components/DataGrid/columns/AutocompleteColumn.d.ts.map +1 -1
  79. package/dist/components/DataGrid/columns/AutocompleteColumn.js +113 -67
  80. package/dist/components/DataGrid/columns/AutocompleteColumn.js.map +1 -1
  81. package/dist/components/DataGrid/columns/AutocompleteMultipleEnumColumn.d.ts +2 -1
  82. package/dist/components/DataGrid/columns/AutocompleteMultipleEnumColumn.d.ts.map +1 -1
  83. package/dist/components/DataGrid/columns/AutocompleteMultipleEnumColumn.js +126 -122
  84. package/dist/components/DataGrid/columns/AutocompleteMultipleEnumColumn.js.map +1 -1
  85. package/dist/components/DataGrid/columns/useGridAutocompleteState.d.ts +58 -0
  86. package/dist/components/DataGrid/columns/useGridAutocompleteState.d.ts.map +1 -0
  87. package/dist/components/DataGrid/columns/useGridAutocompleteState.js +52 -0
  88. package/dist/components/DataGrid/columns/useGridAutocompleteState.js.map +1 -0
  89. package/dist/components/DataGrid/components/ValidationAlert.d.ts +5 -2
  90. package/dist/components/DataGrid/components/ValidationAlert.d.ts.map +1 -1
  91. package/dist/components/DataGrid/components/ValidationAlert.js +429 -24
  92. package/dist/components/DataGrid/components/ValidationAlert.js.map +1 -1
  93. package/dist/components/DataGrid/hooks/useColumnResizeHandles.js.map +1 -1
  94. package/dist/components/DataGrid/hooks/useGetSchemaForGrid.js.map +1 -1
  95. package/dist/components/DataGrid/hooks/useGridUndoRedo.js.map +1 -1
  96. package/dist/components/DataGrid/hooks/useStack.js.map +1 -1
  97. package/dist/components/DataGrid/useCRDTModelView.js.map +1 -1
  98. package/dist/components/DataGrid/useDataGridWebsocket.d.ts +7 -0
  99. package/dist/components/DataGrid/useDataGridWebsocket.d.ts.map +1 -1
  100. package/dist/components/DataGrid/useDataGridWebsocket.js +16 -2
  101. package/dist/components/DataGrid/useDataGridWebsocket.js.map +1 -1
  102. package/dist/components/DataGrid/useInitializeGridConnection.js.map +1 -1
  103. package/dist/components/DataGrid/useMergeGridWithRecordSet.js.map +1 -1
  104. package/dist/components/DataGrid/useMergeGridWithSource.js.map +1 -1
  105. package/dist/components/DataGrid/useMergeGridWithTable.js.map +1 -1
  106. package/dist/components/DataGrid/utils/DataGridUtils.js.map +1 -1
  107. package/dist/components/DataGrid/utils/applyModelChange.js.map +1 -1
  108. package/dist/components/DataGrid/utils/columnFactory.js.map +1 -1
  109. package/dist/components/DataGrid/utils/computeReplicaSelectionModel.js.map +1 -1
  110. package/dist/components/DataGrid/utils/extractColumnValidationMessages.js.map +1 -1
  111. package/dist/components/DataGrid/utils/getCellClassName.d.ts.map +1 -1
  112. package/dist/components/DataGrid/utils/getCellClassName.js +8 -8
  113. package/dist/components/DataGrid/utils/getCellClassName.js.map +1 -1
  114. package/dist/components/DataGrid/utils/json-rx/JsonRx.js.map +1 -1
  115. package/dist/components/DataGrid/utils/modelRowsToGrid.js.map +1 -1
  116. package/dist/components/DataGrid/utils/parseFreeTextUsingJsonSchemaType.js.map +1 -1
  117. package/dist/components/DataGrid/utils/splitPatch.js.map +1 -1
  118. package/dist/components/DateTimePicker/DateTimePicker.js.map +1 -1
  119. package/dist/components/DirectDownload/DirectDownload.js.map +1 -1
  120. package/dist/components/DirectDownloadButton.js.map +1 -1
  121. package/dist/components/DownloadCart/CreatePackageV2.js.map +1 -1
  122. package/dist/components/DownloadCart/DownloadIneligibleForPackagingFilesFromListButton.js.map +1 -1
  123. package/dist/components/DownloadCart/DownloadListActionsRequired.js.map +1 -1
  124. package/dist/components/DownloadCart/DownloadListTable.js.map +1 -1
  125. package/dist/components/DownloadCart/fileNameUtils.js.map +1 -1
  126. package/dist/components/DraggableDialog/DraggableDialog.js.map +1 -1
  127. package/dist/components/DynamicForm/DynamicFormModal.js.map +1 -1
  128. package/dist/components/Ecosystem/TableQueryEcosystem.js.map +1 -1
  129. package/dist/components/EntityAclEditor/EntityAclEditor.d.ts.map +1 -1
  130. package/dist/components/EntityAclEditor/EntityAclEditor.js +103 -103
  131. package/dist/components/EntityAclEditor/EntityAclEditor.js.map +1 -1
  132. package/dist/components/EntityAclEditor/useNotifyNewACLUsers.js.map +1 -1
  133. package/dist/components/EntityBadgeIcons/EntityBadgeIcons.js.map +1 -1
  134. package/dist/components/EntityCitation/EntityCitation.js.map +1 -1
  135. package/dist/components/EntityDownloadButton/EntityDownloadButton.js.map +1 -1
  136. package/dist/components/EntityDownloadConfirmation/EntityDownloadConfirmation.d.ts.map +1 -1
  137. package/dist/components/EntityDownloadConfirmation/EntityDownloadConfirmation.js +36 -30
  138. package/dist/components/EntityDownloadConfirmation/EntityDownloadConfirmation.js.map +1 -1
  139. package/dist/components/EntityFinder/EntityFinder.js.map +1 -1
  140. package/dist/components/EntityFinder/VersionSelectionType.js.map +1 -1
  141. package/dist/components/EntityFinder/details/configurations/EntityChildrenDetails.js.map +1 -1
  142. package/dist/components/EntityFinder/details/configurations/FavoritesDetails.js.map +1 -1
  143. package/dist/components/EntityFinder/details/configurations/ProjectListDetails.js.map +1 -1
  144. package/dist/components/EntityFinder/details/view/DetailsView.js.map +1 -1
  145. package/dist/components/EntityFinder/tree/EntityTree.js.map +1 -1
  146. package/dist/components/EntityFinder/tree/VirtualizedTree.js.map +1 -1
  147. package/dist/components/EntityFinder/useEntitySelection.js.map +1 -1
  148. package/dist/components/EntityForm/EntityForm.js.map +1 -1
  149. package/dist/components/EntityHeaderTable/EntityHeaderTable.js.map +1 -1
  150. package/dist/components/EntityHeaderTable/Filter.js.map +1 -1
  151. package/dist/components/EntityHeaderTable/useEntityHeaderTableState.js.map +1 -1
  152. package/dist/components/EntitySubjectsSelector/EntitySubjectsSelector.js.map +1 -1
  153. package/dist/components/EntityTreeTable/components/IdColumnHeader.js.map +1 -1
  154. package/dist/components/EntityTreeTable/hooks/useEntityTreeState.js.map +1 -1
  155. package/dist/components/EntityTreeTable/hooks/useTableColumns.js.map +1 -1
  156. package/dist/components/EntityTreeTable/hooks/useTableData.js.map +1 -1
  157. package/dist/components/EntityTreeTable/hooks/useTreeOperationsWithDirectFetch.js.map +1 -1
  158. package/dist/components/EntityUpload/EntityUpload.js.map +1 -1
  159. package/dist/components/ExperimentalMode/ExperimentalMode.js.map +1 -1
  160. package/dist/components/ExternalFileHandleLink/ExternalFileHandleLink.js.map +1 -1
  161. package/dist/components/FeaturedDataTabs/FacetPlotsCard.js.map +1 -1
  162. package/dist/components/FeaturedDataTabs/QueryPerFacetPlotsCard.js.map +1 -1
  163. package/dist/components/FeaturedDataTabs/SingleQueryFacetPlotsCards.js.map +1 -1
  164. package/dist/components/FeaturedResearch/FeaturedResearch.js.map +1 -1
  165. package/dist/components/FeaturedToolsList/FeaturedToolsList.js.map +1 -1
  166. package/dist/components/FilePreview/FileHandleContentRenderer.js.map +1 -1
  167. package/dist/components/FilePreview/HtmlPreview/HtmlPreview.js.map +1 -1
  168. package/dist/components/FilePreview/PreviewRendererType.js.map +1 -1
  169. package/dist/components/Forum/DiscussionReply.js.map +1 -1
  170. package/dist/components/Forum/DiscussionSearchResult.js.map +1 -1
  171. package/dist/components/Forum/ForumTable.js.map +1 -1
  172. package/dist/components/Forum/ForumThreadEditor.js.map +1 -1
  173. package/dist/components/FullTextSearch/FullTextSearchUtils.js.map +1 -1
  174. package/dist/components/GenericCard/GenericCard.d.ts.map +1 -1
  175. package/dist/components/GenericCard/GenericCard.js +12 -7
  176. package/dist/components/GenericCard/GenericCard.js.map +1 -1
  177. package/dist/components/GenericCard/Linkify.js.map +1 -1
  178. package/dist/components/GenericCard/SynapseCardLabel.js.map +1 -1
  179. package/dist/components/GenericCard/TableRowGenericCard.js +105 -105
  180. package/dist/components/GenericCard/TableRowGenericCard.js.map +1 -1
  181. package/dist/components/Goals/Goals.Mobile.js.map +1 -1
  182. package/dist/components/Goals/Goals.js.map +1 -1
  183. package/dist/components/GoalsV2/GoalsV2.Mobile.js.map +1 -1
  184. package/dist/components/GoalsV2/GoalsV2.js.map +1 -1
  185. package/dist/components/GoalsV3/GoalsV3.Mobile.js.map +1 -1
  186. package/dist/components/GoalsV3/GoalsV3.js.map +1 -1
  187. package/dist/components/GoogleMap/SynapseUserMarker.js.map +1 -1
  188. package/dist/components/HasAccess/AccessIcon.js.map +1 -1
  189. package/dist/components/HasAccess/useHasAccess.js.map +1 -1
  190. package/dist/components/HeaderCard/HeaderCardV2.js.map +1 -1
  191. package/dist/components/HeaderCard.d.ts +6 -1
  192. package/dist/components/HeaderCard.d.ts.map +1 -1
  193. package/dist/components/HeaderCard.js +107 -76
  194. package/dist/components/HeaderCard.js.map +1 -1
  195. package/dist/components/HexGrid/HexGrid.js.map +1 -1
  196. package/dist/components/IconList.js.map +1 -1
  197. package/dist/components/ImageCardGridWithLinks/ImageCardGridWithLinks.js.map +1 -1
  198. package/dist/components/ImageFromSynapseTable.js.map +1 -1
  199. package/dist/components/JSONArrayEditor/useParseCsv.js.map +1 -1
  200. package/dist/components/JsonSchemaForm/templates/ArrayFieldDescriptionTemplate.js.map +1 -1
  201. package/dist/components/JsonSchemaForm/templates/ArrayFieldItemTemplate.js.map +1 -1
  202. package/dist/components/JsonSchemaForm/templates/BaseInputTemplate.js.map +1 -1
  203. package/dist/components/JsonSchemaForm/templates/FieldTemplate.js.map +1 -1
  204. package/dist/components/JsonSchemaForm/templates/RJSFInputLabel.js.map +1 -1
  205. package/dist/components/Markdown/MarkdownGithub.js.map +1 -1
  206. package/dist/components/Markdown/MarkdownSynapse.js.map +1 -1
  207. package/dist/components/Markdown/MarkdownUtils.js.map +1 -1
  208. package/dist/components/Markdown/SynapseWikiContext.js.map +1 -1
  209. package/dist/components/Markdown/UserMentionModal.js.map +1 -1
  210. package/dist/components/Markdown/widget/MarkdownProvenanceGraph.js.map +1 -1
  211. package/dist/components/MissingQueryResultsWarning/MissingQueryResultsWarning.js.map +1 -1
  212. package/dist/components/ModalDownload/ModalDownload.js.map +1 -1
  213. package/dist/components/OAuthClientAclEditor/OAuthClientAclEditor.d.ts.map +1 -1
  214. package/dist/components/OAuthClientAclEditor/OAuthClientAclEditor.js +45 -39
  215. package/dist/components/OAuthClientAclEditor/OAuthClientAclEditor.js.map +1 -1
  216. package/dist/components/OAuthClientManagement/OAuthManagement.js.map +1 -1
  217. package/dist/components/PageProgress/PageProgress.js.map +1 -1
  218. package/dist/components/Plot/DotPlot.js.map +1 -1
  219. package/dist/components/Plot/Plot.js.map +1 -1
  220. package/dist/components/Plot/SynapsePlot.js.map +1 -1
  221. package/dist/components/Plot/ThemesPlot.js.map +1 -1
  222. package/dist/components/Plot/UpsetPlot.js.map +1 -1
  223. package/dist/components/PortalAclEditor/PortalAclEditor.d.ts.map +1 -1
  224. package/dist/components/PortalAclEditor/PortalAclEditor.js +43 -41
  225. package/dist/components/PortalAclEditor/PortalAclEditor.js.map +1 -1
  226. package/dist/components/PortalFeaturedPartners/PortalFeaturedPartners.js.map +1 -1
  227. package/dist/components/PortalList/CreatePortalModal.js.map +1 -1
  228. package/dist/components/ProgrammaticInstructionsModal/ProgrammaticInstructionsModal.js.map +1 -1
  229. package/dist/components/ProgrammaticTableDownload/ProgrammaticTableDownload.js.map +1 -1
  230. package/dist/components/Programs/Programs.Mobile.js.map +1 -1
  231. package/dist/components/Programs/Programs.js.map +1 -1
  232. package/dist/components/ProvenanceGraph/ProvenanceExternalIcon.js.map +1 -1
  233. package/dist/components/ProvenanceGraph/ProvenanceGraph.js.map +1 -1
  234. package/dist/components/ProvenanceGraph/ProvenanceGraphUtils.js.map +1 -1
  235. package/dist/components/ProvenanceGraph/ProvenanceUtils.js.map +1 -1
  236. package/dist/components/QueryCount/QueryCount.js.map +1 -1
  237. package/dist/components/QueryCountButton/QueryCountButton.js.map +1 -1
  238. package/dist/components/QueryVisualizationWrapper/QueryVisualizationWrapper.js.map +1 -1
  239. package/dist/components/QueryWrapper/QueryWrapper.js.map +1 -1
  240. package/dist/components/QueryWrapper/TableQueryUseQueryOptions.js.map +1 -1
  241. package/dist/components/QueryWrapper/TableRowSelectionState.js.map +1 -1
  242. package/dist/components/QueryWrapper/generateEncodedPathAndQueryForSelectedFacetURL.js.map +1 -1
  243. package/dist/components/QueryWrapper/useGetQueryMetadata.js.map +1 -1
  244. package/dist/components/QueryWrapperErrorBoundary.js.map +1 -1
  245. package/dist/components/QueryWrapperPlotNav/QueryWrapperPlotNav.js.map +1 -1
  246. package/dist/components/QueryWrapperPlotNav/UseRowSet.js.map +1 -1
  247. package/dist/components/RecentPublicationsGrid/RecentPublicationsGrid.js.map +1 -1
  248. package/dist/components/ReleaseCard/ReleaseCardUtils.js.map +1 -1
  249. package/dist/components/ResizableContainer/hooks/useResizable.js.map +1 -1
  250. package/dist/components/Resources/Resources.Mobile.js.map +1 -1
  251. package/dist/components/Resources/Resources.js.map +1 -1
  252. package/dist/components/RowDataTable/RowDataTableWithQuery.js.map +1 -1
  253. package/dist/components/SageResourcesPopover/SageResourcesPopover.js.map +1 -1
  254. package/dist/components/SchemaDrivenAnnotationEditor/AnnotationEditorUtils.js.map +1 -1
  255. package/dist/components/SetAccessRequirementCommonFields/SetAccessRequirementCommonFields.js.map +1 -1
  256. package/dist/components/SetManagedAccessRequirementFields/SetManagedAccessRequirementFields.js.map +1 -1
  257. package/dist/components/SmartLink/SmartButton.js.map +1 -1
  258. package/dist/components/SmartLink/SmartLink.js.map +1 -1
  259. package/dist/components/SourceAppImage.js.map +1 -1
  260. package/dist/components/StandaloneQueryWrapper/StandaloneQueryWrapper.js.map +1 -1
  261. package/dist/components/StatisticsPlot.js.map +1 -1
  262. package/dist/components/StorybookComponentWrapper.js.map +1 -1
  263. package/dist/components/SubsectionRowRenderer/SubsectionRowRenderer.js.map +1 -1
  264. package/dist/components/SustainabilityScorecard/SustainabilityScorecard.js.map +1 -1
  265. package/dist/components/SynapseChat/GridAgentChat.js.map +1 -1
  266. package/dist/components/SynapseChat/SynapseChatInteraction.js.map +1 -1
  267. package/dist/components/SynapseChat/SynapseChatMessage.js.map +1 -1
  268. package/dist/components/SynapseChat/extractMessageFromTraceEvent.js.map +1 -1
  269. package/dist/components/SynapseForm/StepsSideNav.js.map +1 -1
  270. package/dist/components/SynapseForm/SummaryTable.js.map +1 -1
  271. package/dist/components/SynapseForm/SynapseForm.js +4 -2
  272. package/dist/components/SynapseForm/SynapseForm.js.map +1 -1
  273. package/dist/components/SynapseForm/SynapseFormWrapper.js.map +1 -1
  274. package/dist/components/SynapseHomepageV2/SynapseByTheNumbersItem.js.map +1 -1
  275. package/dist/components/SynapseHomepageV2/SynapseFeatureItem.js.map +1 -1
  276. package/dist/components/SynapseHomepageV2/SynapseHomepageChatSearch.js.map +1 -1
  277. package/dist/components/SynapseHomepageV2/SynapseHomepageSearch.js.map +1 -1
  278. package/dist/components/SynapseHomepageV2/SynapseInActionItem.js.map +1 -1
  279. package/dist/components/SynapseHomepageV2/SynapsePlans.js.map +1 -1
  280. package/dist/components/SynapseHomepageV2/SynapseTrendingProjects.js.map +1 -1
  281. package/dist/components/SynapseNavDrawer/SynapseNavDrawer.d.ts +8 -7
  282. package/dist/components/SynapseNavDrawer/SynapseNavDrawer.d.ts.map +1 -1
  283. package/dist/components/SynapseNavDrawer/SynapseNavDrawer.js +173 -164
  284. package/dist/components/SynapseNavDrawer/SynapseNavDrawer.js.map +1 -1
  285. package/dist/components/SynapsePortalBanners/SynapsePortalBanners.js.map +1 -1
  286. package/dist/components/SynapseSearchPageResults/SearchFacetPanel/SearchFacetPanel.js.map +1 -1
  287. package/dist/components/SynapseSearchPageResults/SearchFacetPanel/SearchFacetPanelUtils.js.map +1 -1
  288. package/dist/components/SynapseSearchPageResults/SynapseSearchPageResults.js.map +1 -1
  289. package/dist/components/SynapseTable/EntityIDColumnCopyIcon.js.map +1 -1
  290. package/dist/components/SynapseTable/NoContentPlaceholderType.js.map +1 -1
  291. package/dist/components/SynapseTable/RowSelection/RowSelectionControls.js.map +1 -1
  292. package/dist/components/SynapseTable/SynapseTableCell/SynapseTableCell.js.map +1 -1
  293. package/dist/components/SynapseTable/SynapseTableRenderers.js.map +1 -1
  294. package/dist/components/SynapseTable/datasets/DatasetItemsEditor.js.map +1 -1
  295. package/dist/components/SynapseTable/table-top/ColumnSelection.js.map +1 -1
  296. package/dist/components/SynapseTable/table-top/DownloadOptions.js.map +1 -1
  297. package/dist/components/SynapseTable/usePrefetchTableData.js.map +1 -1
  298. package/dist/components/TableColumnSchemaEditor/ColumnModelForm.js.map +1 -1
  299. package/dist/components/TableColumnSchemaEditor/ColumnModelFormFields/DefaultValueField.js.map +1 -1
  300. package/dist/components/TableColumnSchemaEditor/ImportTableColumnsButton.js.map +1 -1
  301. package/dist/components/TableColumnSchemaEditor/TableColumnSchemaEditorUtils.d.ts +1 -1
  302. package/dist/components/TableColumnSchemaEditor/TableColumnSchemaEditorUtils.d.ts.map +1 -1
  303. package/dist/components/TableColumnSchemaEditor/TableColumnSchemaEditorUtils.js.map +1 -1
  304. package/dist/components/TableColumnSchemaEditor/TableColumnSchemaForm.js.map +1 -1
  305. package/dist/components/TableColumnSchemaEditor/TableColumnSchemaFormReducer.js.map +1 -1
  306. package/dist/components/TableColumnSchemaEditor/Validators/ColumnModelValidator.js.map +1 -1
  307. package/dist/components/TableColumnSchemaEditor/Validators/DatetimeSchema.js.map +1 -1
  308. package/dist/components/TanStackTable/ColumnHeader.d.ts +1 -0
  309. package/dist/components/TanStackTable/ColumnHeader.d.ts.map +1 -1
  310. package/dist/components/TanStackTable/ColumnHeader.js +8 -8
  311. package/dist/components/TanStackTable/ColumnHeader.js.map +1 -1
  312. package/dist/components/TanStackTable/ColumnHeaderEnumFilter.js.map +1 -1
  313. package/dist/components/TanStackTable/TableBody.js.map +1 -1
  314. package/dist/components/TeamSubjectsSelector/TeamSubjectsSelector.js.map +1 -1
  315. package/dist/components/TextField/TextField.js.map +1 -1
  316. package/dist/components/TimelinePlot/TimelinePhase.js.map +1 -1
  317. package/dist/components/TimelinePlot/TimelinePlot.js.map +1 -1
  318. package/dist/components/TimelinePlot/TimelinePlotSpeciesSelector.js.map +1 -1
  319. package/dist/components/UserCard/Avatar.js.map +1 -1
  320. package/dist/components/UserCardList/UserCardList.js.map +1 -1
  321. package/dist/components/UserCardList/UserCardListGroups/UserCardListGroups.Mobile.js.map +1 -1
  322. package/dist/components/UserCardList/UserCardListRotate.js.map +1 -1
  323. package/dist/components/UserOrTeamBadge/useUserOrTeam.js.map +1 -1
  324. package/dist/components/UserProfileLinks/UserProjects.js.map +1 -1
  325. package/dist/components/UserSearchBox/UserSearchBox.js.map +1 -1
  326. package/dist/components/Webhook/WebhookDashboard.js.map +1 -1
  327. package/dist/components/WikiMarkdownEditor/WikiMarkdownEditor.js.map +1 -1
  328. package/dist/components/WikiMarkdownEditorButton/WikiMarkdownEditorButton.js.map +1 -1
  329. package/dist/components/dataaccess/AccessApprovalsTable.js.map +1 -1
  330. package/dist/components/dataaccess/AccessRequestSubmissionTable.js.map +1 -1
  331. package/dist/components/dataaccess/SubmissionPage/SubmissionPage.js.map +1 -1
  332. package/dist/components/dataaccess/UseAccessRequirementTable.js.map +1 -1
  333. package/dist/components/dataaccess/UserAccessRequestHistory/UserAccessRequestHistoryTable.js.map +1 -1
  334. package/dist/components/doi/CreateOrUpdateDoiModal.js.map +1 -1
  335. package/dist/components/entity/page/CreatedByModifiedBy.js.map +1 -1
  336. package/dist/components/entity/page/action_menu/EntityActionMenu.js.map +1 -1
  337. package/dist/components/entity/page/title_bar/useDataCiteUsage.js.map +1 -1
  338. package/dist/components/entity/page/title_bar/useGetMentions.js.map +1 -1
  339. package/dist/components/error/ErrorPage.js.map +1 -1
  340. package/dist/components/favorites/FavoritesPage.js.map +1 -1
  341. package/dist/components/file/upload/BasicFileHandleUpload.js.map +1 -1
  342. package/dist/components/layout/SWCHeader.d.ts +9 -0
  343. package/dist/components/layout/SWCHeader.d.ts.map +1 -0
  344. package/dist/components/layout/SWCHeader.js +19 -0
  345. package/dist/components/layout/SWCHeader.js.map +1 -0
  346. package/dist/components/layout/SWCPageLayout.d.ts +9 -0
  347. package/dist/components/layout/SWCPageLayout.d.ts.map +1 -0
  348. package/dist/components/layout/SWCPageLayout.js +14 -0
  349. package/dist/components/layout/SWCPageLayout.js.map +1 -0
  350. package/dist/components/menu/ComplexMenu.js.map +1 -1
  351. package/dist/components/row_renderers/utils/ChipContainer.js.map +1 -1
  352. package/dist/components/styled/StyledPopover.js.map +1 -1
  353. package/dist/components/table/CsvPreview/CsvPreview.js +2 -1
  354. package/dist/components/table/CsvPreview/CsvPreview.js.map +1 -1
  355. package/dist/components/table/CsvPreview/CsvPreviewDialog.js.map +1 -1
  356. package/dist/components/trash/TrashCanList.js.map +1 -1
  357. package/dist/components/widgets/FileHandleLink.js.map +1 -1
  358. package/dist/components/widgets/RangeSlider/RangeSlider.js.map +1 -1
  359. package/dist/components/widgets/SynapseVideo.js.map +1 -1
  360. package/dist/components/widgets/facet-nav/FacetNavPanel.js.map +1 -1
  361. package/dist/components/widgets/facet-nav/PlotsContainer.js.map +1 -1
  362. package/dist/components/widgets/facet-nav/SelectionCriteriaPills.js.map +1 -1
  363. package/dist/components/widgets/facet-nav/useFacetPlots.js.map +1 -1
  364. package/dist/components/widgets/query-filter/CombinedRangeFacetFilter.js.map +1 -1
  365. package/dist/components/widgets/query-filter/EnumFacetFilter/EnumFacetFilter.js.map +1 -1
  366. package/dist/components/widgets/query-filter/FacetFilterControls.js.map +1 -1
  367. package/dist/components/widgets/query-filter/RangeFacetFilter.js.map +1 -1
  368. package/dist/components/widgets/query-filter/RangeFacetFilterUI.js.map +1 -1
  369. package/dist/features/curator/GridPage/components/GridPageTitle.d.ts.map +1 -1
  370. package/dist/features/curator/GridPage/components/GridPageTitle.js +23 -30
  371. package/dist/features/curator/GridPage/components/GridPageTitle.js.map +1 -1
  372. package/dist/features/curator/dashboard/CuratorDashboard.d.ts +2 -0
  373. package/dist/features/curator/dashboard/CuratorDashboard.d.ts.map +1 -0
  374. package/dist/features/curator/dashboard/CuratorDashboard.js +45 -0
  375. package/dist/features/curator/dashboard/CuratorDashboard.js.map +1 -0
  376. package/dist/features/curator/dashboard/components/CurationTaskCard.css +1 -0
  377. package/dist/features/curator/dashboard/components/CurationTaskCard.d.ts +9 -0
  378. package/dist/features/curator/dashboard/components/CurationTaskCard.d.ts.map +1 -0
  379. package/dist/features/curator/dashboard/components/CurationTaskCard.js +106 -0
  380. package/dist/features/curator/dashboard/components/CurationTaskCard.js.map +1 -0
  381. package/dist/features/curator/dashboard/components/CurationTaskCard.module.js +12 -0
  382. package/dist/features/curator/dashboard/components/CurationTaskCard.module.js.map +1 -0
  383. package/dist/features/curator/dashboard/components/CurationTaskCard.module.scss +52 -0
  384. package/dist/features/curator/dashboard/components/NextStepButton.css +1 -0
  385. package/dist/features/curator/dashboard/components/NextStepButton.d.ts +14 -0
  386. package/dist/features/curator/dashboard/components/NextStepButton.d.ts.map +1 -0
  387. package/dist/features/curator/dashboard/components/NextStepButton.js +35 -0
  388. package/dist/features/curator/dashboard/components/NextStepButton.js.map +1 -0
  389. package/dist/features/curator/dashboard/components/NextStepButton.module.js +11 -0
  390. package/dist/features/curator/dashboard/components/NextStepButton.module.js.map +1 -0
  391. package/dist/features/curator/dashboard/components/NextStepButton.module.scss +57 -0
  392. package/dist/features/curator/dashboard/components/UserOrTeamChip.css +1 -1
  393. package/dist/features/curator/dashboard/components/UserOrTeamChip.module.js +1 -1
  394. package/dist/features/curator/dashboard/components/UserOrTeamChip.module.js.map +1 -1
  395. package/dist/features/curator/dashboard/components/UserOrTeamChip.module.scss +5 -5
  396. package/dist/features/curator/dashboard/components/shared.css +1 -0
  397. package/dist/features/curator/dashboard/components/shared.module.js +5 -0
  398. package/dist/features/curator/dashboard/components/shared.module.js.map +1 -0
  399. package/dist/features/curator/dashboard/components/shared.module.scss +8 -0
  400. package/dist/features/entity/metadata-task/components/MetadataTaskTableActionCell.d.ts +0 -2
  401. package/dist/features/entity/metadata-task/components/MetadataTaskTableActionCell.d.ts.map +1 -1
  402. package/dist/features/entity/metadata-task/components/MetadataTaskTableActionCell.js +16 -34
  403. package/dist/features/entity/metadata-task/components/MetadataTaskTableActionCell.js.map +1 -1
  404. package/dist/features/entity/metadata-task/components/MetadataTasksTableAssigneeCell.js.map +1 -1
  405. package/dist/features/entity/metadata-task/hooks/useGetOrCreateGridSessionForSource.js.map +1 -1
  406. package/dist/features/entity/metadata-task/hooks/useGridSessionForCurationTask.js.map +1 -1
  407. package/dist/features/entity/metadata-task/hooks/useGridSessionForCurationTask_legacy.js.map +1 -1
  408. package/dist/features/entity/metadata-task/hooks/useMetadataTaskTable.js +1 -1
  409. package/dist/features/entity/metadata-task/hooks/useMetadataTaskTable.js.map +1 -1
  410. package/dist/features/entity/metadata-task/hooks/useOpenCuratorButton.d.ts +10 -0
  411. package/dist/features/entity/metadata-task/hooks/useOpenCuratorButton.d.ts.map +1 -0
  412. package/dist/features/entity/metadata-task/hooks/useOpenCuratorButton.js +37 -0
  413. package/dist/features/entity/metadata-task/hooks/useOpenCuratorButton.js.map +1 -0
  414. package/dist/features/entity/metadata-task/utils/constants.d.ts +5 -0
  415. package/dist/features/entity/metadata-task/utils/constants.d.ts.map +1 -0
  416. package/dist/features/entity/metadata-task/utils/constants.js +6 -0
  417. package/dist/features/entity/metadata-task/utils/constants.js.map +1 -0
  418. package/dist/mocks/challenge/mockChallenge.js.map +1 -1
  419. package/dist/mocks/entity/mockDataset.js.map +1 -1
  420. package/dist/mocks/entity/mockDatasetCollection.js.map +1 -1
  421. package/dist/mocks/entity/mockFileEntity.js.map +1 -1
  422. package/dist/mocks/entity/mockFileView.js.map +1 -1
  423. package/dist/mocks/entity/mockGeneratedEntityData.js.map +1 -1
  424. package/dist/mocks/entity/mockProject.js.map +1 -1
  425. package/dist/mocks/entity/mockProjectView.js.map +1 -1
  426. package/dist/mocks/entity/mockRootEntity.js.map +1 -1
  427. package/dist/mocks/entity/mockTableEntity.js.map +1 -1
  428. package/dist/mocks/mockWiki.js.map +1 -1
  429. package/dist/mocks/msw/handlers/asyncJobHandlers.js.map +1 -1
  430. package/dist/mocks/msw/handlers/challengeHandlers.js.map +1 -1
  431. package/dist/mocks/msw/handlers/changePasswordHandlers.js.map +1 -1
  432. package/dist/mocks/msw/handlers/discussionHandlers.js.map +1 -1
  433. package/dist/mocks/msw/handlers/entityHandlers.js.map +1 -1
  434. package/dist/mocks/msw/handlers/fileHandlers.js.map +1 -1
  435. package/dist/mocks/msw/handlers/gridHandlers.js.map +1 -1
  436. package/dist/mocks/msw/handlers/personalAccessTokenHandlers.js.map +1 -1
  437. package/dist/mocks/msw/handlers/subscriptionHandlers.js.map +1 -1
  438. package/dist/mocks/msw/handlers/teamHandlers.js.map +1 -1
  439. package/dist/mocks/msw/handlers/userProfileHandlers.js.map +1 -1
  440. package/dist/mocks/msw/handlers/wikiHandlers.js.map +1 -1
  441. package/dist/mocks/provenance/mockActivity.js.map +1 -1
  442. package/dist/mocks/query/mockReleaseCardsTableQueryResultBundle.js.map +1 -1
  443. package/dist/ror-client/index.js.map +1 -1
  444. package/dist/style/components/_cards.scss +4 -0
  445. package/dist/style/components/_data-grid-extra.css +1 -1
  446. package/dist/style/components/_data-grid-extra.scss +2 -0
  447. package/dist/style/main.css +1 -1
  448. package/dist/synapse-client/HttpClient.js.map +1 -1
  449. package/dist/synapse-client/SynapseClient.js.map +1 -1
  450. package/dist/synapse-queries/QueryMatching.test-utils.js.map +1 -1
  451. package/dist/synapse-queries/auth/useTwoFactorEnrollment.js.map +1 -1
  452. package/dist/synapse-queries/curation/task/useCurationTask.d.ts +1 -1
  453. package/dist/synapse-queries/curation/task/useCurationTask.d.ts.map +1 -1
  454. package/dist/synapse-queries/curation/task/useCurationTask.js +1 -1
  455. package/dist/synapse-queries/curation/task/useCurationTask.js.map +1 -1
  456. package/dist/synapse-queries/dataaccess/useRestrictionInformation.js.map +1 -1
  457. package/dist/synapse-queries/doi/useDOI.js.map +1 -1
  458. package/dist/synapse-queries/download/useDownloadList.js.map +1 -1
  459. package/dist/synapse-queries/entity/useEntity.js.map +1 -1
  460. package/dist/synapse-queries/entity/useEntityBundle.js.map +1 -1
  461. package/dist/synapse-queries/entity/useExportTableQueryToAnalysisPlatform.js.map +1 -1
  462. package/dist/synapse-queries/entity/useExportToTerra.js.map +1 -1
  463. package/dist/synapse-queries/entity/useGetQueryResultBundle.js.map +1 -1
  464. package/dist/synapse-queries/entity/useSchema.js.map +1 -1
  465. package/dist/synapse-queries/file/UploadToS3.js.map +1 -1
  466. package/dist/synapse-queries/file/useDirectUploadToS3.js.map +1 -1
  467. package/dist/synapse-queries/file/useFiles.js.map +1 -1
  468. package/dist/synapse-queries/forum/useReply.js.map +1 -1
  469. package/dist/synapse-queries/forum/useThread.js.map +1 -1
  470. package/dist/synapse-queries/grid/useEstablishWebsocketConnection.d.ts +2 -0
  471. package/dist/synapse-queries/grid/useEstablishWebsocketConnection.d.ts.map +1 -1
  472. package/dist/synapse-queries/grid/useEstablishWebsocketConnection.js.map +1 -1
  473. package/dist/synapse-queries/grid/useExportGrid.js.map +1 -1
  474. package/dist/synapse-queries/grid/useGridSession.js.map +1 -1
  475. package/dist/synapse-queries/grid/useImportCsvIntoGrid.js.map +1 -1
  476. package/dist/synapse-queries/subscription/useSubscription.js.map +1 -1
  477. package/dist/synapse-queries/table/useGetCsvPreview.js.map +1 -1
  478. package/dist/synapse-queries/table/useTableUpdateTransaction.js.map +1 -1
  479. package/dist/synapse-queries/team/useTeamMembers.js.map +1 -1
  480. package/dist/synapse-queries/user/useGetUserChallenges.js.map +1 -1
  481. package/dist/synapse-queries/user/useUserBundle.js.map +1 -1
  482. package/dist/synapse-queries/user/useUserGroupHeader.js.map +1 -1
  483. package/dist/testutils/ReactQueryMockUtils.js.map +1 -1
  484. package/dist/theme/ThemeProvider.js.map +1 -1
  485. package/dist/tsconfig.build.tsbuildinfo +1 -1
  486. package/dist/utils/AppUtils/session/SynapseSessionManager.js.map +1 -1
  487. package/dist/utils/AppUtils/session/useSessionManager.js.map +1 -1
  488. package/dist/utils/PermissionLevelToAccessType.js.map +1 -1
  489. package/dist/utils/challenge/evaluation/EvaluationUtils.js.map +1 -1
  490. package/dist/utils/context/SynapseContext.js.map +1 -1
  491. package/dist/utils/functions/AccessControlListUtils.d.ts +4 -0
  492. package/dist/utils/functions/AccessControlListUtils.d.ts.map +1 -1
  493. package/dist/utils/functions/AccessControlListUtils.js +12 -1
  494. package/dist/utils/functions/AccessControlListUtils.js.map +1 -1
  495. package/dist/utils/functions/GridApiUtils.js.map +1 -1
  496. package/dist/utils/functions/QueryFilterUtils.js.map +1 -1
  497. package/dist/utils/functions/RealmUtils.d.ts +4 -0
  498. package/dist/utils/functions/RealmUtils.d.ts.map +1 -1
  499. package/dist/utils/functions/RealmUtils.js +9 -3
  500. package/dist/utils/functions/RealmUtils.js.map +1 -1
  501. package/dist/utils/functions/SanitizeHtmlUtils.js.map +1 -1
  502. package/dist/utils/functions/SanitizeHtmlUtils.test-utils.js.map +1 -1
  503. package/dist/utils/functions/SqlFunctions.js.map +1 -1
  504. package/dist/utils/functions/StringUtils.js.map +1 -1
  505. package/dist/utils/functions/deepLinkingUtils.js.map +1 -1
  506. package/dist/utils/functions/getDataFromFromStorage.js.map +1 -1
  507. package/dist/utils/functions/getEndpoint.js.map +1 -1
  508. package/dist/utils/functions/getUserData.js.map +1 -1
  509. package/dist/utils/functions/queryUtils.js.map +1 -1
  510. package/dist/utils/functions/testDownloadSpeed.js.map +1 -1
  511. package/dist/utils/hooks/useConfirmItems.js.map +1 -1
  512. package/dist/utils/hooks/useCookiePreferences.js.map +1 -1
  513. package/dist/utils/hooks/useCreateShortUrl.js.map +1 -1
  514. package/dist/utils/hooks/useDetectSSOCode.js.map +1 -1
  515. package/dist/utils/hooks/useDirectDownloadHandler.js.map +1 -1
  516. package/dist/utils/hooks/useGetGoalData.js.map +1 -1
  517. package/dist/utils/hooks/useGetInfoFromIds.js.map +1 -1
  518. package/dist/utils/hooks/useImageUrlUtils.js.map +1 -1
  519. package/dist/utils/hooks/useImmutableTableQuery/useImmutableTableQuery.js.map +1 -1
  520. package/dist/utils/hooks/useImmutableTableQuery/useTableQueryReducer.js.map +1 -1
  521. package/dist/utils/hooks/useIsBot.js.map +1 -1
  522. package/dist/utils/hooks/useListState.js.map +1 -1
  523. package/dist/utils/hooks/useLogin.d.ts.map +1 -1
  524. package/dist/utils/hooks/useLogin.js +53 -52
  525. package/dist/utils/hooks/useLogin.js.map +1 -1
  526. package/dist/utils/hooks/useMutuallyExclusiveState.js.map +1 -1
  527. package/dist/utils/hooks/useOverlay.js.map +1 -1
  528. package/dist/utils/hooks/usePreFetchResource.js.map +1 -1
  529. package/dist/utils/hooks/useQuerySearchParam.js.map +1 -1
  530. package/dist/utils/hooks/useScrollFadeTransition.js.map +1 -1
  531. package/dist/utils/hooks/useSet.js.map +1 -1
  532. package/dist/utils/hooks/useSourceAppConfigs.js.map +1 -1
  533. package/dist/utils/hooks/useTableImageUrl.js.map +1 -1
  534. package/dist/utils/hooks/useUploadFileEntity/useCreatePathsAndGetParentId.js.map +1 -1
  535. package/dist/utils/hooks/useUploadFileEntity/useLinkFileEntityToURL.js.map +1 -1
  536. package/dist/utils/hooks/useUploadFileEntity/usePrepareFileEntityUpload.js.map +1 -1
  537. package/dist/utils/hooks/useUploadFileEntity/useTrackFileUploads.js.map +1 -1
  538. package/dist/utils/hooks/useUploadFileEntity/useUploadFileEntities.js.map +1 -1
  539. package/dist/utils/hooks/useUploadFileEntity/useUploadFiles.js.map +1 -1
  540. package/dist/utils/hooks/useUploadFileEntity/willUploadsExceedStorageLimit.js.map +1 -1
  541. package/dist/utils/html/TargetEnum.js.map +1 -1
  542. package/dist/utils/jsonschema/SchemaAnnotationUtils.js.map +1 -1
  543. package/package.json +4 -4
@@ -1,100 +1,101 @@
1
1
  import { AUTHENTICATION_RECEIPT_LOCALSTORAGE_KEY as e } from "../SynapseConstants.js";
2
2
  import t from "../../synapse-client/index.js";
3
3
  import { useOneSageURL as n } from "./useOneSageURL.js";
4
- import { useResetTwoFactorAuth as r } from "../../synapse-queries/auth/useTwoFactorEnrollment.js";
4
+ import { TOTP_CLOCK_SKEW_ERROR_APPENDAGE as r } from "../../components/Authentication/Constants.js";
5
+ import { useResetTwoFactorAuth as i } from "../../synapse-queries/auth/useTwoFactorEnrollment.js";
5
6
  import "../../synapse-queries/index.js";
6
- import { useEffect as i, useState as a } from "react";
7
- import { noop as o } from "lodash-es";
8
- import { ErrorResponseCode as s } from "@sage-bionetworks/synapse-types";
9
- import { instanceOfTwoFactorAuthErrorResponse as c } from "@sage-bionetworks/synapse-client/generated/models/TwoFactorAuthErrorResponse";
10
- import { useMutation as l } from "@tanstack/react-query";
7
+ import { useEffect as a, useState as o } from "react";
8
+ import { noop as s } from "lodash-es";
9
+ import { ErrorResponseCode as c } from "@sage-bionetworks/synapse-types";
10
+ import { instanceOfTwoFactorAuthErrorResponse as l } from "@sage-bionetworks/synapse-client/generated/models/TwoFactorAuthErrorResponse";
11
+ import { useMutation as u } from "@tanstack/react-query";
11
12
  //#region src/utils/hooks/useLogin.ts
12
- var u = [
13
+ var d = [
13
14
  "VERIFICATION_CODE",
14
15
  "RECOVERY_CODE",
15
16
  "LOGGED_IN",
16
17
  "DISABLE_2FA_PROMPT"
17
18
  ];
18
- function d(d) {
19
- let { sessionCallback: f = o, twoFaErrorResponse: p, onTwoFactorAuthRequired: m = o } = d, [h, g] = a("CHOOSE_AUTH_METHOD"), [_, v] = a(), [y, b] = a(), x = n("/changePassword");
20
- i(() => {
21
- p && b(p);
22
- }, [p]), i(() => {
19
+ function f(f) {
20
+ let { sessionCallback: p = s, twoFaErrorResponse: m, onTwoFactorAuthRequired: h = s } = f, [g, _] = o("CHOOSE_AUTH_METHOD"), [v, y] = o(), [b, x] = o(), S = n("/changePassword");
21
+ a(() => {
22
+ m && x(m);
23
+ }, [m]), a(() => {
23
24
  let { searchParams: e } = new URL(window.location.href);
24
25
  if (e) {
25
26
  let t = e.get("userId"), n = e.get("twoFaToken");
26
- t && n && (b({
27
- errorCode: s.TWO_FA_REQUIRED,
27
+ t && n && (x({
28
+ errorCode: c.TWO_FA_REQUIRED,
28
29
  reason: "",
29
30
  userId: parseInt(t, 10),
30
31
  twoFaToken: n,
31
32
  concreteType: "org.sagebionetworks.repo.model.auth.TwoFactorAuthErrorResponse"
32
- }), u.includes(h) || g("VERIFICATION_CODE"));
33
+ }), d.includes(g) || _("VERIFICATION_CODE"));
33
34
  }
34
- }, [h]), i(() => {
35
- y && (b(y), u.includes(h) || g("VERIFICATION_CODE"));
36
- }, [y]), i(() => {
37
- y && m(y);
38
- }, [m, y]), i(() => {
39
- v(void 0);
40
- }, [h]);
41
- async function S(n) {
42
- await t.setAccessTokenCookie(n.accessToken), localStorage.setItem(e, n.authenticationReceipt ?? ""), g("LOGGED_IN"), f && f();
35
+ }, [g]), a(() => {
36
+ b && (x(b), d.includes(g) || _("VERIFICATION_CODE"));
37
+ }, [b]), a(() => {
38
+ b && h(b);
39
+ }, [h, b]), a(() => {
40
+ y(void 0);
41
+ }, [g]);
42
+ async function C(n) {
43
+ await t.setAccessTokenCookie(n.accessToken), localStorage.setItem(e, n.authenticationReceipt ?? ""), _("LOGGED_IN"), p && p();
43
44
  }
44
- let { mutate: C, isPending: w } = l({
45
+ let { mutate: w, isPending: T } = u({
45
46
  mutationFn: ({ username: e, password: n, authenticationReceipt: r }) => t.login(e, n, r),
46
47
  onError: (e) => {
47
- v(e.reason);
48
+ y(e.reason);
48
49
  let { errorResponse: t } = e;
49
- t && "errorCode" in t && t.errorCode == s.PASSWORD_RESET_VIA_EMAIL_REQUIRED && window.location.assign(x.toString());
50
+ t && "errorCode" in t && t.errorCode == c.PASSWORD_RESET_VIA_EMAIL_REQUIRED && window.location.assign(S.toString());
50
51
  },
51
52
  onSuccess: async (e) => {
52
- e && (c(e) ? (g("VERIFICATION_CODE"), b(e)) : await S(e));
53
+ e && (l(e) ? (_("VERIFICATION_CODE"), x(e)) : await C(e));
53
54
  }
54
- }), { mutate: T, isPending: E } = l({
55
+ }), { mutate: E, isPending: D } = u({
55
56
  mutationFn: t.loginWith2fa,
56
57
  onError: (e) => {
57
- v(e.reason), (e.reason.includes("The provided twoFaToken is invalid") || e.reason.includes("Token has expired")) && (console.warn(e), v("Something went wrong. Refresh the page and try again."), window.location.href.includes("twoFaToken") && window.history.replaceState({}, document.title, window.location.href.replaceAll(/(twoFaToken|userId)=[^&]*&?/g, "")));
58
+ y(e.reason), e.reason.includes("The provided code is invalid") ? y(`${e.reason} ${r}`) : (e.reason.includes("The provided twoFaToken is invalid") || e.reason.includes("Token has expired")) && (console.warn(e), y("Something went wrong. Refresh the page and try again."), window.location.href.includes("twoFaToken") && window.history.replaceState({}, document.title, window.location.href.replaceAll(/(twoFaToken|userId)=[^&]*&?/g, "")));
58
59
  },
59
- onSuccess: S
60
- }), { mutate: D, isSuccess: O, isPending: k } = r({ onError: (e) => {
61
- v(e.reason);
62
- } }), A = (t, n) => {
63
- v(void 0), C({
60
+ onSuccess: C
61
+ }), { mutate: O, isSuccess: k, isPending: A } = i({ onError: (e) => {
62
+ y(e.reason);
63
+ } }), j = (t, n) => {
64
+ y(void 0), w({
64
65
  username: t,
65
66
  password: n,
66
67
  authenticationReceipt: localStorage.getItem(e)
67
68
  });
68
69
  };
69
- function j(e) {
70
- return e == null ? (v("You did not first log in with your password or a third-party identity provider."), !1) : !0;
70
+ function M(e) {
71
+ return e == null ? (y("You did not first log in with your password or a third-party identity provider."), !1) : !0;
71
72
  }
72
73
  return {
73
- step: h,
74
- onStepChange: g,
75
- submitUsernameAndPassword: A,
76
- submitOneTimePassword: (e, t = h === "RECOVERY_CODE" ? "RECOVERY_CODE" : "TOTP") => {
77
- v(void 0), j(y) && T({
78
- userId: y.userId,
79
- twoFaToken: y.twoFaToken,
74
+ step: g,
75
+ onStepChange: _,
76
+ submitUsernameAndPassword: j,
77
+ submitOneTimePassword: (e, t = g === "RECOVERY_CODE" ? "RECOVERY_CODE" : "TOTP") => {
78
+ y(void 0), M(b) && E({
79
+ userId: b.userId,
80
+ twoFaToken: b.twoFaToken,
80
81
  otpCode: e,
81
82
  otpType: t
82
83
  });
83
84
  },
84
- errorMessage: _,
85
- loginIsPending: w || E,
85
+ errorMessage: v,
86
+ loginIsPending: T || D,
86
87
  beginTwoFactorAuthReset: (e) => {
87
- v(void 0), j(y) && D({
88
- userId: y.userId,
89
- twoFaToken: y.twoFaToken,
88
+ y(void 0), M(b) && O({
89
+ userId: b.userId,
90
+ twoFaToken: b.twoFaToken,
90
91
  twoFaResetEndpoint: e
91
92
  });
92
93
  },
93
- twoFactorAuthResetIsPending: k,
94
- twoFactorAuthResetIsSuccess: O
94
+ twoFactorAuthResetIsPending: A,
95
+ twoFactorAuthResetIsSuccess: k
95
96
  };
96
97
  }
97
98
  //#endregion
98
- export { d as default };
99
+ export { f as default };
99
100
 
100
101
  //# sourceMappingURL=useLogin.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useLogin.js","names":[],"sources":["../../../src/utils/hooks/useLogin.ts"],"sourcesContent":["import SynapseClient from '@/synapse-client'\nimport { useResetTwoFactorAuth } from '@/synapse-queries'\nimport {\n instanceOfTwoFactorAuthErrorResponse,\n TwoFactorAuthErrorResponse,\n} from '@sage-bionetworks/synapse-client/generated/models/TwoFactorAuthErrorResponse'\nimport { SynapseClientError } from '@sage-bionetworks/synapse-client/util/SynapseClientError'\nimport {\n ErrorResponseCode,\n LoginResponse,\n TwoFactorAuthLoginRequest,\n TwoFactorAuthOtpType,\n TwoFactorAuthResetRequest,\n} from '@sage-bionetworks/synapse-types'\nimport { useMutation } from '@tanstack/react-query'\nimport { noop } from 'lodash-es'\nimport { Dispatch, SetStateAction, useEffect, useState } from 'react'\nimport { AUTHENTICATION_RECEIPT_LOCALSTORAGE_KEY } from '../SynapseConstants'\nimport { useOneSageURL } from './useOneSageURL'\nimport { ONE_TIME_PASSWORD_STEP } from '@/components/Authentication/OneTimePasswordForm'\n\nexport type UseLoginOptions = {\n sessionCallback?: () => void\n twoFaErrorResponse?: TwoFactorAuthErrorResponse\n /* If a twoFactorAuthError is encountered (including passed in the twoFactorAuthenticationRequired arg), this callback will be invoked */\n onTwoFactorAuthRequired?: (\n twoFaToken: Pick<TwoFactorAuthErrorResponse, 'twoFaToken' | 'userId'>,\n ) => void\n}\n\nexport type UseLoginReturn = {\n step:\n | 'CHOOSE_AUTH_METHOD'\n | 'USERNAME_PASSWORD'\n | ONE_TIME_PASSWORD_STEP\n | 'LOGGED_IN'\n onStepChange: Dispatch<SetStateAction<UseLoginReturn['step']>>\n submitUsernameAndPassword: (username: string, password: string) => void\n submitOneTimePassword: (\n code: string,\n /* The type of one time password code that can be used to authenticate through two-factor authentication. Default is based on current value of `step` */\n otpType?: TwoFactorAuthOtpType,\n ) => void\n /* Trigger sending an email which can be used to disable 2FA */\n beginTwoFactorAuthReset: (twoFaResetEndpoint: string) => void\n twoFactorAuthResetIsPending: boolean\n twoFactorAuthResetIsSuccess: boolean\n errorMessage: string | undefined\n loginIsPending: boolean\n}\n\n// When prompting the user for a 2FA code, allow the UI to show only these steps\nconst VALID_STEPS_DURING_2FA_PROMPT: Array<UseLoginReturn['step']> = [\n 'VERIFICATION_CODE',\n 'RECOVERY_CODE',\n 'LOGGED_IN',\n 'DISABLE_2FA_PROMPT',\n]\n\n/**\n * Stateful hook that manages logging into Synapse\n */\nexport default function useLogin(opts: UseLoginOptions): UseLoginReturn {\n const {\n sessionCallback = noop,\n twoFaErrorResponse: twoFaErrorResponseFromProps,\n onTwoFactorAuthRequired = noop,\n } = opts\n const [step, setStep] = useState<UseLoginReturn['step']>('CHOOSE_AUTH_METHOD')\n const [errorMessage, setErrorMessage] = useState<string | undefined>()\n const [twoFaErrorResponse, setTwoFaErrorResponse] = useState<\n TwoFactorAuthErrorResponse | undefined\n >()\n\n const changePasswordUrl = useOneSageURL('/changePassword')\n /**\n * Update state variable if optional prop changes\n */\n useEffect(() => {\n if (twoFaErrorResponseFromProps) {\n setTwoFaErrorResponse(twoFaErrorResponseFromProps)\n }\n }, [twoFaErrorResponseFromProps])\n\n /*\n * In SWC, if logging in with OAuth, the servlet will call POST /oauth2/session2 to get the access token.\n *\n * If the user has 2FA enabled, the servlet will receive a 401 with the userId and twoFaToken in the response.\n * The servlet will then redirect the user to the login page with the userId and twoFaToken in the searchParams.\n *\n * This effect will check if the search params are present, and change the step to 'VERIFICATION_CODE' if they are.\n */\n useEffect(() => {\n const fullUrl: URL = new URL(window.location.href)\n\n const { searchParams } = fullUrl\n if (searchParams) {\n const userId = searchParams.get('userId')\n const twoFaToken = searchParams.get('twoFaToken')\n if (userId && twoFaToken) {\n setTwoFaErrorResponse({\n errorCode: ErrorResponseCode.TWO_FA_REQUIRED,\n reason: '',\n userId: parseInt(userId, 10),\n twoFaToken,\n concreteType:\n 'org.sagebionetworks.repo.model.auth.TwoFactorAuthErrorResponse',\n })\n if (!VALID_STEPS_DURING_2FA_PROMPT.includes(step)) {\n setStep('VERIFICATION_CODE')\n }\n }\n }\n }, [step])\n\n /*\n * In client-only apps (like Portals), if logging in with OAuth, the client app will call POST /oauth2/session2 to get the access token.\n *\n * If the user has 2FA enabled, the client app will receive a 401 with the userId and twoFaToken in the response.\n *\n * The app can pass the error response as a prop/argument, and we'll use it here to jump straight to the VERIFICATION_CODE step.\n */\n useEffect(() => {\n if (twoFaErrorResponse) {\n setTwoFaErrorResponse(twoFaErrorResponse)\n if (!VALID_STEPS_DURING_2FA_PROMPT.includes(step)) {\n setStep('VERIFICATION_CODE')\n }\n }\n // We do NOT want to rerun this effect on step change. It should only run once, when we get the error.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [twoFaErrorResponse])\n\n /**\n * Call the onTwoFactorAuthRequired callback when a twoFaToken is present\n */\n useEffect(() => {\n if (twoFaErrorResponse) {\n onTwoFactorAuthRequired(twoFaErrorResponse)\n }\n }, [onTwoFactorAuthRequired, twoFaErrorResponse])\n\n /* When the step changes, clear the old error message. */\n useEffect(() => {\n setErrorMessage(undefined)\n }, [step])\n\n async function finishLogin(loginResponse: LoginResponse) {\n await SynapseClient.setAccessTokenCookie(loginResponse.accessToken)\n localStorage.setItem(\n AUTHENTICATION_RECEIPT_LOCALSTORAGE_KEY,\n loginResponse.authenticationReceipt ?? '',\n )\n setStep('LOGGED_IN')\n\n if (sessionCallback) {\n sessionCallback()\n }\n }\n\n const {\n mutate: mutateLoginWithUsernameAndPassword,\n isPending: loginWithUsernameAndPasswordIsPending,\n } = useMutation<\n LoginResponse | TwoFactorAuthErrorResponse,\n SynapseClientError,\n { username: string; password: string; authenticationReceipt: string | null }\n >({\n mutationFn: ({ username, password, authenticationReceipt }) =>\n SynapseClient.login(username, password, authenticationReceipt),\n onError: error => {\n setErrorMessage(error.reason)\n const { errorResponse } = error\n if (\n errorResponse &&\n 'errorCode' in errorResponse &&\n errorResponse.errorCode ==\n ErrorResponseCode.PASSWORD_RESET_VIA_EMAIL_REQUIRED\n ) {\n window.location.assign(changePasswordUrl.toString())\n }\n },\n onSuccess: async loginResponse => {\n if (loginResponse) {\n if (instanceOfTwoFactorAuthErrorResponse(loginResponse)) {\n setStep('VERIFICATION_CODE')\n setTwoFaErrorResponse(loginResponse)\n } else {\n await finishLogin(loginResponse)\n }\n }\n },\n })\n\n const {\n mutate: mutateLoginWith2FACode,\n isPending: loginWith2FACodeIsPending,\n } = useMutation<LoginResponse, SynapseClientError, TwoFactorAuthLoginRequest>(\n {\n mutationFn: SynapseClient.loginWith2fa,\n onError: e => {\n setErrorMessage(e.reason)\n if (\n // The twoFaToken wasn't transmitted correctly\n e.reason.includes('The provided twoFaToken is invalid') ||\n // The user waited too long to enter the code.\n e.reason.includes('Token has expired')\n ) {\n console.warn(e)\n // Instruct the user refresh to start over.\n setErrorMessage(\n 'Something went wrong. Refresh the page and try again.',\n )\n // If the 2FA token is in the search parameters, remove it so the user doesn't just get the same error again.\n if (window.location.href.includes('twoFaToken')) {\n window.history.replaceState(\n {},\n document.title,\n // using regex because SWC hashbang doesn't work with URLSearchParams\n window.location.href.replaceAll(\n /(twoFaToken|userId)=[^&]*&?/g,\n '',\n ),\n )\n }\n }\n },\n onSuccess: finishLogin,\n },\n )\n\n const {\n mutate: resetTwoFactorAuth,\n isSuccess: twoFactorAuthResetIsSuccess,\n isPending: twoFactorAuthResetIsPending,\n } = useResetTwoFactorAuth({\n onError: e => {\n setErrorMessage(e.reason)\n },\n })\n\n const submitUsernameAndPassword: UseLoginReturn['submitUsernameAndPassword'] =\n (username, password) => {\n setErrorMessage(undefined)\n const authenticationReceipt = localStorage.getItem(\n AUTHENTICATION_RECEIPT_LOCALSTORAGE_KEY,\n )\n mutateLoginWithUsernameAndPassword({\n username,\n password,\n authenticationReceipt,\n })\n }\n\n function verifyTwoFaErrorIsPresent(\n twoFaErrorResponse: TwoFactorAuthErrorResponse | null | undefined,\n ): twoFaErrorResponse is TwoFactorAuthErrorResponse {\n if (twoFaErrorResponse == null) {\n // This type of error could happen if the 2FA component exists on its own route, and the user directly navigates to it without first logging in with credentials/OAuth\n setErrorMessage(\n 'You did not first log in with your password or a third-party identity provider.',\n )\n return false\n }\n return true\n }\n\n const submitOneTimePassword: UseLoginReturn['submitOneTimePassword'] = (\n code,\n otpType = step === 'RECOVERY_CODE' ? 'RECOVERY_CODE' : 'TOTP',\n ) => {\n setErrorMessage(undefined)\n if (verifyTwoFaErrorIsPresent(twoFaErrorResponse)) {\n const request: TwoFactorAuthLoginRequest = {\n userId: twoFaErrorResponse.userId!,\n twoFaToken: twoFaErrorResponse.twoFaToken!,\n otpCode: code,\n otpType: otpType,\n }\n mutateLoginWith2FACode(request)\n }\n }\n\n const beginTwoFactorAuthReset: UseLoginReturn['beginTwoFactorAuthReset'] = (\n twoFaResetEndpoint: string,\n ) => {\n setErrorMessage(undefined)\n if (verifyTwoFaErrorIsPresent(twoFaErrorResponse)) {\n const request: TwoFactorAuthResetRequest = {\n userId: twoFaErrorResponse.userId!,\n twoFaToken: twoFaErrorResponse.twoFaToken,\n twoFaResetEndpoint: twoFaResetEndpoint,\n }\n resetTwoFactorAuth(request)\n }\n }\n\n return {\n step,\n onStepChange: setStep,\n submitUsernameAndPassword,\n submitOneTimePassword: submitOneTimePassword,\n errorMessage,\n loginIsPending:\n loginWithUsernameAndPasswordIsPending || loginWith2FACodeIsPending,\n beginTwoFactorAuthReset,\n twoFactorAuthResetIsPending,\n twoFactorAuthResetIsSuccess,\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"useLogin.js","names":[],"sources":["../../../src/utils/hooks/useLogin.ts"],"sourcesContent":["import SynapseClient from '@/synapse-client'\nimport { useResetTwoFactorAuth } from '@/synapse-queries'\nimport {\n instanceOfTwoFactorAuthErrorResponse,\n TwoFactorAuthErrorResponse,\n} from '@sage-bionetworks/synapse-client/generated/models/TwoFactorAuthErrorResponse'\nimport { SynapseClientError } from '@sage-bionetworks/synapse-client/util/SynapseClientError'\nimport {\n ErrorResponseCode,\n LoginResponse,\n TwoFactorAuthLoginRequest,\n TwoFactorAuthOtpType,\n TwoFactorAuthResetRequest,\n} from '@sage-bionetworks/synapse-types'\nimport { useMutation } from '@tanstack/react-query'\nimport { noop } from 'lodash-es'\nimport { Dispatch, SetStateAction, useEffect, useState } from 'react'\nimport { AUTHENTICATION_RECEIPT_LOCALSTORAGE_KEY } from '../SynapseConstants'\nimport { useOneSageURL } from './useOneSageURL'\nimport { ONE_TIME_PASSWORD_STEP } from '@/components/Authentication/OneTimePasswordForm'\nimport { TOTP_CLOCK_SKEW_ERROR_APPENDAGE } from '@/components/Authentication/Constants'\n\nexport type UseLoginOptions = {\n sessionCallback?: () => void\n twoFaErrorResponse?: TwoFactorAuthErrorResponse\n /* If a twoFactorAuthError is encountered (including passed in the twoFactorAuthenticationRequired arg), this callback will be invoked */\n onTwoFactorAuthRequired?: (\n twoFaToken: Pick<TwoFactorAuthErrorResponse, 'twoFaToken' | 'userId'>,\n ) => void\n}\n\nexport type UseLoginReturn = {\n step:\n | 'CHOOSE_AUTH_METHOD'\n | 'USERNAME_PASSWORD'\n | ONE_TIME_PASSWORD_STEP\n | 'LOGGED_IN'\n onStepChange: Dispatch<SetStateAction<UseLoginReturn['step']>>\n submitUsernameAndPassword: (username: string, password: string) => void\n submitOneTimePassword: (\n code: string,\n /* The type of one time password code that can be used to authenticate through two-factor authentication. Default is based on current value of `step` */\n otpType?: TwoFactorAuthOtpType,\n ) => void\n /* Trigger sending an email which can be used to disable 2FA */\n beginTwoFactorAuthReset: (twoFaResetEndpoint: string) => void\n twoFactorAuthResetIsPending: boolean\n twoFactorAuthResetIsSuccess: boolean\n errorMessage: string | undefined\n loginIsPending: boolean\n}\n\n// When prompting the user for a 2FA code, allow the UI to show only these steps\nconst VALID_STEPS_DURING_2FA_PROMPT: Array<UseLoginReturn['step']> = [\n 'VERIFICATION_CODE',\n 'RECOVERY_CODE',\n 'LOGGED_IN',\n 'DISABLE_2FA_PROMPT',\n]\n\n/**\n * Stateful hook that manages logging into Synapse\n */\nexport default function useLogin(opts: UseLoginOptions): UseLoginReturn {\n const {\n sessionCallback = noop,\n twoFaErrorResponse: twoFaErrorResponseFromProps,\n onTwoFactorAuthRequired = noop,\n } = opts\n const [step, setStep] = useState<UseLoginReturn['step']>('CHOOSE_AUTH_METHOD')\n const [errorMessage, setErrorMessage] = useState<string | undefined>()\n const [twoFaErrorResponse, setTwoFaErrorResponse] = useState<\n TwoFactorAuthErrorResponse | undefined\n >()\n\n const changePasswordUrl = useOneSageURL('/changePassword')\n /**\n * Update state variable if optional prop changes\n */\n useEffect(() => {\n if (twoFaErrorResponseFromProps) {\n setTwoFaErrorResponse(twoFaErrorResponseFromProps)\n }\n }, [twoFaErrorResponseFromProps])\n\n /*\n * In SWC, if logging in with OAuth, the servlet will call POST /oauth2/session2 to get the access token.\n *\n * If the user has 2FA enabled, the servlet will receive a 401 with the userId and twoFaToken in the response.\n * The servlet will then redirect the user to the login page with the userId and twoFaToken in the searchParams.\n *\n * This effect will check if the search params are present, and change the step to 'VERIFICATION_CODE' if they are.\n */\n useEffect(() => {\n const fullUrl: URL = new URL(window.location.href)\n\n const { searchParams } = fullUrl\n if (searchParams) {\n const userId = searchParams.get('userId')\n const twoFaToken = searchParams.get('twoFaToken')\n if (userId && twoFaToken) {\n setTwoFaErrorResponse({\n errorCode: ErrorResponseCode.TWO_FA_REQUIRED,\n reason: '',\n userId: parseInt(userId, 10),\n twoFaToken,\n concreteType:\n 'org.sagebionetworks.repo.model.auth.TwoFactorAuthErrorResponse',\n })\n if (!VALID_STEPS_DURING_2FA_PROMPT.includes(step)) {\n setStep('VERIFICATION_CODE')\n }\n }\n }\n }, [step])\n\n /*\n * In client-only apps (like Portals), if logging in with OAuth, the client app will call POST /oauth2/session2 to get the access token.\n *\n * If the user has 2FA enabled, the client app will receive a 401 with the userId and twoFaToken in the response.\n *\n * The app can pass the error response as a prop/argument, and we'll use it here to jump straight to the VERIFICATION_CODE step.\n */\n useEffect(() => {\n if (twoFaErrorResponse) {\n setTwoFaErrorResponse(twoFaErrorResponse)\n if (!VALID_STEPS_DURING_2FA_PROMPT.includes(step)) {\n setStep('VERIFICATION_CODE')\n }\n }\n // We do NOT want to rerun this effect on step change. It should only run once, when we get the error.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [twoFaErrorResponse])\n\n /**\n * Call the onTwoFactorAuthRequired callback when a twoFaToken is present\n */\n useEffect(() => {\n if (twoFaErrorResponse) {\n onTwoFactorAuthRequired(twoFaErrorResponse)\n }\n }, [onTwoFactorAuthRequired, twoFaErrorResponse])\n\n /* When the step changes, clear the old error message. */\n useEffect(() => {\n setErrorMessage(undefined)\n }, [step])\n\n async function finishLogin(loginResponse: LoginResponse) {\n await SynapseClient.setAccessTokenCookie(loginResponse.accessToken)\n localStorage.setItem(\n AUTHENTICATION_RECEIPT_LOCALSTORAGE_KEY,\n loginResponse.authenticationReceipt ?? '',\n )\n setStep('LOGGED_IN')\n\n if (sessionCallback) {\n sessionCallback()\n }\n }\n\n const {\n mutate: mutateLoginWithUsernameAndPassword,\n isPending: loginWithUsernameAndPasswordIsPending,\n } = useMutation<\n LoginResponse | TwoFactorAuthErrorResponse,\n SynapseClientError,\n { username: string; password: string; authenticationReceipt: string | null }\n >({\n mutationFn: ({ username, password, authenticationReceipt }) =>\n SynapseClient.login(username, password, authenticationReceipt),\n onError: error => {\n setErrorMessage(error.reason)\n const { errorResponse } = error\n if (\n errorResponse &&\n 'errorCode' in errorResponse &&\n errorResponse.errorCode ==\n ErrorResponseCode.PASSWORD_RESET_VIA_EMAIL_REQUIRED\n ) {\n window.location.assign(changePasswordUrl.toString())\n }\n },\n onSuccess: async loginResponse => {\n if (loginResponse) {\n if (instanceOfTwoFactorAuthErrorResponse(loginResponse)) {\n setStep('VERIFICATION_CODE')\n setTwoFaErrorResponse(loginResponse)\n } else {\n await finishLogin(loginResponse)\n }\n }\n },\n })\n\n const {\n mutate: mutateLoginWith2FACode,\n isPending: loginWith2FACodeIsPending,\n } = useMutation<LoginResponse, SynapseClientError, TwoFactorAuthLoginRequest>(\n {\n mutationFn: SynapseClient.loginWith2fa,\n onError: e => {\n setErrorMessage(e.reason)\n if (e.reason.includes('The provided code is invalid')) {\n // The most common cause of an invalid TOTP code is clock skew between the user's device and the server.\n setErrorMessage(`${e.reason} ${TOTP_CLOCK_SKEW_ERROR_APPENDAGE}`)\n } else if (\n // The twoFaToken wasn't transmitted correctly\n e.reason.includes('The provided twoFaToken is invalid') ||\n // The user waited too long to enter the code.\n e.reason.includes('Token has expired')\n ) {\n console.warn(e)\n // Instruct the user refresh to start over.\n setErrorMessage(\n 'Something went wrong. Refresh the page and try again.',\n )\n // If the 2FA token is in the search parameters, remove it so the user doesn't just get the same error again.\n if (window.location.href.includes('twoFaToken')) {\n window.history.replaceState(\n {},\n document.title,\n // using regex because SWC hashbang doesn't work with URLSearchParams\n window.location.href.replaceAll(\n /(twoFaToken|userId)=[^&]*&?/g,\n '',\n ),\n )\n }\n }\n },\n onSuccess: finishLogin,\n },\n )\n\n const {\n mutate: resetTwoFactorAuth,\n isSuccess: twoFactorAuthResetIsSuccess,\n isPending: twoFactorAuthResetIsPending,\n } = useResetTwoFactorAuth({\n onError: e => {\n setErrorMessage(e.reason)\n },\n })\n\n const submitUsernameAndPassword: UseLoginReturn['submitUsernameAndPassword'] =\n (username, password) => {\n setErrorMessage(undefined)\n const authenticationReceipt = localStorage.getItem(\n AUTHENTICATION_RECEIPT_LOCALSTORAGE_KEY,\n )\n mutateLoginWithUsernameAndPassword({\n username,\n password,\n authenticationReceipt,\n })\n }\n\n function verifyTwoFaErrorIsPresent(\n twoFaErrorResponse: TwoFactorAuthErrorResponse | null | undefined,\n ): twoFaErrorResponse is TwoFactorAuthErrorResponse {\n if (twoFaErrorResponse == null) {\n // This type of error could happen if the 2FA component exists on its own route, and the user directly navigates to it without first logging in with credentials/OAuth\n setErrorMessage(\n 'You did not first log in with your password or a third-party identity provider.',\n )\n return false\n }\n return true\n }\n\n const submitOneTimePassword: UseLoginReturn['submitOneTimePassword'] = (\n code,\n otpType = step === 'RECOVERY_CODE' ? 'RECOVERY_CODE' : 'TOTP',\n ) => {\n setErrorMessage(undefined)\n if (verifyTwoFaErrorIsPresent(twoFaErrorResponse)) {\n const request: TwoFactorAuthLoginRequest = {\n userId: twoFaErrorResponse.userId!,\n twoFaToken: twoFaErrorResponse.twoFaToken!,\n otpCode: code,\n otpType: otpType,\n }\n mutateLoginWith2FACode(request)\n }\n }\n\n const beginTwoFactorAuthReset: UseLoginReturn['beginTwoFactorAuthReset'] = (\n twoFaResetEndpoint: string,\n ) => {\n setErrorMessage(undefined)\n if (verifyTwoFaErrorIsPresent(twoFaErrorResponse)) {\n const request: TwoFactorAuthResetRequest = {\n userId: twoFaErrorResponse.userId!,\n twoFaToken: twoFaErrorResponse.twoFaToken,\n twoFaResetEndpoint: twoFaResetEndpoint,\n }\n resetTwoFactorAuth(request)\n }\n }\n\n return {\n step,\n onStepChange: setStep,\n submitUsernameAndPassword,\n submitOneTimePassword: submitOneTimePassword,\n errorMessage,\n loginIsPending:\n loginWithUsernameAndPasswordIsPending || loginWith2FACodeIsPending,\n beginTwoFactorAuthReset,\n twoFactorAuthResetIsPending,\n twoFactorAuthResetIsSuccess,\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"useMutuallyExclusiveState.js","names":[],"sources":["../../../src/utils/hooks/useMutuallyExclusiveState.ts"],"sourcesContent":["import { useMemo } from 'react'\nimport * as React from 'react'\n\n/**\n * Given a React state setter and a \"mutually exclusive\" setter, return a state setter that will always\n * set the mutually exclusive state value to `false` when the new state value is `true`.\n * @param setState\n * @param setMutuallyExclusiveState\n */\nfunction mutuallyExclusiveSetterFactory(\n setState: React.Dispatch<React.SetStateAction<boolean>>,\n setMutuallyExclusiveState: React.Dispatch<React.SetStateAction<boolean>>,\n): React.Dispatch<React.SetStateAction<boolean>> {\n return arg => {\n if (typeof arg === 'function') {\n setState(prev => {\n const next = arg(prev)\n if (next) {\n setMutuallyExclusiveState(false)\n }\n return next\n })\n } else {\n if (arg) {\n setState(true)\n setMutuallyExclusiveState(false)\n } else {\n setState(false)\n }\n }\n }\n}\n\n/**\n * Wrapper for two `useState` hooks where only one of the values can be true at a time.\n * @param initialValue1\n * @param initialValue2\n */\nexport function useMutuallyExclusiveState(\n initialValue1: boolean,\n initialValue2: boolean,\n): [\n value1: boolean,\n setValue1: React.Dispatch<React.SetStateAction<boolean>>,\n value2: boolean,\n setValue2: React.Dispatch<React.SetStateAction<boolean>>,\n] {\n if (initialValue1 && initialValue2) {\n throw new Error('initialValue1 and initialValue2 cannot both be true')\n }\n\n const [value1, _setValue1] = React.useState(initialValue1)\n const [value2, _setValue2] = React.useState(initialValue2)\n\n const setValue1 = useMemo(\n () => mutuallyExclusiveSetterFactory(_setValue1, _setValue2),\n [],\n )\n\n const setValue2 = useMemo(\n () => mutuallyExclusiveSetterFactory(_setValue2, _setValue1),\n [],\n )\n\n return [value1, setValue1, value2, setValue2]\n}\n\nexport default useMutuallyExclusiveState\n"],"mappings":";;;AASA,SAAS,EACP,GACA,GAC+C;AAC/C,SAAO,MAAO;AACZ,EAAI,OAAO,KAAQ,aACjB,GAAS,MAAQ;GACf,IAAM,IAAO,EAAI,EAAK;AAItB,UAHI,KACF,EAA0B,GAAM,EAE3B;IACP,GAEE,KACF,EAAS,GAAK,EACd,EAA0B,GAAM,IAEhC,EAAS,GAAM;;;AAWvB,SAAgB,EACd,GACA,GAMA;AACA,KAAI,KAAiB,EACnB,OAAU,MAAM,sDAAsD;CAGxE,IAAM,CAAC,GAAQ,KAAc,EAAM,SAAS,EAAc,EACpD,CAAC,GAAQ,KAAc,EAAM,SAAS,EAAc;AAY1D,QAAO;EAAC;EAVU,QACV,EAA+B,GAAY,EAAW,EAC5D,EAAE,CACH;EAO0B;EALT,QACV,EAA+B,GAAY,EAAW,EAC5D,EAAE,CACH;EAE4C"}
1
+ {"version":3,"file":"useMutuallyExclusiveState.js","names":[],"sources":["../../../src/utils/hooks/useMutuallyExclusiveState.ts"],"sourcesContent":["import { useMemo } from 'react'\nimport * as React from 'react'\n\n/**\n * Given a React state setter and a \"mutually exclusive\" setter, return a state setter that will always\n * set the mutually exclusive state value to `false` when the new state value is `true`.\n * @param setState\n * @param setMutuallyExclusiveState\n */\nfunction mutuallyExclusiveSetterFactory(\n setState: React.Dispatch<React.SetStateAction<boolean>>,\n setMutuallyExclusiveState: React.Dispatch<React.SetStateAction<boolean>>,\n): React.Dispatch<React.SetStateAction<boolean>> {\n return arg => {\n if (typeof arg === 'function') {\n setState(prev => {\n const next = arg(prev)\n if (next) {\n setMutuallyExclusiveState(false)\n }\n return next\n })\n } else {\n if (arg) {\n setState(true)\n setMutuallyExclusiveState(false)\n } else {\n setState(false)\n }\n }\n }\n}\n\n/**\n * Wrapper for two `useState` hooks where only one of the values can be true at a time.\n * @param initialValue1\n * @param initialValue2\n */\nexport function useMutuallyExclusiveState(\n initialValue1: boolean,\n initialValue2: boolean,\n): [\n value1: boolean,\n setValue1: React.Dispatch<React.SetStateAction<boolean>>,\n value2: boolean,\n setValue2: React.Dispatch<React.SetStateAction<boolean>>,\n] {\n if (initialValue1 && initialValue2) {\n throw new Error('initialValue1 and initialValue2 cannot both be true')\n }\n\n const [value1, _setValue1] = React.useState(initialValue1)\n const [value2, _setValue2] = React.useState(initialValue2)\n\n const setValue1 = useMemo(\n () => mutuallyExclusiveSetterFactory(_setValue1, _setValue2),\n [],\n )\n\n const setValue2 = useMemo(\n () => mutuallyExclusiveSetterFactory(_setValue2, _setValue1),\n [],\n )\n\n return [value1, setValue1, value2, setValue2]\n}\n\nexport default useMutuallyExclusiveState\n"],"mappings":";;;AASA,SAAS,EACP,GACA,GAC+C;AAC/C,SAAO,MAAO;AACZ,EAAI,OAAO,KAAQ,aACjB,GAAS,MAAQ;GACf,IAAM,IAAO,EAAI,EAAK;AAItB,UAHI,KACF,EAA0B,GAAM,EAE3B;IACP,GAEE,KACF,EAAS,GAAK,EACd,EAA0B,GAAM,IAEhC,EAAS,GAAM;;;AAWvB,SAAgB,EACd,GACA,GAMA;AACA,KAAI,KAAiB,EACnB,OAAU,MAAM,sDAAsD;CAGxE,IAAM,CAAC,GAAQ,KAAc,EAAM,SAAS,EAAc,EACpD,CAAC,GAAQ,KAAc,EAAM,SAAS,EAAc;AAY1D,QAAO;EAAC;EAVU,QACV,EAA+B,GAAY,EAAW,EAC5D,EAAE,CAQY;EAAW;EALT,QACV,EAA+B,GAAY,EAAW,EAC5D,EAAE,CAG+B;EAAU"}
@@ -1 +1 @@
1
- {"version":3,"file":"useOverlay.js","names":[],"sources":["../../../src/utils/hooks/useOverlay.tsx"],"sourcesContent":["import {\n ReactNode,\n RefObject,\n useCallback,\n useEffect,\n useRef,\n useState,\n} from 'react'\nimport { Fade, Paper, PaperProps, Popover, PopoverProps } from '@mui/material'\n\nfunction resetTimer(timer: NodeJS.Timeout | null) {\n if (timer) {\n clearTimeout(timer)\n }\n}\n\nconst DEFAULT_DELAY_SHOW_MS = 250\nconst DEFAULT_DELAY_HIDE_MS = 500\n\nconst defaultAnchorOrigin: PopoverProps['anchorOrigin'] = {\n vertical: 'top',\n horizontal: 'left',\n}\n\nconst defaultTransformOrigin: PopoverProps['transformOrigin'] = {\n vertical: 'bottom',\n horizontal: 'left',\n}\n\nexport function useOverlay(\n children: ReactNode,\n targetRef: RefObject<any>,\n delayShow = DEFAULT_DELAY_SHOW_MS,\n delayHide = DEFAULT_DELAY_HIDE_MS,\n paperProps?: PaperProps,\n) {\n const isMounted = useRef(false)\n const timer = useRef<NodeJS.Timeout | null>(null)\n const [isShowing, setIsShowing] = useState(false)\n\n useEffect(() => {\n isMounted.current = true\n return () => {\n isMounted.current = false\n }\n }, [])\n\n const toggle = useCallback(\n (show: boolean = isShowing, withDelay: boolean = true) => {\n resetTimer(timer.current)\n if (withDelay) {\n timer.current = setTimeout(\n () => {\n if (isMounted.current) {\n setIsShowing(show)\n }\n },\n show ? delayShow : delayHide,\n )\n } else {\n setIsShowing(show)\n }\n },\n [delayHide, delayShow, isShowing],\n )\n\n const toggleShow = useCallback(\n (withDelay: boolean = true) => {\n toggle(true, withDelay)\n },\n [toggle],\n )\n\n const toggleHide = useCallback(\n (withDelay: boolean = true) => {\n toggle(false, withDelay)\n },\n [toggle],\n )\n\n const OverlayComponent = useCallback(\n () => (\n <Popover\n anchorEl={targetRef.current}\n open={isShowing}\n anchorOrigin={defaultAnchorOrigin}\n transformOrigin={defaultTransformOrigin}\n sx={{ pointerEvents: 'none' }}\n slots={{\n transition: Fade,\n }}\n >\n <Paper\n {...paperProps}\n onMouseEnter={() => {\n toggle(true, false)\n }}\n onMouseLeave={() => {\n toggleHide(true)\n }}\n sx={{\n pointerEvents: 'auto',\n width: 'max-content',\n minWidth: '300px',\n ...paperProps?.sx,\n }}\n >\n {children}\n </Paper>\n </Popover>\n ),\n [children, isShowing, paperProps, targetRef, toggle, toggleHide],\n )\n\n return { OverlayComponent, isShowing, toggleShow, toggleHide, toggle }\n}\n"],"mappings":";;;;AAUA,SAAS,EAAW,GAA8B;AAChD,CAAI,KACF,aAAa,EAAM;;AAIvB,IAAM,IAAwB,KACxB,IAAwB,KAExB,IAAoD;CACxD,UAAU;CACV,YAAY;CACb,EAEK,IAA0D;CAC9D,UAAU;CACV,YAAY;CACb;AAED,SAAgB,EACd,GACA,GACA,IAAY,GACZ,IAAY,GACZ,GACA;CACA,IAAM,IAAY,EAAO,GAAM,EACzB,IAAQ,EAA8B,KAAK,EAC3C,CAAC,GAAW,KAAgB,EAAS,GAAM;AAEjD,UACE,EAAU,UAAU,UACP;AACX,IAAU,UAAU;KAErB,EAAE,CAAC;CAEN,IAAM,IAAS,GACZ,IAAgB,GAAW,IAAqB,OAAS;AAExD,EADA,EAAW,EAAM,QAAQ,EACrB,IACF,EAAM,UAAU,iBACR;AACJ,GAAI,EAAU,WACZ,EAAa,EAAK;KAGtB,IAAO,IAAY,EACpB,GAED,EAAa,EAAK;IAGtB;EAAC;EAAW;EAAW;EAAU,CAClC,EAEK,IAAa,GAChB,IAAqB,OAAS;AAC7B,IAAO,IAAM,EAAU;IAEzB,CAAC,EAAO,CACT,EAEK,IAAa,GAChB,IAAqB,OAAS;AAC7B,IAAO,IAAO,EAAU;IAE1B,CAAC,EAAO,CACT;AAoCD,QAAO;EAAE,kBAlCgB,QAErB,kBAAC,GAAD;GACE,UAAU,EAAU;GACpB,MAAM;GACN,cAAc;GACd,iBAAiB;GACjB,IAAI,EAAE,eAAe,QAAQ;GAC7B,OAAO,EACL,YAAY,GACb;aAED,kBAAC,GAAD;IACE,GAAI;IACJ,oBAAoB;AAClB,OAAO,IAAM,GAAM;;IAErB,oBAAoB;AAClB,OAAW,GAAK;;IAElB,IAAI;KACF,eAAe;KACf,OAAO;KACP,UAAU;KACV,GAAG,GAAY;KAChB;IAEA;IACK,CAAA;GACA,CAAA,EAEZ;GAAC;GAAU;GAAW;GAAY;GAAW;GAAQ;GAAW,CACjE;EAE0B;EAAW;EAAY;EAAY;EAAQ"}
1
+ {"version":3,"file":"useOverlay.js","names":[],"sources":["../../../src/utils/hooks/useOverlay.tsx"],"sourcesContent":["import {\n ReactNode,\n RefObject,\n useCallback,\n useEffect,\n useRef,\n useState,\n} from 'react'\nimport { Fade, Paper, PaperProps, Popover, PopoverProps } from '@mui/material'\n\nfunction resetTimer(timer: NodeJS.Timeout | null) {\n if (timer) {\n clearTimeout(timer)\n }\n}\n\nconst DEFAULT_DELAY_SHOW_MS = 250\nconst DEFAULT_DELAY_HIDE_MS = 500\n\nconst defaultAnchorOrigin: PopoverProps['anchorOrigin'] = {\n vertical: 'top',\n horizontal: 'left',\n}\n\nconst defaultTransformOrigin: PopoverProps['transformOrigin'] = {\n vertical: 'bottom',\n horizontal: 'left',\n}\n\nexport function useOverlay(\n children: ReactNode,\n targetRef: RefObject<any>,\n delayShow = DEFAULT_DELAY_SHOW_MS,\n delayHide = DEFAULT_DELAY_HIDE_MS,\n paperProps?: PaperProps,\n) {\n const isMounted = useRef(false)\n const timer = useRef<NodeJS.Timeout | null>(null)\n const [isShowing, setIsShowing] = useState(false)\n\n useEffect(() => {\n isMounted.current = true\n return () => {\n isMounted.current = false\n }\n }, [])\n\n const toggle = useCallback(\n (show: boolean = isShowing, withDelay: boolean = true) => {\n resetTimer(timer.current)\n if (withDelay) {\n timer.current = setTimeout(\n () => {\n if (isMounted.current) {\n setIsShowing(show)\n }\n },\n show ? delayShow : delayHide,\n )\n } else {\n setIsShowing(show)\n }\n },\n [delayHide, delayShow, isShowing],\n )\n\n const toggleShow = useCallback(\n (withDelay: boolean = true) => {\n toggle(true, withDelay)\n },\n [toggle],\n )\n\n const toggleHide = useCallback(\n (withDelay: boolean = true) => {\n toggle(false, withDelay)\n },\n [toggle],\n )\n\n const OverlayComponent = useCallback(\n () => (\n <Popover\n anchorEl={targetRef.current}\n open={isShowing}\n anchorOrigin={defaultAnchorOrigin}\n transformOrigin={defaultTransformOrigin}\n sx={{ pointerEvents: 'none' }}\n slots={{\n transition: Fade,\n }}\n >\n <Paper\n {...paperProps}\n onMouseEnter={() => {\n toggle(true, false)\n }}\n onMouseLeave={() => {\n toggleHide(true)\n }}\n sx={{\n pointerEvents: 'auto',\n width: 'max-content',\n minWidth: '300px',\n ...paperProps?.sx,\n }}\n >\n {children}\n </Paper>\n </Popover>\n ),\n [children, isShowing, paperProps, targetRef, toggle, toggleHide],\n )\n\n return { OverlayComponent, isShowing, toggleShow, toggleHide, toggle }\n}\n"],"mappings":";;;;AAUA,SAAS,EAAW,GAA8B;AAChD,CAAI,KACF,aAAa,EAAM;;AAIvB,IAAM,IAAwB,KACxB,IAAwB,KAExB,IAAoD;CACxD,UAAU;CACV,YAAY;CACb,EAEK,IAA0D;CAC9D,UAAU;CACV,YAAY;CACb;AAED,SAAgB,EACd,GACA,GACA,IAAY,GACZ,IAAY,GACZ,GACA;CACA,IAAM,IAAY,EAAO,GAAM,EACzB,IAAQ,EAA8B,KAAK,EAC3C,CAAC,GAAW,KAAgB,EAAS,GAAM;AAEjD,UACE,EAAU,UAAU,UACP;AACX,IAAU,UAAU;KAErB,EAAE,CAAC;CAEN,IAAM,IAAS,GACZ,IAAgB,GAAW,IAAqB,OAAS;AAExD,EADA,EAAW,EAAM,QAAQ,EACrB,IACF,EAAM,UAAU,iBACR;AACJ,GAAI,EAAU,WACZ,EAAa,EAAK;KAGtB,IAAO,IAAY,EACpB,GAED,EAAa,EAAK;IAGtB;EAAC;EAAW;EAAW;EAAU,CAClC,EAEK,IAAa,GAChB,IAAqB,OAAS;AAC7B,IAAO,IAAM,EAAU;IAEzB,CAAC,EAAO,CACT,EAEK,IAAa,GAChB,IAAqB,OAAS;AAC7B,IAAO,IAAO,EAAU;IAE1B,CAAC,EAAO,CACT;AAoCD,QAAO;EAAE,kBAlCgB,QAErB,kBAAC,GAAD;GACE,UAAU,EAAU;GACpB,MAAM;GACN,cAAc;GACd,iBAAiB;GACjB,IAAI,EAAE,eAAe,QAAQ;GAC7B,OAAO,EACL,YAAY,GACb;aAED,kBAAC,GAAD;IACE,GAAI;IACJ,oBAAoB;AAClB,OAAO,IAAM,GAAM;;IAErB,oBAAoB;AAClB,OAAW,GAAK;;IAElB,IAAI;KACF,eAAe;KACf,OAAO;KACP,UAAU;KACV,GAAG,GAAY;KAChB;IAEA;IACK,CAAA;GACA,CAAA,EAEZ;GAAC;GAAU;GAAW;GAAY;GAAW;GAAQ;GAAW,CAGzD;EAAkB;EAAW;EAAY;EAAY;EAAQ"}
@@ -1 +1 @@
1
- {"version":3,"file":"usePreFetchResource.js","names":[],"sources":["../../../src/utils/hooks/usePreFetchResource.ts"],"sourcesContent":["import { useEffect, useState } from 'react'\nimport { useQuery } from '@tanstack/react-query'\n\nexport async function fetchBlob(url: string): Promise<Blob> {\n const response = await fetch(url)\n return await response.blob()\n}\n\nexport function releaseResourceUrl(resourceUrl: string) {\n URL.revokeObjectURL(resourceUrl)\n}\n\n/**\n * Custom hook for retrieving a resource and assigning it a localhost URL. This is useful for\n * fetching resources from URLs that may expire before the resource renders.\n * @param preSignedURL\n * @returns a localhost URL referencing the prefetched resource\n */\nexport default function usePreFetchResource(\n preSignedURL?: string,\n): string | undefined {\n const { data: blob, error } = useQuery({\n queryKey: ['usePreFetchResource', preSignedURL],\n queryFn: () => fetchBlob(preSignedURL!),\n enabled: !!preSignedURL,\n // The URL may expire, so the fetched item should never be marked as 'stale'; a refetch may not work.\n staleTime: Infinity,\n })\n\n useEffect(() => {\n if (error) {\n console.error(\n `Failed to fetch object with presigned URL ${preSignedURL}. See network log for details`,\n )\n }\n }, [error, preSignedURL])\n\n // While the retrieved data is cached by react-query, the object created by URL.createObjectURL is not. More specifically,\n // the stored object is different for each instance of this hook, so when one instance tears down and deletes its object,\n // other instances of the hook are not affected.\n // This uses more memory than necessary in cases where the same object is fetched multiple times, but it's a tradeoff\n // for simple cache and resource management.\n return useCreateUrlForData(blob)\n}\n\n/**\n * Uses URL.createObjectURL to create a URL for the given blob. When this hook unmounts, the URL is released.\n * @param blob\n */\nexport function useCreateUrlForData(blob: Blob | null | undefined) {\n const [resourceUrl, setResourceURL] = useState<string | undefined>(undefined)\n\n useEffect(() => {\n if (blob) {\n const objectUrl = URL.createObjectURL(blob)\n setResourceURL(objectUrl)\n } else {\n setResourceURL(undefined)\n }\n }, [blob])\n\n useEffect(() => {\n return () => {\n // When we no longer need the object, we release it.\n // See https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL\n if (resourceUrl) {\n releaseResourceUrl(resourceUrl)\n }\n }\n }, [resourceUrl])\n\n return resourceUrl\n}\n"],"mappings":";;;AAGA,eAAsB,EAAU,GAA4B;AAE1D,QAAO,OADU,MAAM,MAAM,EAAI,EACX,MAAM;;AAG9B,SAAgB,EAAmB,GAAqB;AACtD,KAAI,gBAAgB,EAAY;;AASlC,SAAwB,EACtB,GACoB;CACpB,IAAM,EAAE,MAAM,GAAM,aAAU,EAAS;EACrC,UAAU,CAAC,uBAAuB,EAAa;EAC/C,eAAe,EAAU,EAAc;EACvC,SAAS,CAAC,CAAC;EAEX,WAAW;EACZ,CAAC;AAeF,QAbA,QAAgB;AACd,EAAI,KACF,QAAQ,MACN,6CAA6C,EAAa,+BAC3D;IAEF,CAAC,GAAO,EAAa,CAAC,EAOlB,EAAoB,EAAK;;AAOlC,SAAgB,EAAoB,GAA+B;CACjE,IAAM,CAAC,GAAa,KAAkB,EAA6B,KAAA,EAAU;AAqB7E,QAnBA,QAAgB;AACd,EAEE,EAFE,IACgB,IAAI,gBAAgB,EAAK,GAG5B,KAAA,EAAU;IAE1B,CAAC,EAAK,CAAC,EAEV,cACe;AAGX,EAAI,KACF,EAAmB,EAAY;IAGlC,CAAC,EAAY,CAAC,EAEV"}
1
+ {"version":3,"file":"usePreFetchResource.js","names":[],"sources":["../../../src/utils/hooks/usePreFetchResource.ts"],"sourcesContent":["import { useEffect, useState } from 'react'\nimport { useQuery } from '@tanstack/react-query'\n\nexport async function fetchBlob(url: string): Promise<Blob> {\n const response = await fetch(url)\n return await response.blob()\n}\n\nexport function releaseResourceUrl(resourceUrl: string) {\n URL.revokeObjectURL(resourceUrl)\n}\n\n/**\n * Custom hook for retrieving a resource and assigning it a localhost URL. This is useful for\n * fetching resources from URLs that may expire before the resource renders.\n * @param preSignedURL\n * @returns a localhost URL referencing the prefetched resource\n */\nexport default function usePreFetchResource(\n preSignedURL?: string,\n): string | undefined {\n const { data: blob, error } = useQuery({\n queryKey: ['usePreFetchResource', preSignedURL],\n queryFn: () => fetchBlob(preSignedURL!),\n enabled: !!preSignedURL,\n // The URL may expire, so the fetched item should never be marked as 'stale'; a refetch may not work.\n staleTime: Infinity,\n })\n\n useEffect(() => {\n if (error) {\n console.error(\n `Failed to fetch object with presigned URL ${preSignedURL}. See network log for details`,\n )\n }\n }, [error, preSignedURL])\n\n // While the retrieved data is cached by react-query, the object created by URL.createObjectURL is not. More specifically,\n // the stored object is different for each instance of this hook, so when one instance tears down and deletes its object,\n // other instances of the hook are not affected.\n // This uses more memory than necessary in cases where the same object is fetched multiple times, but it's a tradeoff\n // for simple cache and resource management.\n return useCreateUrlForData(blob)\n}\n\n/**\n * Uses URL.createObjectURL to create a URL for the given blob. When this hook unmounts, the URL is released.\n * @param blob\n */\nexport function useCreateUrlForData(blob: Blob | null | undefined) {\n const [resourceUrl, setResourceURL] = useState<string | undefined>(undefined)\n\n useEffect(() => {\n if (blob) {\n const objectUrl = URL.createObjectURL(blob)\n setResourceURL(objectUrl)\n } else {\n setResourceURL(undefined)\n }\n }, [blob])\n\n useEffect(() => {\n return () => {\n // When we no longer need the object, we release it.\n // See https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL\n if (resourceUrl) {\n releaseResourceUrl(resourceUrl)\n }\n }\n }, [resourceUrl])\n\n return resourceUrl\n}\n"],"mappings":";;;AAGA,eAAsB,EAAU,GAA4B;AAE1D,QAAO,OAAM,MADU,MAAM,EAAI,EACX,MAAM;;AAG9B,SAAgB,EAAmB,GAAqB;AACtD,KAAI,gBAAgB,EAAY;;AASlC,SAAwB,EACtB,GACoB;CACpB,IAAM,EAAE,MAAM,GAAM,aAAU,EAAS;EACrC,UAAU,CAAC,uBAAuB,EAAa;EAC/C,eAAe,EAAU,EAAc;EACvC,SAAS,CAAC,CAAC;EAEX,WAAW;EACZ,CAAC;AAeF,QAbA,QAAgB;AACd,EAAI,KACF,QAAQ,MACN,6CAA6C,EAAa,+BAC3D;IAEF,CAAC,GAAO,EAAa,CAAC,EAOlB,EAAoB,EAAK;;AAOlC,SAAgB,EAAoB,GAA+B;CACjE,IAAM,CAAC,GAAa,KAAkB,EAA6B,KAAA,EAAU;AAqB7E,QAnBA,QAAgB;AACd,EAEE,EAFE,IACgB,IAAI,gBAAgB,EACvB,GAEA,KAAA,EAAU;IAE1B,CAAC,EAAK,CAAC,EAEV,cACe;AAGX,EAAI,KACF,EAAmB,EAAY;IAGlC,CAAC,EAAY,CAAC,EAEV"}
@@ -1 +1 @@
1
- {"version":3,"file":"useQuerySearchParam.js","names":[],"sources":["../../../src/utils/hooks/useQuerySearchParam.ts"],"sourcesContent":["import { useLocation } from 'react-router'\n\n/**\n * If defined, get the query search parameter value for the given parameter name\n * @param queryParamName\n * @returns\n */\nexport function useQuerySearchParam(\n queryParamName: string,\n): string | undefined {\n const search = new URLSearchParams(useLocation().search)\n const value = search.get(queryParamName)\n return value ?? undefined\n}\n"],"mappings":";;AAOA,SAAgB,EACd,GACoB;AAGpB,QAFe,IAAI,gBAAgB,GAAa,CAAC,OAAO,CACnC,IAAI,EAAe,IACxB,KAAA"}
1
+ {"version":3,"file":"useQuerySearchParam.js","names":[],"sources":["../../../src/utils/hooks/useQuerySearchParam.ts"],"sourcesContent":["import { useLocation } from 'react-router'\n\n/**\n * If defined, get the query search parameter value for the given parameter name\n * @param queryParamName\n * @returns\n */\nexport function useQuerySearchParam(\n queryParamName: string,\n): string | undefined {\n const search = new URLSearchParams(useLocation().search)\n const value = search.get(queryParamName)\n return value ?? undefined\n}\n"],"mappings":";;AAOA,SAAgB,EACd,GACoB;AAGpB,QADc,IADK,gBAAgB,GAAa,CAAC,OACnC,CAAO,IAAI,EAClB,IAAS,KAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"useScrollFadeTransition.js","names":[],"sources":["../../../src/utils/hooks/useScrollFadeTransition.ts"],"sourcesContent":["import { useState, useEffect, useRef } from 'react'\n\nconst calculateOpacity = (rect: DOMRect): number => {\n const viewportHeight = window.innerHeight\n const elementCenterY = rect.top + rect.height / 2\n const middleRangeStart = viewportHeight * 0.4\n const middleRangeEnd = viewportHeight * 0.6\n\n if (elementCenterY <= middleRangeEnd) {\n // && elementCenterY >= middleRangeStart) {\n // center of element is within the viewport bounds that I want to show full opacity\n return 1\n } else {\n //otherwise, subtract the % (of the center of the element to one of the boundaries)\n const distanceToMiddle = Math.min(\n Math.abs(elementCenterY - middleRangeStart),\n Math.abs(elementCenterY - middleRangeEnd),\n )\n const maxDistance = viewportHeight / 4\n return Math.max(0, 1 - distanceToMiddle / maxDistance)\n }\n}\n/**\n * Use this hook if you want to change the opacity of an element dynamically based on where an element is in the current viewport.\n * Opacity will be 1 when the element is in the middle of the viewport, and 0 when it is outside of the viewport.\n * @returns\n */\nexport function useScrollFadeTransition() {\n const ref = useRef(null)\n // opacity of the desktop image\n const [opacity, setOpacity] = useState(1)\n\n // listen to the scroll event, and calculate the opacity based on\n useEffect(() => {\n const handleScroll = () => {\n const rect = (ref.current as any).getBoundingClientRect()\n setOpacity(calculateOpacity(rect))\n }\n\n if (ref) {\n window.addEventListener('scroll', handleScroll)\n handleScroll()\n }\n return () => window.removeEventListener('scroll', handleScroll)\n }, [ref])\n return { ref, opacity }\n}\n"],"mappings":";;AAEA,IAAM,KAAoB,MAA0B;CAClD,IAAM,IAAiB,OAAO,aACxB,IAAiB,EAAK,MAAM,EAAK,SAAS,GAC1C,IAAmB,IAAiB,IACpC,IAAiB,IAAiB;AAExC,KAAI,KAAkB,EAGpB,QAAO;CACF;EAEL,IAAM,IAAmB,KAAK,IAC5B,KAAK,IAAI,IAAiB,EAAiB,EAC3C,KAAK,IAAI,IAAiB,EAAe,CAC1C,EACK,IAAc,IAAiB;AACrC,SAAO,KAAK,IAAI,GAAG,IAAI,IAAmB,EAAY;;;AAQ1D,SAAgB,IAA0B;CACxC,IAAM,IAAM,EAAO,KAAK,EAElB,CAAC,GAAS,KAAc,EAAS,EAAE;AAezC,QAZA,QAAgB;EACd,IAAM,UAAqB;AAEzB,KAAW,EADG,EAAI,QAAgB,uBAAuB,CACxB,CAAC;;AAOpC,SAJI,MACF,OAAO,iBAAiB,UAAU,EAAa,EAC/C,GAAc,SAEH,OAAO,oBAAoB,UAAU,EAAa;IAC9D,CAAC,EAAI,CAAC,EACF;EAAE;EAAK;EAAS"}
1
+ {"version":3,"file":"useScrollFadeTransition.js","names":[],"sources":["../../../src/utils/hooks/useScrollFadeTransition.ts"],"sourcesContent":["import { useState, useEffect, useRef } from 'react'\n\nconst calculateOpacity = (rect: DOMRect): number => {\n const viewportHeight = window.innerHeight\n const elementCenterY = rect.top + rect.height / 2\n const middleRangeStart = viewportHeight * 0.4\n const middleRangeEnd = viewportHeight * 0.6\n\n if (elementCenterY <= middleRangeEnd) {\n // && elementCenterY >= middleRangeStart) {\n // center of element is within the viewport bounds that I want to show full opacity\n return 1\n } else {\n //otherwise, subtract the % (of the center of the element to one of the boundaries)\n const distanceToMiddle = Math.min(\n Math.abs(elementCenterY - middleRangeStart),\n Math.abs(elementCenterY - middleRangeEnd),\n )\n const maxDistance = viewportHeight / 4\n return Math.max(0, 1 - distanceToMiddle / maxDistance)\n }\n}\n/**\n * Use this hook if you want to change the opacity of an element dynamically based on where an element is in the current viewport.\n * Opacity will be 1 when the element is in the middle of the viewport, and 0 when it is outside of the viewport.\n * @returns\n */\nexport function useScrollFadeTransition() {\n const ref = useRef(null)\n // opacity of the desktop image\n const [opacity, setOpacity] = useState(1)\n\n // listen to the scroll event, and calculate the opacity based on\n useEffect(() => {\n const handleScroll = () => {\n const rect = (ref.current as any).getBoundingClientRect()\n setOpacity(calculateOpacity(rect))\n }\n\n if (ref) {\n window.addEventListener('scroll', handleScroll)\n handleScroll()\n }\n return () => window.removeEventListener('scroll', handleScroll)\n }, [ref])\n return { ref, opacity }\n}\n"],"mappings":";;AAEA,IAAM,KAAoB,MAA0B;CAClD,IAAM,IAAiB,OAAO,aACxB,IAAiB,EAAK,MAAM,EAAK,SAAS,GAC1C,IAAmB,IAAiB,IACpC,IAAiB,IAAiB;AAExC,KAAI,KAAkB,EAGpB,QAAO;CACF;EAEL,IAAM,IAAmB,KAAK,IAC5B,KAAK,IAAI,IAAiB,EAAiB,EAC3C,KAAK,IAAI,IAAiB,EAAe,CAC1C,EACK,IAAc,IAAiB;AACrC,SAAO,KAAK,IAAI,GAAG,IAAI,IAAmB,EAAY;;;AAQ1D,SAAgB,IAA0B;CACxC,IAAM,IAAM,EAAO,KAAK,EAElB,CAAC,GAAS,KAAc,EAAS,EAAE;AAezC,QAZA,QAAgB;EACd,IAAM,UAAqB;AAEzB,KAAW,EADG,EAAI,QAAgB,uBACN,CAAK,CAAC;;AAOpC,SAJI,MACF,OAAO,iBAAiB,UAAU,EAAa,EAC/C,GAAc,SAEH,OAAO,oBAAoB,UAAU,EAAa;IAC9D,CAAC,EAAI,CAAC,EACF;EAAE;EAAK;EAAS"}
@@ -1 +1 @@
1
- {"version":3,"file":"useSet.js","names":[],"sources":["../../../src/utils/hooks/useSet.ts"],"sourcesContent":["import { useCallback, useState } from 'react'\n\nexport interface UseSetReturn<T> {\n set: Omit<Set<T>, 'add' | 'delete' | 'clear'>\n add: (...items: T[]) => void\n remove: (item: T) => void\n clear: () => void\n}\n\n/**\n * Hook for storing an ES6 set in React state. Methods that mutate the set have been hidden via TypeScript,\n * so if you adhere to the type definitions, the set is essentially immutable.\n * Modifications to the set should only be done via the functions returned by the hook, which will create a new object to trigger rerendering.\n * @param initialState\n * @returns\n */\nexport function useSet<T>(initialState?: T[] | Set<T>): UseSetReturn<T> {\n const [set, setSet] = useState(new Set(initialState))\n\n const add = useCallback(\n (...items: T[]) => {\n const newSet = new Set(set)\n for (const item of items) {\n newSet.add(item)\n }\n setSet(newSet)\n },\n [set],\n )\n\n const remove = useCallback(\n (item: T) => {\n const newSet = new Set(set)\n newSet.delete(item)\n setSet(newSet)\n },\n [set],\n )\n\n const clear = useCallback(() => {\n setSet(new Set())\n }, [])\n\n return {\n set,\n add,\n remove,\n clear,\n }\n}\n"],"mappings":";;AAgBA,SAAgB,EAAU,GAA8C;CACtE,IAAM,CAAC,GAAK,KAAU,EAAS,IAAI,IAAI,EAAa,CAAC;AA0BrD,QAAO;EACL;EACA,KA1BU,GACT,GAAG,MAAe;GACjB,IAAM,IAAS,IAAI,IAAI,EAAI;AAC3B,QAAK,IAAM,KAAQ,EACjB,GAAO,IAAI,EAAK;AAElB,KAAO,EAAO;KAEhB,CAAC,EAAI,CACN;EAkBC,QAhBa,GACZ,MAAY;GACX,IAAM,IAAS,IAAI,IAAI,EAAI;AAE3B,GADA,EAAO,OAAO,EAAK,EACnB,EAAO,EAAO;KAEhB,CAAC,EAAI,CACN;EAUC,OARY,QAAkB;AAC9B,qBAAO,IAAI,KAAK,CAAC;KAChB,EAAE,CAAC;EAOL"}
1
+ {"version":3,"file":"useSet.js","names":[],"sources":["../../../src/utils/hooks/useSet.ts"],"sourcesContent":["import { useCallback, useState } from 'react'\n\nexport interface UseSetReturn<T> {\n set: Omit<Set<T>, 'add' | 'delete' | 'clear'>\n add: (...items: T[]) => void\n remove: (item: T) => void\n clear: () => void\n}\n\n/**\n * Hook for storing an ES6 set in React state. Methods that mutate the set have been hidden via TypeScript,\n * so if you adhere to the type definitions, the set is essentially immutable.\n * Modifications to the set should only be done via the functions returned by the hook, which will create a new object to trigger rerendering.\n * @param initialState\n * @returns\n */\nexport function useSet<T>(initialState?: T[] | Set<T>): UseSetReturn<T> {\n const [set, setSet] = useState(new Set(initialState))\n\n const add = useCallback(\n (...items: T[]) => {\n const newSet = new Set(set)\n for (const item of items) {\n newSet.add(item)\n }\n setSet(newSet)\n },\n [set],\n )\n\n const remove = useCallback(\n (item: T) => {\n const newSet = new Set(set)\n newSet.delete(item)\n setSet(newSet)\n },\n [set],\n )\n\n const clear = useCallback(() => {\n setSet(new Set())\n }, [])\n\n return {\n set,\n add,\n remove,\n clear,\n }\n}\n"],"mappings":";;AAgBA,SAAgB,EAAU,GAA8C;CACtE,IAAM,CAAC,GAAK,KAAU,EAAS,IAAI,IAAI,EAAa,CAAC;AA0BrD,QAAO;EACL;EACA,KA1BU,GACT,GAAG,MAAe;GACjB,IAAM,IAAS,IAAI,IAAI,EAAI;AAC3B,QAAK,IAAM,KAAQ,EACjB,GAAO,IAAI,EAAK;AAElB,KAAO,EAAO;KAEhB,CAAC,EAAI,CAkBL;EACA,QAhBa,GACZ,MAAY;GACX,IAAM,IAAS,IAAI,IAAI,EAAI;AAE3B,GADA,EAAO,OAAO,EAAK,EACnB,EAAO,EAAO;KAEhB,CAAC,EAAI,CAUL;EACA,OARY,QAAkB;AAC9B,qBAAO,IAAI,KAAK,CAAC;KAChB,EAAE,CAMH;EACD"}
@@ -1 +1 @@
1
- {"version":3,"file":"useSourceAppConfigs.js","names":[],"sources":["../../../src/utils/hooks/useSourceAppConfigs.tsx"],"sourcesContent":["import SourceAppImage from '@/components/SourceAppImage'\nimport SynapseClient from '@/synapse-client'\nimport Palettes from '@/theme/palette/Palettes'\nimport { Realm } from '@sage-bionetworks/synapse-client'\nimport { PaletteOptions } from '@mui/material'\nimport { ReactNode, useMemo } from 'react'\nimport { BUNDLE_MASK_QUERY_RESULTS } from '../SynapseConstants'\nimport { QueryFilter } from '@sage-bionetworks/synapse-types'\nimport { useQueries, useSuspenseQuery } from '@tanstack/react-query'\nimport { useSynapseContext } from '../context/SynapseContext'\nimport { tableQueryUseQueryDefaults } from '@/synapse-queries'\nimport { getRealmByIdQueryOptions } from '@/synapse-queries/realm/useRealmPrincipals'\n\nexport type SourceAppConfig = {\n appId: string // app ID used in the query params\n appURL: string // URL that points to the production version of this app\n friendlyName: string\n logo: ReactNode\n palette: PaletteOptions\n description: string\n requestAffiliation: boolean // If set to true, a special screen is shown requesting the user to fill out UserProfile.company\n isPublicized: boolean // If set to true, this will be included in the list of the available Sage Resources\n shortDescription: string\n defaultRealm: Realm\n area?: string\n}\n\n// A static SourceAppConfig to use as a fallback in case the request to get source app configs fails\nexport const STATIC_SOURCE_APP_CONFIG: SourceAppConfig = {\n appId: 'synapse.org',\n appURL: '',\n description: '',\n friendlyName: 'Synapse',\n requestAffiliation: false,\n logo: <></>,\n isPublicized: true,\n palette: { ...Palettes.palette },\n shortDescription: '',\n defaultRealm: {\n id: '0',\n name: 'Synapse',\n },\n}\n\n// Stable array reference to prevent infinite re-renders\nconst FALLBACK_SOURCE_APP_CONFIGS = [STATIC_SOURCE_APP_CONFIG]\n\ntype SourceAppConfigWithRealmId = Omit<SourceAppConfig, 'defaultRealm'> & {\n realmId: string\n}\n\nexport const useSourceAppConfigs = (\n sourceAppConfigTableID: string,\n additionalFilters?: QueryFilter[],\n): SourceAppConfig[] | undefined => {\n const { synapseClient, keyFactory } = useSynapseContext()\n\n const queryBundleRequest = {\n entityId: sourceAppConfigTableID,\n query: {\n sql: `SELECT * FROM ${sourceAppConfigTableID}`,\n limit: 75,\n additionalFilters,\n },\n partMask: BUNDLE_MASK_QUERY_RESULTS,\n concreteType:\n 'org.sagebionetworks.repo.model.table.QueryBundleRequest' as const,\n }\n\n const { data: tableQueryResult } = useSuspenseQuery({\n ...tableQueryUseQueryDefaults,\n queryKey: keyFactory.getEntityTableQueryResultWithAsyncStatusQueryKey(\n queryBundleRequest,\n false,\n ),\n queryFn: async () => {\n try {\n return await SynapseClient.getQueryTableAsyncJobResults(\n queryBundleRequest,\n undefined, // use an undefined access token to query a table in the default realm\n )\n } catch (_error) {\n // Return null on error to signal fallback behavior\n return null\n }\n },\n })\n\n const rowSet = tableQueryResult?.responseBody?.queryResult?.queryResults\n\n // transform row data to SourceAppConfigWithRealmId[]\n const headers = rowSet?.headers\n const appIdColIndex = headers?.findIndex(\n selectColumn => selectColumn.name == 'appId',\n )!\n const appURLColIndex = headers?.findIndex(\n selectColumn => selectColumn.name == 'appURL',\n )!\n const friendlyNameColIndex = headers?.findIndex(\n selectColumn => selectColumn.name == 'friendlyName',\n )!\n const descriptionColIndex = headers?.findIndex(\n selectColumn => selectColumn.name == 'description',\n )!\n const logoFileHandleColIndex = headers?.findIndex(\n selectColumn => selectColumn.name == 'logo',\n )!\n const requestAffiliationColIndex = headers?.findIndex(\n selectColumn => selectColumn.name == 'requestAffiliation',\n )!\n const primaryColorColIndex = headers?.findIndex(\n selectColumn => selectColumn.name == 'primaryColor',\n )!\n const secondaryColorColIndex = headers?.findIndex(\n selectColumn => selectColumn.name == 'secondaryColor',\n )!\n const isPublicizedColIndex = headers?.findIndex(\n selectColumn => selectColumn.name == 'isPublicized',\n )!\n const shortDescriptionColIndex = headers?.findIndex(\n selectColumn => selectColumn.name == 'shortDescription',\n )!\n const realmIdColIndex = headers?.findIndex(\n selectColumn => selectColumn.name == 'realmId',\n )!\n const areaColIndex =\n headers?.findIndex(selectColumn => selectColumn.name == 'area') ?? -1\n\n const rows = rowSet?.rows\n\n // Extract configs with realmIds first\n const configsWithRealmId: SourceAppConfigWithRealmId[] | undefined = useMemo(\n () =>\n rows?.map(row => {\n const rowVals = row.values\n const fileHandleId = rowVals[logoFileHandleColIndex]\n const friendlyName = rowVals[friendlyNameColIndex] ?? ''\n const logo = (\n <SourceAppImage\n sourceAppConfigTableID={sourceAppConfigTableID}\n fileHandleId={fileHandleId}\n friendlyName={friendlyName}\n />\n )\n const appPalette: PaletteOptions = {\n ...Palettes.palette,\n primary: Palettes.generatePalette(\n rowVals[primaryColorColIndex] ?? '',\n ),\n secondary: Palettes.generatePalette(\n rowVals[secondaryColorColIndex] ?? '',\n ),\n }\n const appId =\n rowVals[appIdColIndex] == null ? '' : rowVals[appIdColIndex]\n const appURL =\n rowVals[appURLColIndex] == null ? '' : rowVals[appURLColIndex]\n const description =\n rowVals[descriptionColIndex] == null\n ? ''\n : rowVals[descriptionColIndex]\n const shortDescription =\n rowVals[shortDescriptionColIndex] == null\n ? ''\n : rowVals[shortDescriptionColIndex]\n const requestAffiliation =\n rowVals[requestAffiliationColIndex] == null\n ? false\n : rowVals[requestAffiliationColIndex] == 'true'\n const isPublicized =\n rowVals[isPublicizedColIndex] == null\n ? true\n : rowVals[isPublicizedColIndex] == 'true'\n const realmId =\n rowVals[realmIdColIndex] == null ? '0' : rowVals[realmIdColIndex]\n const area =\n areaColIndex >= 0 && rowVals[areaColIndex] != null\n ? rowVals[areaColIndex]\n : undefined\n return {\n appId,\n appURL,\n description,\n friendlyName,\n requestAffiliation,\n logo,\n isPublicized,\n palette: appPalette,\n shortDescription,\n realmId,\n area,\n }\n }),\n [\n rows,\n logoFileHandleColIndex,\n friendlyNameColIndex,\n sourceAppConfigTableID,\n primaryColorColIndex,\n secondaryColorColIndex,\n appIdColIndex,\n appURLColIndex,\n descriptionColIndex,\n shortDescriptionColIndex,\n requestAffiliationColIndex,\n isPublicizedColIndex,\n realmIdColIndex,\n areaColIndex,\n ],\n )\n\n // Fetch all realms in parallel\n const realmQueries = useQueries({\n queries:\n configsWithRealmId?.map(config =>\n getRealmByIdQueryOptions(config.realmId, keyFactory, synapseClient),\n ) ?? [],\n })\n\n // Combine configs with realm data\n return useMemo(() => {\n // If query returned null (error case) or no rowSet, return static config fallback\n if (!tableQueryResult || !rowSet) {\n return FALLBACK_SOURCE_APP_CONFIGS\n }\n\n if (!configsWithRealmId) return undefined\n\n // Check if all realm queries have succeeded\n const allRealmsLoaded = realmQueries.every(query => query.isSuccess)\n if (!allRealmsLoaded) return undefined\n\n return configsWithRealmId.map((config, index) => {\n const realm = realmQueries[index].data\n return {\n ...config,\n defaultRealm: realm,\n }\n })\n }, [configsWithRealmId, realmQueries, tableQueryResult, rowSet])\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"useSourceAppConfigs.js","names":[],"sources":["../../../src/utils/hooks/useSourceAppConfigs.tsx"],"sourcesContent":["import SourceAppImage from '@/components/SourceAppImage'\nimport SynapseClient from '@/synapse-client'\nimport Palettes from '@/theme/palette/Palettes'\nimport { Realm } from '@sage-bionetworks/synapse-client'\nimport { PaletteOptions } from '@mui/material'\nimport { ReactNode, useMemo } from 'react'\nimport { BUNDLE_MASK_QUERY_RESULTS } from '../SynapseConstants'\nimport { QueryFilter } from '@sage-bionetworks/synapse-types'\nimport { useQueries, useSuspenseQuery } from '@tanstack/react-query'\nimport { useSynapseContext } from '../context/SynapseContext'\nimport { tableQueryUseQueryDefaults } from '@/synapse-queries'\nimport { getRealmByIdQueryOptions } from '@/synapse-queries/realm/useRealmPrincipals'\n\nexport type SourceAppConfig = {\n appId: string // app ID used in the query params\n appURL: string // URL that points to the production version of this app\n friendlyName: string\n logo: ReactNode\n palette: PaletteOptions\n description: string\n requestAffiliation: boolean // If set to true, a special screen is shown requesting the user to fill out UserProfile.company\n isPublicized: boolean // If set to true, this will be included in the list of the available Sage Resources\n shortDescription: string\n defaultRealm: Realm\n area?: string\n}\n\n// A static SourceAppConfig to use as a fallback in case the request to get source app configs fails\nexport const STATIC_SOURCE_APP_CONFIG: SourceAppConfig = {\n appId: 'synapse.org',\n appURL: '',\n description: '',\n friendlyName: 'Synapse',\n requestAffiliation: false,\n logo: <></>,\n isPublicized: true,\n palette: { ...Palettes.palette },\n shortDescription: '',\n defaultRealm: {\n id: '0',\n name: 'Synapse',\n },\n}\n\n// Stable array reference to prevent infinite re-renders\nconst FALLBACK_SOURCE_APP_CONFIGS = [STATIC_SOURCE_APP_CONFIG]\n\ntype SourceAppConfigWithRealmId = Omit<SourceAppConfig, 'defaultRealm'> & {\n realmId: string\n}\n\nexport const useSourceAppConfigs = (\n sourceAppConfigTableID: string,\n additionalFilters?: QueryFilter[],\n): SourceAppConfig[] | undefined => {\n const { synapseClient, keyFactory } = useSynapseContext()\n\n const queryBundleRequest = {\n entityId: sourceAppConfigTableID,\n query: {\n sql: `SELECT * FROM ${sourceAppConfigTableID}`,\n limit: 75,\n additionalFilters,\n },\n partMask: BUNDLE_MASK_QUERY_RESULTS,\n concreteType:\n 'org.sagebionetworks.repo.model.table.QueryBundleRequest' as const,\n }\n\n const { data: tableQueryResult } = useSuspenseQuery({\n ...tableQueryUseQueryDefaults,\n queryKey: keyFactory.getEntityTableQueryResultWithAsyncStatusQueryKey(\n queryBundleRequest,\n false,\n ),\n queryFn: async () => {\n try {\n return await SynapseClient.getQueryTableAsyncJobResults(\n queryBundleRequest,\n undefined, // use an undefined access token to query a table in the default realm\n )\n } catch (_error) {\n // Return null on error to signal fallback behavior\n return null\n }\n },\n })\n\n const rowSet = tableQueryResult?.responseBody?.queryResult?.queryResults\n\n // transform row data to SourceAppConfigWithRealmId[]\n const headers = rowSet?.headers\n const appIdColIndex = headers?.findIndex(\n selectColumn => selectColumn.name == 'appId',\n )!\n const appURLColIndex = headers?.findIndex(\n selectColumn => selectColumn.name == 'appURL',\n )!\n const friendlyNameColIndex = headers?.findIndex(\n selectColumn => selectColumn.name == 'friendlyName',\n )!\n const descriptionColIndex = headers?.findIndex(\n selectColumn => selectColumn.name == 'description',\n )!\n const logoFileHandleColIndex = headers?.findIndex(\n selectColumn => selectColumn.name == 'logo',\n )!\n const requestAffiliationColIndex = headers?.findIndex(\n selectColumn => selectColumn.name == 'requestAffiliation',\n )!\n const primaryColorColIndex = headers?.findIndex(\n selectColumn => selectColumn.name == 'primaryColor',\n )!\n const secondaryColorColIndex = headers?.findIndex(\n selectColumn => selectColumn.name == 'secondaryColor',\n )!\n const isPublicizedColIndex = headers?.findIndex(\n selectColumn => selectColumn.name == 'isPublicized',\n )!\n const shortDescriptionColIndex = headers?.findIndex(\n selectColumn => selectColumn.name == 'shortDescription',\n )!\n const realmIdColIndex = headers?.findIndex(\n selectColumn => selectColumn.name == 'realmId',\n )!\n const areaColIndex =\n headers?.findIndex(selectColumn => selectColumn.name == 'area') ?? -1\n\n const rows = rowSet?.rows\n\n // Extract configs with realmIds first\n const configsWithRealmId: SourceAppConfigWithRealmId[] | undefined = useMemo(\n () =>\n rows?.map(row => {\n const rowVals = row.values\n const fileHandleId = rowVals[logoFileHandleColIndex]\n const friendlyName = rowVals[friendlyNameColIndex] ?? ''\n const logo = (\n <SourceAppImage\n sourceAppConfigTableID={sourceAppConfigTableID}\n fileHandleId={fileHandleId}\n friendlyName={friendlyName}\n />\n )\n const appPalette: PaletteOptions = {\n ...Palettes.palette,\n primary: Palettes.generatePalette(\n rowVals[primaryColorColIndex] ?? '',\n ),\n secondary: Palettes.generatePalette(\n rowVals[secondaryColorColIndex] ?? '',\n ),\n }\n const appId =\n rowVals[appIdColIndex] == null ? '' : rowVals[appIdColIndex]\n const appURL =\n rowVals[appURLColIndex] == null ? '' : rowVals[appURLColIndex]\n const description =\n rowVals[descriptionColIndex] == null\n ? ''\n : rowVals[descriptionColIndex]\n const shortDescription =\n rowVals[shortDescriptionColIndex] == null\n ? ''\n : rowVals[shortDescriptionColIndex]\n const requestAffiliation =\n rowVals[requestAffiliationColIndex] == null\n ? false\n : rowVals[requestAffiliationColIndex] == 'true'\n const isPublicized =\n rowVals[isPublicizedColIndex] == null\n ? true\n : rowVals[isPublicizedColIndex] == 'true'\n const realmId =\n rowVals[realmIdColIndex] == null ? '0' : rowVals[realmIdColIndex]\n const area =\n areaColIndex >= 0 && rowVals[areaColIndex] != null\n ? rowVals[areaColIndex]\n : undefined\n return {\n appId,\n appURL,\n description,\n friendlyName,\n requestAffiliation,\n logo,\n isPublicized,\n palette: appPalette,\n shortDescription,\n realmId,\n area,\n }\n }),\n [\n rows,\n logoFileHandleColIndex,\n friendlyNameColIndex,\n sourceAppConfigTableID,\n primaryColorColIndex,\n secondaryColorColIndex,\n appIdColIndex,\n appURLColIndex,\n descriptionColIndex,\n shortDescriptionColIndex,\n requestAffiliationColIndex,\n isPublicizedColIndex,\n realmIdColIndex,\n areaColIndex,\n ],\n )\n\n // Fetch all realms in parallel\n const realmQueries = useQueries({\n queries:\n configsWithRealmId?.map(config =>\n getRealmByIdQueryOptions(config.realmId, keyFactory, synapseClient),\n ) ?? [],\n })\n\n // Combine configs with realm data\n return useMemo(() => {\n // If query returned null (error case) or no rowSet, return static config fallback\n if (!tableQueryResult || !rowSet) {\n return FALLBACK_SOURCE_APP_CONFIGS\n }\n\n if (!configsWithRealmId) return undefined\n\n // Check if all realm queries have succeeded\n const allRealmsLoaded = realmQueries.every(query => query.isSuccess)\n if (!allRealmsLoaded) return undefined\n\n return configsWithRealmId.map((config, index) => {\n const realm = realmQueries[index].data\n return {\n ...config,\n defaultRealm: realm,\n }\n })\n }, [configsWithRealmId, realmQueries, tableQueryResult, rowSet])\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"useTableImageUrl.js","names":[],"sources":["../../../src/utils/hooks/useTableImageUrl.ts"],"sourcesContent":["import { useGetStablePresignedUrl } from '@/synapse-queries'\nimport {\n FileHandleAssociation,\n FileHandleAssociateType,\n} from '@sage-bionetworks/synapse-types'\n\nconst useTableImageUrl = (imageFileHandleId: string, entityId: string) => {\n const fha: FileHandleAssociation = {\n associateObjectId: entityId,\n associateObjectType: FileHandleAssociateType.TableEntity,\n fileHandleId: imageFileHandleId ?? '',\n }\n const stablePresignedUrl = useGetStablePresignedUrl(fha, false, {\n enabled: !!imageFileHandleId,\n })\n\n return stablePresignedUrl?.dataUrl\n}\n\nexport default useTableImageUrl\n"],"mappings":";;;;AAMA,IAAM,KAAoB,GAA2B,MAMxB,EALQ;CACjC,mBAAmB;CACnB,qBAAqB,EAAwB;CAC7C,cAAc,KAAqB;CACpC,EACwD,IAAO,EAC9D,SAAS,CAAC,CAAC,GACZ,CAAC,EAEyB"}
1
+ {"version":3,"file":"useTableImageUrl.js","names":[],"sources":["../../../src/utils/hooks/useTableImageUrl.ts"],"sourcesContent":["import { useGetStablePresignedUrl } from '@/synapse-queries'\nimport {\n FileHandleAssociation,\n FileHandleAssociateType,\n} from '@sage-bionetworks/synapse-types'\n\nconst useTableImageUrl = (imageFileHandleId: string, entityId: string) => {\n const fha: FileHandleAssociation = {\n associateObjectId: entityId,\n associateObjectType: FileHandleAssociateType.TableEntity,\n fileHandleId: imageFileHandleId ?? '',\n }\n const stablePresignedUrl = useGetStablePresignedUrl(fha, false, {\n enabled: !!imageFileHandleId,\n })\n\n return stablePresignedUrl?.dataUrl\n}\n\nexport default useTableImageUrl\n"],"mappings":";;;;AAMA,IAAM,KAAoB,GAA2B,MAMxB,EAAyB;CAJlD,mBAAmB;CACnB,qBAAqB,EAAwB;CAC7C,cAAc,KAAqB;CAEe,EAAK,IAAO,EAC9D,SAAS,CAAC,CAAC,GACZ,CAEM,EAAoB"}
@@ -1 +1 @@
1
- {"version":3,"file":"useCreatePathsAndGetParentId.js","names":[],"sources":["../../../../src/utils/hooks/useUploadFileEntity/useCreatePathsAndGetParentId.ts"],"sourcesContent":["import { useMutation } from '@tanstack/react-query'\nimport { useCreateFolderPath } from './useCreateFolderPath'\n\n/**\n * Given a File to upload as a FileEntity, create a matching directory structure (as necessary)\n * and return the ID of the folder in which the file should be uploaded.\n */\nexport function useCreatePathsAndGetParentId() {\n const { mutateAsync: createFolderPath } = useCreateFolderPath()\n\n return useMutation({\n mutationFn: (args: { file: File; rootContainerId: string }) => {\n const { file, rootContainerId } = args\n const relativePath = file.webkitRelativePath\n // Create folders as necessary based on the path\n if (relativePath) {\n const path = relativePath\n .split('/')\n // remove the file name\n .slice(0, -1)\n return createFolderPath({ rootContainerId, path }).then(parentId => ({\n file,\n parentId,\n }))\n } else {\n // There is no relative path, so just use the passed rootContainerId\n return Promise.resolve({\n file,\n parentId: rootContainerId,\n })\n }\n },\n })\n}\n"],"mappings":";;;AAOA,SAAgB,IAA+B;CAC7C,IAAM,EAAE,aAAa,MAAqB,GAAqB;AAE/D,QAAO,EAAY,EACjB,aAAa,MAAkD;EAC7D,IAAM,EAAE,SAAM,uBAAoB,GAC5B,IAAe,EAAK;AAaxB,SAXE,IAKK,EAAiB;GAAE;GAAiB,MAJ9B,EACV,MAAM,IAAI,CAEV,MAAM,GAAG,GAAG;GACkC,CAAC,CAAC,MAAK,OAAa;GACnE;GACA;GACD,EAAE,GAGI,QAAQ,QAAQ;GACrB;GACA,UAAU;GACX,CAAC;IAGP,CAAC"}
1
+ {"version":3,"file":"useCreatePathsAndGetParentId.js","names":[],"sources":["../../../../src/utils/hooks/useUploadFileEntity/useCreatePathsAndGetParentId.ts"],"sourcesContent":["import { useMutation } from '@tanstack/react-query'\nimport { useCreateFolderPath } from './useCreateFolderPath'\n\n/**\n * Given a File to upload as a FileEntity, create a matching directory structure (as necessary)\n * and return the ID of the folder in which the file should be uploaded.\n */\nexport function useCreatePathsAndGetParentId() {\n const { mutateAsync: createFolderPath } = useCreateFolderPath()\n\n return useMutation({\n mutationFn: (args: { file: File; rootContainerId: string }) => {\n const { file, rootContainerId } = args\n const relativePath = file.webkitRelativePath\n // Create folders as necessary based on the path\n if (relativePath) {\n const path = relativePath\n .split('/')\n // remove the file name\n .slice(0, -1)\n return createFolderPath({ rootContainerId, path }).then(parentId => ({\n file,\n parentId,\n }))\n } else {\n // There is no relative path, so just use the passed rootContainerId\n return Promise.resolve({\n file,\n parentId: rootContainerId,\n })\n }\n },\n })\n}\n"],"mappings":";;;AAOA,SAAgB,IAA+B;CAC7C,IAAM,EAAE,aAAa,MAAqB,GAAqB;AAE/D,QAAO,EAAY,EACjB,aAAa,MAAkD;EAC7D,IAAM,EAAE,SAAM,uBAAoB,GAC5B,IAAe,EAAK;AAaxB,SAXE,IAKK,EAAiB;GAAE;GAAiB,MAJ9B,EACV,MAAM,IAAI,CAEV,MAAM,GAAG,GAC+B;GAAM,CAAC,CAAC,MAAK,OAAa;GACnE;GACA;GACD,EAAE,GAGI,QAAQ,QAAQ;GACrB;GACA,UAAU;GACX,CAAC;IAGP,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"useLinkFileEntityToURL.js","names":[],"sources":["../../../../src/utils/hooks/useUploadFileEntity/useLinkFileEntityToURL.ts"],"sourcesContent":["import {\n useCreateEntity,\n useUpdateEntity,\n} from '@/synapse-queries/entity/useEntity'\nimport { useCreateExternalFileHandle } from '@/synapse-queries/file/useFileHandle'\nimport { FileEntity } from '@sage-bionetworks/synapse-types'\nimport { useMutation, UseMutationOptions } from '@tanstack/react-query'\nimport { useSynapseContext } from '../../context/SynapseContext'\nimport {\n convertToEntityType,\n isContainerType,\n} from '../../functions/EntityTypeUtils'\n\nexport function getFileNameFromExternalUrl(path: string) {\n // grab the text between the last '/' and following '?'\n let fileName = ''\n if (path != null) {\n const lastSlash = path.lastIndexOf('/')\n if (lastSlash > -1) {\n const firstQuestionMark = path.indexOf('?', lastSlash)\n if (firstQuestionMark > -1) {\n fileName = path.substring(lastSlash + 1, firstQuestionMark)\n } else {\n fileName = path.substring(lastSlash + 1)\n }\n }\n }\n return fileName\n}\n\ntype UseLinkFileEntityToURLArgs = {\n /** The FileEntity to update or container entity in which a new FileEntity should be created */\n entityId: string\n /** A URL pointing to the external file */\n url: string\n /** An optional name for the FileHandle and newly created FileEntity. If not provided, the name will be generated based\n * on the URL. An updated FileEntity will not receive the new name. */\n name?: string\n}\n\nexport function useLinkFileEntityToURL(\n options?: UseMutationOptions<FileEntity, Error, UseLinkFileEntityToURLArgs>,\n) {\n const { synapseClient } = useSynapseContext()\n\n const { mutateAsync: createExternalFileHandle } =\n useCreateExternalFileHandle()\n\n const { mutateAsync: createEntity } = useCreateEntity()\n\n const { mutateAsync: updateEntity } = useUpdateEntity()\n\n return useMutation<FileEntity, Error, UseLinkFileEntityToURLArgs>({\n ...options,\n mutationFn: async (args: UseLinkFileEntityToURLArgs) => {\n const { url, entityId } = args\n let name = args.name\n if (name == '' || name == null) {\n name = getFileNameFromExternalUrl(url)\n }\n\n // Fetch the entity to determine if we're updating or creating a new entity\n const entity = await synapseClient.entityServicesClient.getRepoV1EntityId(\n { id: entityId },\n )\n\n let isUpdating: boolean\n if (entity.concreteType === 'org.sagebionetworks.repo.model.FileEntity') {\n isUpdating = true\n } else if (isContainerType(convertToEntityType(entity.concreteType))) {\n isUpdating = false\n } else {\n throw new Error(\n `The ${entityId} is not a FileEntity or a container, got concreteType ${entity.concreteType}`,\n )\n }\n\n // Create the file handle\n const fileHandle = await createExternalFileHandle({\n externalFileHandleInterface: {\n concreteType:\n 'org.sagebionetworks.repo.model.file.ExternalFileHandle',\n fileName: name,\n externalURL: url,\n },\n })\n if (isUpdating) {\n // Update the entity with the new file handle\n const fileEntity: FileEntity = {\n ...(entity as FileEntity),\n dataFileHandleId: fileHandle.id!,\n }\n return (await updateEntity(fileEntity)) as FileEntity\n } else {\n // Update the entity with the new file handle\n const fileEntity: FileEntity = {\n name: name,\n concreteType: 'org.sagebionetworks.repo.model.FileEntity',\n parentId: entityId,\n dataFileHandleId: fileHandle.id!,\n }\n return (await createEntity(fileEntity)) as FileEntity\n }\n },\n })\n}\n"],"mappings":";;;;;;AAaA,SAAgB,EAA2B,GAAc;CAEvD,IAAI,IAAW;AACf,KAAI,KAAQ,MAAM;EAChB,IAAM,IAAY,EAAK,YAAY,IAAI;AACvC,MAAI,IAAY,IAAI;GAClB,IAAM,IAAoB,EAAK,QAAQ,KAAK,EAAU;AACtD,GAGE,IAHE,IAAoB,KACX,EAAK,UAAU,IAAY,GAAG,EAAkB,GAEhD,EAAK,UAAU,IAAY,EAAE;;;AAI9C,QAAO;;AAaT,SAAgB,EACd,GACA;CACA,IAAM,EAAE,qBAAkB,GAAmB,EAEvC,EAAE,aAAa,MACnB,GAA6B,EAEzB,EAAE,aAAa,MAAiB,GAAiB,EAEjD,EAAE,aAAa,MAAiB,GAAiB;AAEvD,QAAO,EAA2D;EAChE,GAAG;EACH,YAAY,OAAO,MAAqC;GACtD,IAAM,EAAE,QAAK,gBAAa,GACtB,IAAO,EAAK;AAChB,IAAI,KAAQ,MAAM,KAAQ,UACxB,IAAO,EAA2B,EAAI;GAIxC,IAAM,IAAS,MAAM,EAAc,qBAAqB,kBACtD,EAAE,IAAI,GAAU,CACjB,EAEG;AACJ,OAAI,EAAO,iBAAiB,4CAC1B,KAAa;YACJ,EAAgB,EAAoB,EAAO,aAAa,CAAC,CAClE,KAAa;OAEb,OAAU,MACR,OAAO,EAAS,wDAAwD,EAAO,eAChF;GAIH,IAAM,IAAa,MAAM,EAAyB,EAChD,6BAA6B;IAC3B,cACE;IACF,UAAU;IACV,aAAa;IACd,EACF,CAAC;AAgBA,UAfE,IAMM,MAAM,EAJiB;IAC7B,GAAI;IACJ,kBAAkB,EAAW;IAC9B,CACqC,GAS9B,MAAM,EANiB;IACvB;IACN,cAAc;IACd,UAAU;IACV,kBAAkB,EAAW;IAC9B,CACqC;;EAG3C,CAAC"}
1
+ {"version":3,"file":"useLinkFileEntityToURL.js","names":[],"sources":["../../../../src/utils/hooks/useUploadFileEntity/useLinkFileEntityToURL.ts"],"sourcesContent":["import {\n useCreateEntity,\n useUpdateEntity,\n} from '@/synapse-queries/entity/useEntity'\nimport { useCreateExternalFileHandle } from '@/synapse-queries/file/useFileHandle'\nimport { FileEntity } from '@sage-bionetworks/synapse-types'\nimport { useMutation, UseMutationOptions } from '@tanstack/react-query'\nimport { useSynapseContext } from '../../context/SynapseContext'\nimport {\n convertToEntityType,\n isContainerType,\n} from '../../functions/EntityTypeUtils'\n\nexport function getFileNameFromExternalUrl(path: string) {\n // grab the text between the last '/' and following '?'\n let fileName = ''\n if (path != null) {\n const lastSlash = path.lastIndexOf('/')\n if (lastSlash > -1) {\n const firstQuestionMark = path.indexOf('?', lastSlash)\n if (firstQuestionMark > -1) {\n fileName = path.substring(lastSlash + 1, firstQuestionMark)\n } else {\n fileName = path.substring(lastSlash + 1)\n }\n }\n }\n return fileName\n}\n\ntype UseLinkFileEntityToURLArgs = {\n /** The FileEntity to update or container entity in which a new FileEntity should be created */\n entityId: string\n /** A URL pointing to the external file */\n url: string\n /** An optional name for the FileHandle and newly created FileEntity. If not provided, the name will be generated based\n * on the URL. An updated FileEntity will not receive the new name. */\n name?: string\n}\n\nexport function useLinkFileEntityToURL(\n options?: UseMutationOptions<FileEntity, Error, UseLinkFileEntityToURLArgs>,\n) {\n const { synapseClient } = useSynapseContext()\n\n const { mutateAsync: createExternalFileHandle } =\n useCreateExternalFileHandle()\n\n const { mutateAsync: createEntity } = useCreateEntity()\n\n const { mutateAsync: updateEntity } = useUpdateEntity()\n\n return useMutation<FileEntity, Error, UseLinkFileEntityToURLArgs>({\n ...options,\n mutationFn: async (args: UseLinkFileEntityToURLArgs) => {\n const { url, entityId } = args\n let name = args.name\n if (name == '' || name == null) {\n name = getFileNameFromExternalUrl(url)\n }\n\n // Fetch the entity to determine if we're updating or creating a new entity\n const entity = await synapseClient.entityServicesClient.getRepoV1EntityId(\n { id: entityId },\n )\n\n let isUpdating: boolean\n if (entity.concreteType === 'org.sagebionetworks.repo.model.FileEntity') {\n isUpdating = true\n } else if (isContainerType(convertToEntityType(entity.concreteType))) {\n isUpdating = false\n } else {\n throw new Error(\n `The ${entityId} is not a FileEntity or a container, got concreteType ${entity.concreteType}`,\n )\n }\n\n // Create the file handle\n const fileHandle = await createExternalFileHandle({\n externalFileHandleInterface: {\n concreteType:\n 'org.sagebionetworks.repo.model.file.ExternalFileHandle',\n fileName: name,\n externalURL: url,\n },\n })\n if (isUpdating) {\n // Update the entity with the new file handle\n const fileEntity: FileEntity = {\n ...(entity as FileEntity),\n dataFileHandleId: fileHandle.id!,\n }\n return (await updateEntity(fileEntity)) as FileEntity\n } else {\n // Update the entity with the new file handle\n const fileEntity: FileEntity = {\n name: name,\n concreteType: 'org.sagebionetworks.repo.model.FileEntity',\n parentId: entityId,\n dataFileHandleId: fileHandle.id!,\n }\n return (await createEntity(fileEntity)) as FileEntity\n }\n },\n })\n}\n"],"mappings":";;;;;;AAaA,SAAgB,EAA2B,GAAc;CAEvD,IAAI,IAAW;AACf,KAAI,KAAQ,MAAM;EAChB,IAAM,IAAY,EAAK,YAAY,IAAI;AACvC,MAAI,IAAY,IAAI;GAClB,IAAM,IAAoB,EAAK,QAAQ,KAAK,EAAU;AACtD,GAGE,IAHE,IAAoB,KACX,EAAK,UAAU,IAAY,GAAG,EAAkB,GAEhD,EAAK,UAAU,IAAY,EAAE;;;AAI9C,QAAO;;AAaT,SAAgB,EACd,GACA;CACA,IAAM,EAAE,qBAAkB,GAAmB,EAEvC,EAAE,aAAa,MACnB,GAA6B,EAEzB,EAAE,aAAa,MAAiB,GAAiB,EAEjD,EAAE,aAAa,MAAiB,GAAiB;AAEvD,QAAO,EAA2D;EAChE,GAAG;EACH,YAAY,OAAO,MAAqC;GACtD,IAAM,EAAE,QAAK,gBAAa,GACtB,IAAO,EAAK;AAChB,IAAI,KAAQ,MAAM,KAAQ,UACxB,IAAO,EAA2B,EAAI;GAIxC,IAAM,IAAS,MAAM,EAAc,qBAAqB,kBACtD,EAAE,IAAI,GAAU,CACjB,EAEG;AACJ,OAAI,EAAO,iBAAiB,4CAC1B,KAAa;YACJ,EAAgB,EAAoB,EAAO,aAAa,CAAC,CAClE,KAAa;OAEb,OAAU,MACR,OAAO,EAAS,wDAAwD,EAAO,eAChF;GAIH,IAAM,IAAa,MAAM,EAAyB,EAChD,6BAA6B;IAC3B,cACE;IACF,UAAU;IACV,aAAa;IACd,EACF,CAAC;AAgBA,UAfE,IAMM,MAAM,EAAa;IAHzB,GAAI;IACJ,kBAAkB,EAAW;IAEJ,CAAW,GAS9B,MAAM,EAAa;IALnB;IACN,cAAc;IACd,UAAU;IACV,kBAAkB,EAAW;IAEJ,CAAW;;EAG3C,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"usePrepareFileEntityUpload.js","names":[],"sources":["../../../../src/utils/hooks/useUploadFileEntity/usePrepareFileEntityUpload.ts"],"sourcesContent":["import {\n BaseFilePreparedForUpload,\n BaseFileUploadArgs,\n} from '@/utils/hooks/useUploadFileEntity/useUploadFiles'\nimport { SynapseClientError } from '@sage-bionetworks/synapse-client'\nimport { useMutation, UseMutationOptions } from '@tanstack/react-query'\nimport { useSynapseContext } from '../../context/index'\nimport { getFileEntityIdWithSameName } from './getFileEntityIdWithSameName'\nimport { useCreatePathsAndGetParentId } from './useCreatePathsAndGetParentId'\n\nexport type NewEntityFileUploadArgs = BaseFileUploadArgs & {\n rootContainerId: string\n}\nexport type NewEntityFilePreparedForUpload = BaseFilePreparedForUpload & {\n parentId: string\n}\nexport type UpdateEntityFilePreparedForUpload = BaseFilePreparedForUpload & {\n existingEntityId: string\n}\n\nexport type FilePreparedForUpload =\n | BaseFilePreparedForUpload\n | NewEntityFilePreparedForUpload\n | UpdateEntityFilePreparedForUpload\n\nexport type PrepareDirsForUploadReturn = {\n filesReadyForUpload: FilePreparedForUpload[]\n filesToPromptForNewVersion: FilePreparedForUpload[]\n}\n\nexport type PrepareFileEntityUploadArgs = (\n | NewEntityFileUploadArgs\n | UpdateEntityFilePreparedForUpload\n)[]\n\n/**\n * Mutation used to check and prepare the entity tree just before uploading a list of files.\n *\n * Given a list of files and a parent ID in which the files should be uploaded, the mutation will:\n *\n * 1. Create the necessary Folder entities in which the files to be uploaded (e.g. if the user uploaded a folder)\n * 2. Check if any of the files to be uploaded already exist in the target parent folder\n *\n * The mutation will return two lists of files and their destination parentIds:\n * - filesReadyForUpload: New files that do not have corresponding file entities in the target parent folder, or a file\n * that should be updated without a prompt.\n * - filesToPromptForNewVersion: Files that have corresponding file entities in the target parent folder. The user\n * should be prompted to accept or reject the creation of a new version of the file.\n *\n * In the future, this sequence could be amended to check if the storage location has enough space to accommodate all the\n * new files, and return an error if not.\n *\n * @param options\n */\nexport function usePrepareFileEntityUpload(\n options?: Partial<\n UseMutationOptions<\n PrepareDirsForUploadReturn,\n SynapseClientError,\n PrepareFileEntityUploadArgs\n >\n >,\n) {\n const { synapseClient } = useSynapseContext()\n const { mutateAsync: createDirsForFileList } = useCreatePathsAndGetParentId()\n\n return useMutation({\n ...options,\n mutationFn: async (args: PrepareFileEntityUploadArgs) => {\n // If `existingEntityId` is defined, the file will be used to update a chosen FileEntity\n // we don't need to create dirs or prompt the user to confirm an update.\n const updatedFileEntitiesNoPrompt: FilePreparedForUpload[] = args.filter(\n arg => 'existingEntityId' in arg,\n )\n\n // 1. Create directories for the files uploaded to a container as needed\n const fileAndParentIds: { file: File; parentId: string }[] = []\n for (const arg of args) {\n try {\n if ('rootContainerId' in arg) {\n const { file, rootContainerId } = arg\n // Create the directories serially; if multiple files are uploaded, they may share new directories\n // Creating folders in parallel could cause race conditions\n fileAndParentIds.push(\n await createDirsForFileList({\n file,\n rootContainerId: rootContainerId,\n }),\n )\n }\n } catch (e) {\n throw new Error(\n `Unable to create target folder structure for file ${\n arg.file.name\n }${Object.hasOwn(e, 'message') ? `: ${e.message}` : null}`,\n { cause: e },\n )\n }\n }\n\n // 2. Check for existing files, and prompt the user if a new version should be created\n const getExistingFilesResults = await Promise.allSettled(\n fileAndParentIds.map(fileAndParentId =>\n getFileEntityIdWithSameName(\n fileAndParentId.file.name,\n fileAndParentId.parentId,\n synapseClient,\n 'The file could not be uploaded.',\n ).then(existingEntityId => ({\n ...fileAndParentId,\n existingEntityId,\n })),\n ),\n )\n\n const filesWithError: PromiseRejectedResult[] =\n getExistingFilesResults.filter(promise => promise.status === 'rejected')\n\n if (filesWithError.length > 0) {\n throw new Error(\n `Files could not be uploaded:\\n\\t${filesWithError\n .map(promise => (promise.reason as Error).message)\n .join('\\n\\t')}`,\n )\n }\n\n // Split the results into new and updated files\n const filesPreparedForUpload = getExistingFilesResults\n // All of these promises are fulfilled -- we use this filter to narrow the type\n .filter(promise => promise.status === 'fulfilled')\n .map(promise => promise.value)\n\n const newFileEntities = filesPreparedForUpload.filter(\n f => f.existingEntityId == null,\n )\n\n const filesReadyForUpload = [\n ...newFileEntities,\n ...updatedFileEntitiesNoPrompt,\n ]\n\n const filesToPromptForNewVersion = filesPreparedForUpload.filter(\n f => f.existingEntityId != null,\n )\n\n return {\n filesReadyForUpload,\n filesToPromptForNewVersion,\n }\n },\n })\n}\n"],"mappings":";;;;;;AAsDA,SAAgB,EACd,GAOA;CACA,IAAM,EAAE,qBAAkB,GAAmB,EACvC,EAAE,aAAa,MAA0B,GAA8B;AAE7E,QAAO,EAAY;EACjB,GAAG;EACH,YAAY,OAAO,MAAsC;GAGvD,IAAM,IAAuD,EAAK,QAChE,MAAO,sBAAsB,EAC9B,EAGK,IAAuD,EAAE;AAC/D,QAAK,IAAM,KAAO,EAChB,KAAI;AACF,QAAI,qBAAqB,GAAK;KAC5B,IAAM,EAAE,SAAM,uBAAoB;AAGlC,OAAiB,KACf,MAAM,EAAsB;MAC1B;MACiB;MAClB,CAAC,CACH;;YAEI,GAAG;AACV,UAAU,MACR,qDACE,EAAI,KAAK,OACR,OAAO,OAAO,GAAG,UAAU,GAAG,KAAK,EAAE,YAAY,QACpD,EAAE,OAAO,GAAG,CACb;;GAKL,IAAM,IAA0B,MAAM,QAAQ,WAC5C,EAAiB,KAAI,MACnB,EACE,EAAgB,KAAK,MACrB,EAAgB,UAChB,GACA,kCACD,CAAC,MAAK,OAAqB;IAC1B,GAAG;IACH;IACD,EAAE,CACJ,CACF,EAEK,IACJ,EAAwB,QAAO,MAAW,EAAQ,WAAW,WAAW;AAE1E,OAAI,EAAe,SAAS,EAC1B,OAAU,MACR,mCAAmC,EAChC,KAAI,MAAY,EAAQ,OAAiB,QAAQ,CACjD,KAAK,MAAO,GAChB;GAIH,IAAM,IAAyB,EAE5B,QAAO,MAAW,EAAQ,WAAW,YAAY,CACjD,KAAI,MAAW,EAAQ,MAAM;AAehC,UAAO;IACL,qBAV0B,CAC1B,GALsB,EAAuB,QAC7C,MAAK,EAAE,oBAAoB,KAC5B,EAIC,GAAG,EACJ;IAQC,4BANiC,EAAuB,QACxD,MAAK,EAAE,oBAAoB,KAC5B;IAKA;;EAEJ,CAAC"}
1
+ {"version":3,"file":"usePrepareFileEntityUpload.js","names":[],"sources":["../../../../src/utils/hooks/useUploadFileEntity/usePrepareFileEntityUpload.ts"],"sourcesContent":["import {\n BaseFilePreparedForUpload,\n BaseFileUploadArgs,\n} from '@/utils/hooks/useUploadFileEntity/useUploadFiles'\nimport { SynapseClientError } from '@sage-bionetworks/synapse-client'\nimport { useMutation, UseMutationOptions } from '@tanstack/react-query'\nimport { useSynapseContext } from '../../context/index'\nimport { getFileEntityIdWithSameName } from './getFileEntityIdWithSameName'\nimport { useCreatePathsAndGetParentId } from './useCreatePathsAndGetParentId'\n\nexport type NewEntityFileUploadArgs = BaseFileUploadArgs & {\n rootContainerId: string\n}\nexport type NewEntityFilePreparedForUpload = BaseFilePreparedForUpload & {\n parentId: string\n}\nexport type UpdateEntityFilePreparedForUpload = BaseFilePreparedForUpload & {\n existingEntityId: string\n}\n\nexport type FilePreparedForUpload =\n | BaseFilePreparedForUpload\n | NewEntityFilePreparedForUpload\n | UpdateEntityFilePreparedForUpload\n\nexport type PrepareDirsForUploadReturn = {\n filesReadyForUpload: FilePreparedForUpload[]\n filesToPromptForNewVersion: FilePreparedForUpload[]\n}\n\nexport type PrepareFileEntityUploadArgs = (\n | NewEntityFileUploadArgs\n | UpdateEntityFilePreparedForUpload\n)[]\n\n/**\n * Mutation used to check and prepare the entity tree just before uploading a list of files.\n *\n * Given a list of files and a parent ID in which the files should be uploaded, the mutation will:\n *\n * 1. Create the necessary Folder entities in which the files to be uploaded (e.g. if the user uploaded a folder)\n * 2. Check if any of the files to be uploaded already exist in the target parent folder\n *\n * The mutation will return two lists of files and their destination parentIds:\n * - filesReadyForUpload: New files that do not have corresponding file entities in the target parent folder, or a file\n * that should be updated without a prompt.\n * - filesToPromptForNewVersion: Files that have corresponding file entities in the target parent folder. The user\n * should be prompted to accept or reject the creation of a new version of the file.\n *\n * In the future, this sequence could be amended to check if the storage location has enough space to accommodate all the\n * new files, and return an error if not.\n *\n * @param options\n */\nexport function usePrepareFileEntityUpload(\n options?: Partial<\n UseMutationOptions<\n PrepareDirsForUploadReturn,\n SynapseClientError,\n PrepareFileEntityUploadArgs\n >\n >,\n) {\n const { synapseClient } = useSynapseContext()\n const { mutateAsync: createDirsForFileList } = useCreatePathsAndGetParentId()\n\n return useMutation({\n ...options,\n mutationFn: async (args: PrepareFileEntityUploadArgs) => {\n // If `existingEntityId` is defined, the file will be used to update a chosen FileEntity\n // we don't need to create dirs or prompt the user to confirm an update.\n const updatedFileEntitiesNoPrompt: FilePreparedForUpload[] = args.filter(\n arg => 'existingEntityId' in arg,\n )\n\n // 1. Create directories for the files uploaded to a container as needed\n const fileAndParentIds: { file: File; parentId: string }[] = []\n for (const arg of args) {\n try {\n if ('rootContainerId' in arg) {\n const { file, rootContainerId } = arg\n // Create the directories serially; if multiple files are uploaded, they may share new directories\n // Creating folders in parallel could cause race conditions\n fileAndParentIds.push(\n await createDirsForFileList({\n file,\n rootContainerId: rootContainerId,\n }),\n )\n }\n } catch (e) {\n throw new Error(\n `Unable to create target folder structure for file ${\n arg.file.name\n }${Object.hasOwn(e, 'message') ? `: ${e.message}` : null}`,\n { cause: e },\n )\n }\n }\n\n // 2. Check for existing files, and prompt the user if a new version should be created\n const getExistingFilesResults = await Promise.allSettled(\n fileAndParentIds.map(fileAndParentId =>\n getFileEntityIdWithSameName(\n fileAndParentId.file.name,\n fileAndParentId.parentId,\n synapseClient,\n 'The file could not be uploaded.',\n ).then(existingEntityId => ({\n ...fileAndParentId,\n existingEntityId,\n })),\n ),\n )\n\n const filesWithError: PromiseRejectedResult[] =\n getExistingFilesResults.filter(promise => promise.status === 'rejected')\n\n if (filesWithError.length > 0) {\n throw new Error(\n `Files could not be uploaded:\\n\\t${filesWithError\n .map(promise => (promise.reason as Error).message)\n .join('\\n\\t')}`,\n )\n }\n\n // Split the results into new and updated files\n const filesPreparedForUpload = getExistingFilesResults\n // All of these promises are fulfilled -- we use this filter to narrow the type\n .filter(promise => promise.status === 'fulfilled')\n .map(promise => promise.value)\n\n const newFileEntities = filesPreparedForUpload.filter(\n f => f.existingEntityId == null,\n )\n\n const filesReadyForUpload = [\n ...newFileEntities,\n ...updatedFileEntitiesNoPrompt,\n ]\n\n const filesToPromptForNewVersion = filesPreparedForUpload.filter(\n f => f.existingEntityId != null,\n )\n\n return {\n filesReadyForUpload,\n filesToPromptForNewVersion,\n }\n },\n })\n}\n"],"mappings":";;;;;;AAsDA,SAAgB,EACd,GAOA;CACA,IAAM,EAAE,qBAAkB,GAAmB,EACvC,EAAE,aAAa,MAA0B,GAA8B;AAE7E,QAAO,EAAY;EACjB,GAAG;EACH,YAAY,OAAO,MAAsC;GAGvD,IAAM,IAAuD,EAAK,QAChE,MAAO,sBAAsB,EAC9B,EAGK,IAAuD,EAAE;AAC/D,QAAK,IAAM,KAAO,EAChB,KAAI;AACF,QAAI,qBAAqB,GAAK;KAC5B,IAAM,EAAE,SAAM,uBAAoB;AAGlC,OAAiB,KACf,MAAM,EAAsB;MAC1B;MACiB;MAClB,CAAC,CACH;;YAEI,GAAG;AACV,UAAU,MACR,qDACE,EAAI,KAAK,OACR,OAAO,OAAO,GAAG,UAAU,GAAG,KAAK,EAAE,YAAY,QACpD,EAAE,OAAO,GAAG,CACb;;GAKL,IAAM,IAA0B,MAAM,QAAQ,WAC5C,EAAiB,KAAI,MACnB,EACE,EAAgB,KAAK,MACrB,EAAgB,UAChB,GACA,kCACD,CAAC,MAAK,OAAqB;IAC1B,GAAG;IACH;IACD,EAAE,CACJ,CACF,EAEK,IACJ,EAAwB,QAAO,MAAW,EAAQ,WAAW,WAAW;AAE1E,OAAI,EAAe,SAAS,EAC1B,OAAU,MACR,mCAAmC,EAChC,KAAI,MAAY,EAAQ,OAAiB,QAAQ,CACjD,KAAK,MAAO,GAChB;GAIH,IAAM,IAAyB,EAE5B,QAAO,MAAW,EAAQ,WAAW,YAAY,CACjD,KAAI,MAAW,EAAQ,MAAM;AAehC,UAAO;IACL,qBAAA,CATA,GALsB,EAAuB,QAC7C,MAAK,EAAE,oBAAoB,KAIxB,EACH,GAAG,EAQH;IACA,4BANiC,EAAuB,QACxD,MAAK,EAAE,oBAAoB,KAK3B;IACD;;EAEJ,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"useTrackFileUploads.js","names":[],"sources":["../../../../src/utils/hooks/useUploadFileEntity/useTrackFileUploads.ts"],"sourcesContent":["import { ProgressCallback } from '@/synapse-client/index'\nimport { BaseFileUploadArgs } from '@/utils/hooks/useUploadFileEntity/useUploadFiles'\nimport { useState } from 'react'\nimport { FilePreparedForUpload } from './usePrepareFileEntityUpload'\n\nexport type UploadFileStatus =\n | 'PREPARING'\n | 'UPLOADING'\n | 'PAUSED'\n | 'CANCELED_BY_USER'\n | 'FAILED'\n | 'COMPLETE'\n\nexport type TrackedUploadProgress = {\n filePreparedForUpload: FilePreparedForUpload\n progress: ProgressCallback\n abortController: AbortController\n status: UploadFileStatus\n failureReason?: string\n fileHandleId?: string\n}\n\nexport const PENDING_UPLOAD_STATES: TrackedUploadProgress['status'][] = [\n 'PREPARING',\n 'UPLOADING',\n 'PAUSED',\n]\n\nconst UNTRACKED_BYTE_UPLOAD_STATES: TrackedUploadProgress['status'][] = [\n ...PENDING_UPLOAD_STATES,\n // The project storage limits are eventually consistent, so complete uploads may not count against the limit immediately.\n // So we include those in our estimation.\n 'COMPLETE',\n]\n\n/**\n * Hook used to track the state of multiple file uploads, providing methods that can be used to interact with the upload state.\n */\nexport function useTrackFileUploads() {\n const [trackedUploadProgress, setTrackedUploadProgress] = useState<\n Map<File, TrackedUploadProgress>\n >(new Map())\n\n /**\n * Adds new files to be tracked and puts them in the \"PREPARED\" state.\n * Returns the new map of TrackedUploadProgress in case the new value is needed before\n * the state variable is updated.\n * @param preparedFiles\n */\n function trackNewFiles<T extends BaseFileUploadArgs = BaseFileUploadArgs>(\n ...preparedFiles: T[]\n ): Map<File, TrackedUploadProgress> {\n // If a matching file that is uploading already exists, cancel it!\n preparedFiles.forEach(preparedFile => {\n const existingUpload = trackedUploadProgress.get(preparedFile.file)\n if (existingUpload && existingUpload.status !== 'CANCELED_BY_USER') {\n cancelUpload(preparedFile.file)\n }\n })\n\n // Re-initialize progress tracking state variable\n const newTrackedUploadProgress: Map<File, TrackedUploadProgress> = new Map(\n trackedUploadProgress.entries(),\n )\n\n preparedFiles.forEach(preparedFile => {\n newTrackedUploadProgress.set(preparedFile.file, {\n filePreparedForUpload: preparedFile,\n abortController: new AbortController(),\n // Note that this is number of parts uploaded, not file size or %\n progress: { value: 0, total: 1 },\n status: 'PREPARING',\n })\n })\n\n setTrackedUploadProgress(newTrackedUploadProgress)\n\n return newTrackedUploadProgress\n }\n\n function setProgress(file: File, progress: ProgressCallback) {\n setTrackedUploadProgress(prev => {\n const newMap = new Map(prev.entries())\n const entry = newMap.get(file)\n if (entry) {\n newMap.set(file, {\n ...entry,\n progress: {\n ...entry.progress,\n ...progress,\n },\n })\n }\n return newMap\n })\n }\n\n function setStatus(\n file: File,\n status: TrackedUploadProgress['status'],\n failureReason?: string,\n ) {\n setTrackedUploadProgress(prev => {\n const newMap = new Map(prev.entries())\n const entry = newMap.get(file)\n if (entry) {\n newMap.set(file, {\n ...entry,\n status,\n failureReason,\n })\n }\n return newMap\n })\n }\n\n function setFileHandleId(file: File, fileHandleId: string) {\n setTrackedUploadProgress(prev => {\n const newMap = new Map(prev.entries())\n const entry = newMap.get(file)\n if (entry) {\n newMap.set(file, {\n ...entry,\n fileHandleId,\n })\n }\n return newMap\n })\n }\n\n function setIsUploading(file: File) {\n setStatus(file, 'UPLOADING')\n }\n\n function setComplete(file: File, fileHandleId: string) {\n setStatus(file, 'COMPLETE')\n setFileHandleId(file, fileHandleId)\n }\n\n function setFailed(file: File, failureReason: string) {\n setStatus(file, 'FAILED', failureReason)\n }\n\n function cancelUpload(file: File) {\n const entry = trackedUploadProgress.get(file)\n if (entry != null) {\n entry.abortController.abort()\n }\n setStatus(file, 'CANCELED_BY_USER')\n }\n\n function pauseUpload(file: File) {\n const entry = trackedUploadProgress.get(file)\n if (entry != null) {\n entry.abortController.abort('Paused by user')\n }\n setStatus(file, 'PAUSED')\n }\n\n /**\n * Removes the file from the list if the upload state is CANCELED_BY_USER or FAILED. Otherwise, a noop.\n */\n function removeUpload(file: File) {\n setTrackedUploadProgress(prev => {\n const newMap = new Map(prev.entries())\n const entry = newMap.get(file)\n\n if (entry == null) {\n // nothing to do\n } else {\n if (entry.status != 'CANCELED_BY_USER' && entry.status != 'FAILED') {\n console.warn(\n `Attempted to remove tracked upload progress before it was canceled.`,\n entry,\n )\n } else {\n newMap.delete(file)\n }\n }\n return newMap\n })\n }\n\n // The total number of bytes that are being uploaded, but do not yet count against the project storage limits.\n // Used to prevent the uploader from exceeding storage limits before uploading the file (the backend won't provide an error response until after the file is uploaded)\n const bytesPendingUpload = [...trackedUploadProgress].reduce(\n (acc, [file, progress]) => {\n if (UNTRACKED_BYTE_UPLOAD_STATES.includes(progress.status)) {\n return acc + file.size\n }\n\n return acc\n },\n 0,\n )\n\n const activeUploadCount = [...trackedUploadProgress].filter(\n ([_file, progress]) => PENDING_UPLOAD_STATES.includes(progress.status),\n ).length\n const isUploading = activeUploadCount > 0\n\n const isUploadComplete =\n // At least one file is complete\n [...trackedUploadProgress].some(\n ([_file, progress]) => progress.status === 'COMPLETE',\n ) &&\n // No files are pending upload\n !isUploading\n\n return {\n trackedUploadProgress,\n setProgress,\n setIsUploading,\n trackNewFiles,\n bytesPendingUpload,\n pauseUpload,\n cancelUpload,\n removeUpload,\n setComplete,\n setFailed,\n isUploading,\n isUploadComplete,\n activeUploadCount,\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"useTrackFileUploads.js","names":[],"sources":["../../../../src/utils/hooks/useUploadFileEntity/useTrackFileUploads.ts"],"sourcesContent":["import { ProgressCallback } from '@/synapse-client/index'\nimport { BaseFileUploadArgs } from '@/utils/hooks/useUploadFileEntity/useUploadFiles'\nimport { useState } from 'react'\nimport { FilePreparedForUpload } from './usePrepareFileEntityUpload'\n\nexport type UploadFileStatus =\n | 'PREPARING'\n | 'UPLOADING'\n | 'PAUSED'\n | 'CANCELED_BY_USER'\n | 'FAILED'\n | 'COMPLETE'\n\nexport type TrackedUploadProgress = {\n filePreparedForUpload: FilePreparedForUpload\n progress: ProgressCallback\n abortController: AbortController\n status: UploadFileStatus\n failureReason?: string\n fileHandleId?: string\n}\n\nexport const PENDING_UPLOAD_STATES: TrackedUploadProgress['status'][] = [\n 'PREPARING',\n 'UPLOADING',\n 'PAUSED',\n]\n\nconst UNTRACKED_BYTE_UPLOAD_STATES: TrackedUploadProgress['status'][] = [\n ...PENDING_UPLOAD_STATES,\n // The project storage limits are eventually consistent, so complete uploads may not count against the limit immediately.\n // So we include those in our estimation.\n 'COMPLETE',\n]\n\n/**\n * Hook used to track the state of multiple file uploads, providing methods that can be used to interact with the upload state.\n */\nexport function useTrackFileUploads() {\n const [trackedUploadProgress, setTrackedUploadProgress] = useState<\n Map<File, TrackedUploadProgress>\n >(new Map())\n\n /**\n * Adds new files to be tracked and puts them in the \"PREPARED\" state.\n * Returns the new map of TrackedUploadProgress in case the new value is needed before\n * the state variable is updated.\n * @param preparedFiles\n */\n function trackNewFiles<T extends BaseFileUploadArgs = BaseFileUploadArgs>(\n ...preparedFiles: T[]\n ): Map<File, TrackedUploadProgress> {\n // If a matching file that is uploading already exists, cancel it!\n preparedFiles.forEach(preparedFile => {\n const existingUpload = trackedUploadProgress.get(preparedFile.file)\n if (existingUpload && existingUpload.status !== 'CANCELED_BY_USER') {\n cancelUpload(preparedFile.file)\n }\n })\n\n // Re-initialize progress tracking state variable\n const newTrackedUploadProgress: Map<File, TrackedUploadProgress> = new Map(\n trackedUploadProgress.entries(),\n )\n\n preparedFiles.forEach(preparedFile => {\n newTrackedUploadProgress.set(preparedFile.file, {\n filePreparedForUpload: preparedFile,\n abortController: new AbortController(),\n // Note that this is number of parts uploaded, not file size or %\n progress: { value: 0, total: 1 },\n status: 'PREPARING',\n })\n })\n\n setTrackedUploadProgress(newTrackedUploadProgress)\n\n return newTrackedUploadProgress\n }\n\n function setProgress(file: File, progress: ProgressCallback) {\n setTrackedUploadProgress(prev => {\n const newMap = new Map(prev.entries())\n const entry = newMap.get(file)\n if (entry) {\n newMap.set(file, {\n ...entry,\n progress: {\n ...entry.progress,\n ...progress,\n },\n })\n }\n return newMap\n })\n }\n\n function setStatus(\n file: File,\n status: TrackedUploadProgress['status'],\n failureReason?: string,\n ) {\n setTrackedUploadProgress(prev => {\n const newMap = new Map(prev.entries())\n const entry = newMap.get(file)\n if (entry) {\n newMap.set(file, {\n ...entry,\n status,\n failureReason,\n })\n }\n return newMap\n })\n }\n\n function setFileHandleId(file: File, fileHandleId: string) {\n setTrackedUploadProgress(prev => {\n const newMap = new Map(prev.entries())\n const entry = newMap.get(file)\n if (entry) {\n newMap.set(file, {\n ...entry,\n fileHandleId,\n })\n }\n return newMap\n })\n }\n\n function setIsUploading(file: File) {\n setStatus(file, 'UPLOADING')\n }\n\n function setComplete(file: File, fileHandleId: string) {\n setStatus(file, 'COMPLETE')\n setFileHandleId(file, fileHandleId)\n }\n\n function setFailed(file: File, failureReason: string) {\n setStatus(file, 'FAILED', failureReason)\n }\n\n function cancelUpload(file: File) {\n const entry = trackedUploadProgress.get(file)\n if (entry != null) {\n entry.abortController.abort()\n }\n setStatus(file, 'CANCELED_BY_USER')\n }\n\n function pauseUpload(file: File) {\n const entry = trackedUploadProgress.get(file)\n if (entry != null) {\n entry.abortController.abort('Paused by user')\n }\n setStatus(file, 'PAUSED')\n }\n\n /**\n * Removes the file from the list if the upload state is CANCELED_BY_USER or FAILED. Otherwise, a noop.\n */\n function removeUpload(file: File) {\n setTrackedUploadProgress(prev => {\n const newMap = new Map(prev.entries())\n const entry = newMap.get(file)\n\n if (entry == null) {\n // nothing to do\n } else {\n if (entry.status != 'CANCELED_BY_USER' && entry.status != 'FAILED') {\n console.warn(\n `Attempted to remove tracked upload progress before it was canceled.`,\n entry,\n )\n } else {\n newMap.delete(file)\n }\n }\n return newMap\n })\n }\n\n // The total number of bytes that are being uploaded, but do not yet count against the project storage limits.\n // Used to prevent the uploader from exceeding storage limits before uploading the file (the backend won't provide an error response until after the file is uploaded)\n const bytesPendingUpload = [...trackedUploadProgress].reduce(\n (acc, [file, progress]) => {\n if (UNTRACKED_BYTE_UPLOAD_STATES.includes(progress.status)) {\n return acc + file.size\n }\n\n return acc\n },\n 0,\n )\n\n const activeUploadCount = [...trackedUploadProgress].filter(\n ([_file, progress]) => PENDING_UPLOAD_STATES.includes(progress.status),\n ).length\n const isUploading = activeUploadCount > 0\n\n const isUploadComplete =\n // At least one file is complete\n [...trackedUploadProgress].some(\n ([_file, progress]) => progress.status === 'COMPLETE',\n ) &&\n // No files are pending upload\n !isUploading\n\n return {\n trackedUploadProgress,\n setProgress,\n setIsUploading,\n trackNewFiles,\n bytesPendingUpload,\n pauseUpload,\n cancelUpload,\n removeUpload,\n setComplete,\n setFailed,\n isUploading,\n isUploadComplete,\n activeUploadCount,\n }\n}\n"],"mappings":";;AAsBA,IAAa,IAA2D;CACtE;CACA;CACA;CACD,EAEK,IAAkE,CACtE,GAAG,GAGH,WACD;AAKD,SAAgB,IAAsB;CACpC,IAAM,CAAC,GAAuB,KAA4B,kBAExD,IAAI,KAAK,CAAC;CAQZ,SAAS,EACP,GAAG,GAC+B;AAElC,IAAc,SAAQ,MAAgB;GACpC,IAAM,IAAiB,EAAsB,IAAI,EAAa,KAAK;AACnE,GAAI,KAAkB,EAAe,WAAW,sBAC9C,EAAa,EAAa,KAAK;IAEjC;EAGF,IAAM,IAA6D,IAAI,IACrE,EAAsB,SAAS,CAChC;AAcD,SAZA,EAAc,SAAQ,MAAgB;AACpC,KAAyB,IAAI,EAAa,MAAM;IAC9C,uBAAuB;IACvB,iBAAiB,IAAI,iBAAiB;IAEtC,UAAU;KAAE,OAAO;KAAG,OAAO;KAAG;IAChC,QAAQ;IACT,CAAC;IACF,EAEF,EAAyB,EAAyB,EAE3C;;CAGT,SAAS,EAAY,GAAY,GAA4B;AAC3D,KAAyB,MAAQ;GAC/B,IAAM,IAAS,IAAI,IAAI,EAAK,SAAS,CAAC,EAChC,IAAQ,EAAO,IAAI,EAAK;AAU9B,UATI,KACF,EAAO,IAAI,GAAM;IACf,GAAG;IACH,UAAU;KACR,GAAG,EAAM;KACT,GAAG;KACJ;IACF,CAAC,EAEG;IACP;;CAGJ,SAAS,EACP,GACA,GACA,GACA;AACA,KAAyB,MAAQ;GAC/B,IAAM,IAAS,IAAI,IAAI,EAAK,SAAS,CAAC,EAChC,IAAQ,EAAO,IAAI,EAAK;AAQ9B,UAPI,KACF,EAAO,IAAI,GAAM;IACf,GAAG;IACH;IACA;IACD,CAAC,EAEG;IACP;;CAGJ,SAAS,EAAgB,GAAY,GAAsB;AACzD,KAAyB,MAAQ;GAC/B,IAAM,IAAS,IAAI,IAAI,EAAK,SAAS,CAAC,EAChC,IAAQ,EAAO,IAAI,EAAK;AAO9B,UANI,KACF,EAAO,IAAI,GAAM;IACf,GAAG;IACH;IACD,CAAC,EAEG;IACP;;CAGJ,SAAS,EAAe,GAAY;AAClC,IAAU,GAAM,YAAY;;CAG9B,SAAS,EAAY,GAAY,GAAsB;AAErD,EADA,EAAU,GAAM,WAAW,EAC3B,EAAgB,GAAM,EAAa;;CAGrC,SAAS,EAAU,GAAY,GAAuB;AACpD,IAAU,GAAM,UAAU,EAAc;;CAG1C,SAAS,EAAa,GAAY;AAKhC,EAJc,EAAsB,IAAI,EACpC,EACI,gBAAgB,OAAO,EAE/B,EAAU,GAAM,mBAAmB;;CAGrC,SAAS,EAAY,GAAY;AAK/B,EAJc,EAAsB,IAAI,EACpC,EACI,gBAAgB,MAAM,iBAAiB,EAE/C,EAAU,GAAM,SAAS;;CAM3B,SAAS,EAAa,GAAY;AAChC,KAAyB,MAAQ;GAC/B,IAAM,IAAS,IAAI,IAAI,EAAK,SAAS,CAAC,EAChC,IAAQ,EAAO,IAAI,EAAK;AAc9B,UAZI,KAAS,SAGP,EAAM,UAAU,sBAAsB,EAAM,UAAU,WACxD,QAAQ,KACN,uEACA,EACD,GAED,EAAO,OAAO,EAAK,GAGhB;IACP;;CAKJ,IAAM,IAAqB,CAAC,GAAG,EAAsB,CAAC,QACnD,GAAK,CAAC,GAAM,OACP,EAA6B,SAAS,EAAS,OAAO,GACjD,IAAM,EAAK,OAGb,GAET,EACD,EAEK,IAAoB,CAAC,GAAG,EAAsB,CAAC,QAClD,CAAC,GAAO,OAAc,EAAsB,SAAS,EAAS,OAAO,CACvE,CAAC,QACI,IAAc,IAAoB;AAUxC,QAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,kBAlBA,CAAC,GAAG,EAAsB,CAAC,MACxB,CAAC,GAAO,OAAc,EAAS,WAAW,WAC5C,IAED,CAAC;EAeD;EACD"}
@@ -1 +1 @@
1
- {"version":3,"file":"useUploadFileEntities.js","names":[],"sources":["../../../../src/utils/hooks/useUploadFileEntity/useUploadFileEntities.ts"],"sourcesContent":["import { SYNAPSE_STORAGE_LOCATION_ID } from '@/synapse-client/SynapseClient'\nimport {\n useCreateEntity,\n useGetDefaultUploadDestination,\n useUpdateEntity,\n} from '@/synapse-queries'\nimport {\n UploaderState,\n UploadItem,\n useUploadFiles,\n} from '@/utils/hooks/useUploadFileEntity/useUploadFiles'\nimport { FileEntity } from '@sage-bionetworks/synapse-types'\nimport { noop } from 'lodash-es'\nimport { useCallback, useMemo } from 'react'\nimport { useSynapseContext } from '../../context/index'\nimport useConfirmItems from '../useConfirmItems'\nimport {\n FilePreparedForUpload,\n PrepareDirsForUploadReturn,\n PrepareFileEntityUploadArgs,\n UpdateEntityFilePreparedForUpload,\n usePrepareFileEntityUpload,\n} from './usePrepareFileEntityUpload'\nimport { willUploadsExceedStorageLimit } from './willUploadsExceedStorageLimit'\n\nexport type PromptInfo = {\n type: 'CONFIRM_NEW_VERSION'\n fileName: string\n existingEntityId: string\n}\n\nexport type Prompt = {\n info: PromptInfo\n onConfirmAll: () => void\n onConfirm: () => void\n onSkip: () => void\n onCancelAll: () => void\n}\n\nexport type EntityUploaderState =\n | UploaderState\n | 'LOADING'\n | 'PROMPT_USER'\n | 'ERROR'\n\nexport type InitiateUploadArgs = PrepareFileEntityUploadArgs\n\nexport type UseUploadFileEntitiesReturn = {\n /**\n * The current state of the uploader\n */\n state: EntityUploaderState\n errorMessage?: string\n /**\n * True if the uploader is doing work to prepare the upload (e.g. creating folders, checking for existing files), but the upload has not started.\n */\n isPrecheckingUpload: boolean\n /**\n * Prompts that require user input before the upload can proceed. Typically, these are prompts to confirm uploading a new version\n * of an existing FileEntity.\n *\n * If prompts are present, `state` will always be 'PROMPT_USER'.\n */\n activePrompts: Prompt[]\n /**\n * Arguments used to initialize an upload operation. In addition to providing a file, the caller must also provide one of\n * the following:\n * - rootContainerId: The ID of the parent Project or Folder to upload files to. If the File objects include a webkitRelativePath,\n * then the Files will be uploaded into created sub-folders to match the relative path. Any files that match on path and file name\n * will trigger a prompt to confirm updating a new version in the `activePrompts` field, which must be resolved before the upload can proceed.\n * - existingEntityId: The ID of the FileEntity for which a new version should be uploaded. No prompts will be triggered by this option.\n */\n initiateUpload: (args: InitiateUploadArgs) => void\n /**\n * The total number of files that are actively being uploaded (PREPARING, UPLOADING, or PAUSED)\n */\n activeUploadCount: number\n /**\n * A list of each file being uploaded, along with its progress, status, and callbacks that can be used to pause, resume, or cancel the upload.\n */\n uploadProgress: UploadItem[]\n /** True when files can be uploaded. */\n isUploadReady: boolean\n}\n\n/**\n * Hook to start and track the progress of files uploads in Synapse, creating/updating a FileEntity for each uploaded file.\n *\n * To start an upload, see `initiateUpload` returned by this hook.\n */\nexport function useUploadFileEntities(\n /** The ID of the parent entity to upload files to, or the FileEntity for which a new version should be uploaded */\n containerOrEntityId: string,\n /** Optional accessKey for a direct S3 upload */\n accessKey = '',\n /** Optional secretKey for a direct S3 upload */\n secretKey = '',\n /** Invoked if chosen files will exceed storage limits based on a client-side check. */\n onStorageLimitExceeded: () => void = noop,\n): UseUploadFileEntitiesReturn {\n /**\n * General flow of the internal logic in this hook is\n *\n * 1. `initiateUpload(files)` - called by the caller of the hook\n * - Verify that the upload can proceed. Specifically:\n * 1. Validate the upload destination\n * 2. Check if the uploads will exceed a storage limit\n * - If either of these checks fail, do not proceed.\n * 2. `prepareDirsForUpload(files)` - sets up the folder paths, checks for existing entities with the same name\n * 3. `startUploadOrPromptForConfirmation(files)`\n * - For each file, check if the user should be prompted before the upload starts (e.g. if a new version will be created to update an existing file)\n * - If one or more prompts are required, they are added to the `activePrompts` array. Once the user resolves all prompts, the upload may start.\n * - If no prompts are required, the upload starts immediately.\n * 6. `onUploadComplete(file, fileHandleId)` - using the file handle, creates or updates the FileEntity in Synapse\n */\n\n const { synapseClient } = useSynapseContext()\n\n const { mutateAsync: createEntityWithNewFile } = useCreateEntity()\n const { mutateAsync: updateEntityWithNewFile } = useUpdateEntity()\n\n const onUploadComplete = useCallback(\n async function onUploadComplete(\n preparedFile: FilePreparedForUpload,\n newFileHandleId: string,\n ) {\n if ('existingEntityId' in preparedFile && preparedFile.existingEntityId) {\n // Update the existing entity\n const entity =\n await synapseClient.entityServicesClient.getRepoV1EntityId({\n id: preparedFile.existingEntityId,\n })\n await updateEntityWithNewFile({\n ...entity,\n dataFileHandleId: newFileHandleId,\n } as FileEntity)\n } else if ('parentId' in preparedFile) {\n // else, it's a new file entity\n const newFileEntity: FileEntity = {\n parentId: preparedFile.parentId,\n name: preparedFile.file.name,\n concreteType: 'org.sagebionetworks.repo.model.FileEntity',\n dataFileHandleId: newFileHandleId,\n }\n await createEntityWithNewFile(newFileEntity)\n } else {\n // this should never happen\n throw new Error(\n `Can't upload file without a parent ID or existing entity ID. File was: ${JSON.stringify(\n preparedFile,\n )}`,\n )\n }\n },\n [\n createEntityWithNewFile,\n synapseClient.entityServicesClient,\n updateEntityWithNewFile,\n ],\n )\n\n /*\n TODO: We retrieve the upload destination from the root upload container, but what if:\n 1. The user is uploading a folder with nested subdirectories\n 2. One or more of the subdirectories already exists\n 3. An existing subdirectory has a different upload destination\n The current implementation uploads everything into the root upload destination, but the correct behavior might be\n to fetch the upload destination for each subdirectory.\n */\n const {\n data: uploadDestination,\n isLoading: isLoadingUploadDestination,\n error: getUploadDestinationError,\n } = useGetDefaultUploadDestination(containerOrEntityId, {\n // Storage location usage is eventually consistent. We will keep track of the sum of uploaded files client-side\n // to determine if subsequent uploads will exceed the storage limit.\n // Do not refetch the storage location usage to avoid double-counting the size of new uploads against the limit.\n staleTime: Infinity,\n })\n\n const isUploadReady = Boolean(containerOrEntityId && uploadDestination)\n\n const storageLocationId =\n uploadDestination?.storageLocationId || SYNAPSE_STORAGE_LOCATION_ID\n\n const errorMessage = getUploadDestinationError?.message\n\n const {\n pendingItems: filesToConfirmNewVersion,\n confirmItem: confirmUploadFileWithNewVersion,\n addItemsPendingConfirmation: addFileToConfirmUploadNewVersion,\n removePendingItems: skipFileRequiringNewVersion,\n clear: clearPendingFiles,\n } = useConfirmItems<FilePreparedForUpload>()\n\n const onBeforeUpload = useCallback(() => {\n clearPendingFiles()\n }, [clearPendingFiles])\n\n const {\n state,\n startUpload,\n activeUploadCount,\n uploadProgress,\n bytesPendingUpload,\n } = useUploadFiles({\n onBeforeUpload,\n storageLocationId,\n uploadDestination,\n accessKey,\n secretKey,\n onUploadComplete,\n })\n\n /**\n * After all files have been prepared for upload, process them to determine if the upload can automatically continue.\n * If all files can be uploaded without user intervention, start the upload. Otherwise, prompt the user to make required\n * decisions to continue the upload.\n */\n const startUploadOrPromptForConfirmation = useCallback(\n (preparedFiles: PrepareDirsForUploadReturn) => {\n const { filesReadyForUpload, filesToPromptForNewVersion } = preparedFiles\n if (\n filesReadyForUpload.length > 0 &&\n filesToPromptForNewVersion.length == 0\n ) {\n // No need to prompt the user, just go ahead and upload!\n startUpload(...filesReadyForUpload)\n }\n\n if (filesToPromptForNewVersion.length > 0) {\n // Set up state to ask the user to confirm that they want to upload new versions.\n confirmUploadFileWithNewVersion(...filesReadyForUpload)\n addFileToConfirmUploadNewVersion(...filesToPromptForNewVersion)\n }\n },\n [\n addFileToConfirmUploadNewVersion,\n confirmUploadFileWithNewVersion,\n startUpload,\n ],\n )\n\n const { mutateAsync: prepareDirsForUpload, isPending: isPrecheckingUpload } =\n usePrepareFileEntityUpload()\n\n const initiateUpload = useCallback(\n async (args: InitiateUploadArgs) => {\n if (uploadDestination == null) {\n console.error(\n 'Upload destination was not loaded, or failed to load! Aborting upload.',\n )\n return\n }\n if (\n willUploadsExceedStorageLimit(\n args.map(arg => arg.file),\n uploadDestination.projectStorageLocationUsage,\n bytesPendingUpload,\n )\n ) {\n onStorageLimitExceeded()\n return\n }\n const filesReadyForUpload = await prepareDirsForUpload(args)\n\n startUploadOrPromptForConfirmation(filesReadyForUpload)\n },\n [\n bytesPendingUpload,\n onStorageLimitExceeded,\n startUploadOrPromptForConfirmation,\n prepareDirsForUpload,\n uploadDestination,\n ],\n )\n\n const activePrompts: Prompt[] = useMemo(() => {\n return filesToConfirmNewVersion.map(fileToPrompt => {\n return {\n info: {\n type: 'CONFIRM_NEW_VERSION',\n fileName: fileToPrompt.file.name,\n existingEntityId: (fileToPrompt as UpdateEntityFilePreparedForUpload)\n .existingEntityId,\n },\n onConfirm: () => {\n const { confirmedItems, pendingItems } =\n confirmUploadFileWithNewVersion(fileToPrompt)\n\n if (confirmedItems.length > 0 && pendingItems.length == 0) {\n void startUpload(...confirmedItems)\n }\n },\n onConfirmAll: () => {\n const { confirmedItems } = confirmUploadFileWithNewVersion(\n ...filesToConfirmNewVersion,\n )\n\n void startUpload(...confirmedItems)\n },\n onSkip: () => {\n const { confirmedItems, pendingItems } =\n skipFileRequiringNewVersion(fileToPrompt)\n\n if (confirmedItems.length > 0 && pendingItems.length == 0) {\n void startUpload(...confirmedItems)\n }\n },\n onCancelAll: () => {\n clearPendingFiles()\n },\n }\n })\n }, [\n clearPendingFiles,\n confirmUploadFileWithNewVersion,\n filesToConfirmNewVersion,\n skipFileRequiringNewVersion,\n startUpload,\n ])\n\n // Override the uploader state with the checks custom to this hook\n const uploaderStateOverride = useMemo<EntityUploaderState>(() => {\n if (errorMessage) {\n return 'ERROR'\n }\n if (isLoadingUploadDestination) {\n return 'LOADING'\n }\n if (activePrompts.length > 0) {\n return 'PROMPT_USER'\n }\n return state\n }, [activePrompts.length, errorMessage, isLoadingUploadDestination, state])\n\n return useMemo(() => {\n return {\n state: uploaderStateOverride,\n errorMessage,\n isPrecheckingUpload,\n activeUploadCount,\n initiateUpload: initiateUpload,\n activePrompts: activePrompts,\n uploadProgress: uploadProgress,\n isUploadReady,\n }\n }, [\n uploaderStateOverride,\n errorMessage,\n isPrecheckingUpload,\n activeUploadCount,\n initiateUpload,\n activePrompts,\n uploadProgress,\n isUploadReady,\n ])\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"useUploadFileEntities.js","names":[],"sources":["../../../../src/utils/hooks/useUploadFileEntity/useUploadFileEntities.ts"],"sourcesContent":["import { SYNAPSE_STORAGE_LOCATION_ID } from '@/synapse-client/SynapseClient'\nimport {\n useCreateEntity,\n useGetDefaultUploadDestination,\n useUpdateEntity,\n} from '@/synapse-queries'\nimport {\n UploaderState,\n UploadItem,\n useUploadFiles,\n} from '@/utils/hooks/useUploadFileEntity/useUploadFiles'\nimport { FileEntity } from '@sage-bionetworks/synapse-types'\nimport { noop } from 'lodash-es'\nimport { useCallback, useMemo } from 'react'\nimport { useSynapseContext } from '../../context/index'\nimport useConfirmItems from '../useConfirmItems'\nimport {\n FilePreparedForUpload,\n PrepareDirsForUploadReturn,\n PrepareFileEntityUploadArgs,\n UpdateEntityFilePreparedForUpload,\n usePrepareFileEntityUpload,\n} from './usePrepareFileEntityUpload'\nimport { willUploadsExceedStorageLimit } from './willUploadsExceedStorageLimit'\n\nexport type PromptInfo = {\n type: 'CONFIRM_NEW_VERSION'\n fileName: string\n existingEntityId: string\n}\n\nexport type Prompt = {\n info: PromptInfo\n onConfirmAll: () => void\n onConfirm: () => void\n onSkip: () => void\n onCancelAll: () => void\n}\n\nexport type EntityUploaderState =\n | UploaderState\n | 'LOADING'\n | 'PROMPT_USER'\n | 'ERROR'\n\nexport type InitiateUploadArgs = PrepareFileEntityUploadArgs\n\nexport type UseUploadFileEntitiesReturn = {\n /**\n * The current state of the uploader\n */\n state: EntityUploaderState\n errorMessage?: string\n /**\n * True if the uploader is doing work to prepare the upload (e.g. creating folders, checking for existing files), but the upload has not started.\n */\n isPrecheckingUpload: boolean\n /**\n * Prompts that require user input before the upload can proceed. Typically, these are prompts to confirm uploading a new version\n * of an existing FileEntity.\n *\n * If prompts are present, `state` will always be 'PROMPT_USER'.\n */\n activePrompts: Prompt[]\n /**\n * Arguments used to initialize an upload operation. In addition to providing a file, the caller must also provide one of\n * the following:\n * - rootContainerId: The ID of the parent Project or Folder to upload files to. If the File objects include a webkitRelativePath,\n * then the Files will be uploaded into created sub-folders to match the relative path. Any files that match on path and file name\n * will trigger a prompt to confirm updating a new version in the `activePrompts` field, which must be resolved before the upload can proceed.\n * - existingEntityId: The ID of the FileEntity for which a new version should be uploaded. No prompts will be triggered by this option.\n */\n initiateUpload: (args: InitiateUploadArgs) => void\n /**\n * The total number of files that are actively being uploaded (PREPARING, UPLOADING, or PAUSED)\n */\n activeUploadCount: number\n /**\n * A list of each file being uploaded, along with its progress, status, and callbacks that can be used to pause, resume, or cancel the upload.\n */\n uploadProgress: UploadItem[]\n /** True when files can be uploaded. */\n isUploadReady: boolean\n}\n\n/**\n * Hook to start and track the progress of files uploads in Synapse, creating/updating a FileEntity for each uploaded file.\n *\n * To start an upload, see `initiateUpload` returned by this hook.\n */\nexport function useUploadFileEntities(\n /** The ID of the parent entity to upload files to, or the FileEntity for which a new version should be uploaded */\n containerOrEntityId: string,\n /** Optional accessKey for a direct S3 upload */\n accessKey = '',\n /** Optional secretKey for a direct S3 upload */\n secretKey = '',\n /** Invoked if chosen files will exceed storage limits based on a client-side check. */\n onStorageLimitExceeded: () => void = noop,\n): UseUploadFileEntitiesReturn {\n /**\n * General flow of the internal logic in this hook is\n *\n * 1. `initiateUpload(files)` - called by the caller of the hook\n * - Verify that the upload can proceed. Specifically:\n * 1. Validate the upload destination\n * 2. Check if the uploads will exceed a storage limit\n * - If either of these checks fail, do not proceed.\n * 2. `prepareDirsForUpload(files)` - sets up the folder paths, checks for existing entities with the same name\n * 3. `startUploadOrPromptForConfirmation(files)`\n * - For each file, check if the user should be prompted before the upload starts (e.g. if a new version will be created to update an existing file)\n * - If one or more prompts are required, they are added to the `activePrompts` array. Once the user resolves all prompts, the upload may start.\n * - If no prompts are required, the upload starts immediately.\n * 6. `onUploadComplete(file, fileHandleId)` - using the file handle, creates or updates the FileEntity in Synapse\n */\n\n const { synapseClient } = useSynapseContext()\n\n const { mutateAsync: createEntityWithNewFile } = useCreateEntity()\n const { mutateAsync: updateEntityWithNewFile } = useUpdateEntity()\n\n const onUploadComplete = useCallback(\n async function onUploadComplete(\n preparedFile: FilePreparedForUpload,\n newFileHandleId: string,\n ) {\n if ('existingEntityId' in preparedFile && preparedFile.existingEntityId) {\n // Update the existing entity\n const entity =\n await synapseClient.entityServicesClient.getRepoV1EntityId({\n id: preparedFile.existingEntityId,\n })\n await updateEntityWithNewFile({\n ...entity,\n dataFileHandleId: newFileHandleId,\n } as FileEntity)\n } else if ('parentId' in preparedFile) {\n // else, it's a new file entity\n const newFileEntity: FileEntity = {\n parentId: preparedFile.parentId,\n name: preparedFile.file.name,\n concreteType: 'org.sagebionetworks.repo.model.FileEntity',\n dataFileHandleId: newFileHandleId,\n }\n await createEntityWithNewFile(newFileEntity)\n } else {\n // this should never happen\n throw new Error(\n `Can't upload file without a parent ID or existing entity ID. File was: ${JSON.stringify(\n preparedFile,\n )}`,\n )\n }\n },\n [\n createEntityWithNewFile,\n synapseClient.entityServicesClient,\n updateEntityWithNewFile,\n ],\n )\n\n /*\n TODO: We retrieve the upload destination from the root upload container, but what if:\n 1. The user is uploading a folder with nested subdirectories\n 2. One or more of the subdirectories already exists\n 3. An existing subdirectory has a different upload destination\n The current implementation uploads everything into the root upload destination, but the correct behavior might be\n to fetch the upload destination for each subdirectory.\n */\n const {\n data: uploadDestination,\n isLoading: isLoadingUploadDestination,\n error: getUploadDestinationError,\n } = useGetDefaultUploadDestination(containerOrEntityId, {\n // Storage location usage is eventually consistent. We will keep track of the sum of uploaded files client-side\n // to determine if subsequent uploads will exceed the storage limit.\n // Do not refetch the storage location usage to avoid double-counting the size of new uploads against the limit.\n staleTime: Infinity,\n })\n\n const isUploadReady = Boolean(containerOrEntityId && uploadDestination)\n\n const storageLocationId =\n uploadDestination?.storageLocationId || SYNAPSE_STORAGE_LOCATION_ID\n\n const errorMessage = getUploadDestinationError?.message\n\n const {\n pendingItems: filesToConfirmNewVersion,\n confirmItem: confirmUploadFileWithNewVersion,\n addItemsPendingConfirmation: addFileToConfirmUploadNewVersion,\n removePendingItems: skipFileRequiringNewVersion,\n clear: clearPendingFiles,\n } = useConfirmItems<FilePreparedForUpload>()\n\n const onBeforeUpload = useCallback(() => {\n clearPendingFiles()\n }, [clearPendingFiles])\n\n const {\n state,\n startUpload,\n activeUploadCount,\n uploadProgress,\n bytesPendingUpload,\n } = useUploadFiles({\n onBeforeUpload,\n storageLocationId,\n uploadDestination,\n accessKey,\n secretKey,\n onUploadComplete,\n })\n\n /**\n * After all files have been prepared for upload, process them to determine if the upload can automatically continue.\n * If all files can be uploaded without user intervention, start the upload. Otherwise, prompt the user to make required\n * decisions to continue the upload.\n */\n const startUploadOrPromptForConfirmation = useCallback(\n (preparedFiles: PrepareDirsForUploadReturn) => {\n const { filesReadyForUpload, filesToPromptForNewVersion } = preparedFiles\n if (\n filesReadyForUpload.length > 0 &&\n filesToPromptForNewVersion.length == 0\n ) {\n // No need to prompt the user, just go ahead and upload!\n startUpload(...filesReadyForUpload)\n }\n\n if (filesToPromptForNewVersion.length > 0) {\n // Set up state to ask the user to confirm that they want to upload new versions.\n confirmUploadFileWithNewVersion(...filesReadyForUpload)\n addFileToConfirmUploadNewVersion(...filesToPromptForNewVersion)\n }\n },\n [\n addFileToConfirmUploadNewVersion,\n confirmUploadFileWithNewVersion,\n startUpload,\n ],\n )\n\n const { mutateAsync: prepareDirsForUpload, isPending: isPrecheckingUpload } =\n usePrepareFileEntityUpload()\n\n const initiateUpload = useCallback(\n async (args: InitiateUploadArgs) => {\n if (uploadDestination == null) {\n console.error(\n 'Upload destination was not loaded, or failed to load! Aborting upload.',\n )\n return\n }\n if (\n willUploadsExceedStorageLimit(\n args.map(arg => arg.file),\n uploadDestination.projectStorageLocationUsage,\n bytesPendingUpload,\n )\n ) {\n onStorageLimitExceeded()\n return\n }\n const filesReadyForUpload = await prepareDirsForUpload(args)\n\n startUploadOrPromptForConfirmation(filesReadyForUpload)\n },\n [\n bytesPendingUpload,\n onStorageLimitExceeded,\n startUploadOrPromptForConfirmation,\n prepareDirsForUpload,\n uploadDestination,\n ],\n )\n\n const activePrompts: Prompt[] = useMemo(() => {\n return filesToConfirmNewVersion.map(fileToPrompt => {\n return {\n info: {\n type: 'CONFIRM_NEW_VERSION',\n fileName: fileToPrompt.file.name,\n existingEntityId: (fileToPrompt as UpdateEntityFilePreparedForUpload)\n .existingEntityId,\n },\n onConfirm: () => {\n const { confirmedItems, pendingItems } =\n confirmUploadFileWithNewVersion(fileToPrompt)\n\n if (confirmedItems.length > 0 && pendingItems.length == 0) {\n void startUpload(...confirmedItems)\n }\n },\n onConfirmAll: () => {\n const { confirmedItems } = confirmUploadFileWithNewVersion(\n ...filesToConfirmNewVersion,\n )\n\n void startUpload(...confirmedItems)\n },\n onSkip: () => {\n const { confirmedItems, pendingItems } =\n skipFileRequiringNewVersion(fileToPrompt)\n\n if (confirmedItems.length > 0 && pendingItems.length == 0) {\n void startUpload(...confirmedItems)\n }\n },\n onCancelAll: () => {\n clearPendingFiles()\n },\n }\n })\n }, [\n clearPendingFiles,\n confirmUploadFileWithNewVersion,\n filesToConfirmNewVersion,\n skipFileRequiringNewVersion,\n startUpload,\n ])\n\n // Override the uploader state with the checks custom to this hook\n const uploaderStateOverride = useMemo<EntityUploaderState>(() => {\n if (errorMessage) {\n return 'ERROR'\n }\n if (isLoadingUploadDestination) {\n return 'LOADING'\n }\n if (activePrompts.length > 0) {\n return 'PROMPT_USER'\n }\n return state\n }, [activePrompts.length, errorMessage, isLoadingUploadDestination, state])\n\n return useMemo(() => {\n return {\n state: uploaderStateOverride,\n errorMessage,\n isPrecheckingUpload,\n activeUploadCount,\n initiateUpload: initiateUpload,\n activePrompts: activePrompts,\n uploadProgress: uploadProgress,\n isUploadReady,\n }\n }, [\n uploaderStateOverride,\n errorMessage,\n isPrecheckingUpload,\n activeUploadCount,\n initiateUpload,\n activePrompts,\n uploadProgress,\n isUploadReady,\n ])\n}\n"],"mappings":";;;;;;;;;;;;;AA0FA,SAAgB,EAEd,GAEA,IAAY,IAEZ,IAAY,IAEZ,IAAqC,GACR;CAiB7B,IAAM,EAAE,qBAAkB,GAAmB,EAEvC,EAAE,aAAa,MAA4B,GAAiB,EAC5D,EAAE,aAAa,MAA4B,GAAiB,EAE5D,IAAmB,EACvB,eACE,GACA,GACA;AACA,MAAI,sBAAsB,KAAgB,EAAa,iBAMrD,OAAM,EAAwB;GAC5B,GAAG,MAJG,EAAc,qBAAqB,kBAAkB,EACzD,IAAI,EAAa,kBAClB,CAAC;GAGF,kBAAkB;GACnB,CAAe;WACP,cAAc,EAQvB,OAAM,EAAwB;GAL5B,UAAU,EAAa;GACvB,MAAM,EAAa,KAAK;GACxB,cAAc;GACd,kBAAkB;GAEU,CAAc;MAG5C,OAAU,MACR,0EAA0E,KAAK,UAC7E,EACD,GACF;IAGL;EACE;EACA,EAAc;EACd;EACD,CACF,EAUK,EACJ,MAAM,GACN,WAAW,GACX,OAAO,MACL,EAA+B,GAAqB,EAItD,WAAW,UACZ,CAAC,EAEI,IAAgB,GAAQ,KAAuB,IAE/C,IACJ,GAAmB,qBAAA,GAEf,IAAe,GAA2B,SAE1C,EACJ,cAAc,GACd,aAAa,GACb,6BAA6B,GAC7B,oBAAoB,GACpB,OAAO,MACL,GAAwC,EAMtC,EACJ,UACA,gBACA,sBACA,mBACA,0BACE,EAAe;EACjB,gBAXqB,QAAkB;AACvC,MAAmB;KAClB,CAAC,EAAkB,CASpB;EACA;EACA;EACA;EACA;EACA;EACD,CAAC,EAOI,IAAqC,GACxC,MAA8C;EAC7C,IAAM,EAAE,wBAAqB,kCAA+B;AAS5D,EAPE,EAAoB,SAAS,KAC7B,EAA2B,UAAU,KAGrC,EAAY,GAAG,EAAoB,EAGjC,EAA2B,SAAS,MAEtC,EAAgC,GAAG,EAAoB,EACvD,EAAiC,GAAG,EAA2B;IAGnE;EACE;EACA;EACA;EACD,CACF,EAEK,EAAE,aAAa,GAAsB,WAAW,MACpD,GAA4B,EAExB,IAAiB,EACrB,OAAO,MAA6B;AAClC,MAAI,KAAqB,MAAM;AAC7B,WAAQ,MACN,yEACD;AACD;;AAEF,MACE,EACE,EAAK,KAAI,MAAO,EAAI,KAAK,EACzB,EAAkB,6BAClB,EACD,EACD;AACA,MAAwB;AACxB;;AAIF,IAAmC,MAFD,EAAqB,EAAK,CAEL;IAEzD;EACE;EACA;EACA;EACA;EACA;EACD,CACF,EAEK,IAA0B,QACvB,EAAyB,KAAI,OAC3B;EACL,MAAM;GACJ,MAAM;GACN,UAAU,EAAa,KAAK;GAC5B,kBAAmB,EAChB;GACJ;EACD,iBAAiB;GACf,IAAM,EAAE,mBAAgB,oBACtB,EAAgC,EAAa;AAE/C,GAAI,EAAe,SAAS,KAAK,EAAa,UAAU,KACjD,EAAY,GAAG,EAAe;;EAGvC,oBAAoB;GAClB,IAAM,EAAE,sBAAmB,EACzB,GAAG,EACJ;AAEI,KAAY,GAAG,EAAe;;EAErC,cAAc;GACZ,IAAM,EAAE,mBAAgB,oBACtB,EAA4B,EAAa;AAE3C,GAAI,EAAe,SAAS,KAAK,EAAa,UAAU,KACjD,EAAY,GAAG,EAAe;;EAGvC,mBAAmB;AACjB,MAAmB;;EAEtB,EACD,EACD;EACD;EACA;EACA;EACA;EACA;EACD,CAAC,EAGI,IAAwB,QACxB,IACK,UAEL,IACK,YAEL,EAAc,SAAS,IAClB,gBAEF,GACN;EAAC,EAAc;EAAQ;EAAc;EAA4B;EAAM,CAAC;AAE3E,QAAO,SACE;EACL,OAAO;EACP;EACA;EACA;EACgB;EACD;EACC;EAChB;EACD,GACA;EACD;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CAAC"}