synapse-react-client 4.0.24 → 4.0.25

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 (71) 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/components/AccessRequirementList/ManagedACTAccessRequirementRequestFlow/DataAccessRequestAccessorsFilesForm/DataAccessRequestAccessorsFilesForm.js.map +1 -1
  6. package/dist/components/AclEditor/useUpdateAcl.js.map +1 -1
  7. package/dist/components/Authentication/TwoFactorBackupCodes.js.map +1 -1
  8. package/dist/components/Authentication/TwoFactorEnrollmentForm.js.map +1 -1
  9. package/dist/components/Breadcrumbs/Breadcrumbs.d.ts +3 -0
  10. package/dist/components/Breadcrumbs/Breadcrumbs.d.ts.map +1 -1
  11. package/dist/components/Breadcrumbs/Breadcrumbs.js +43 -30
  12. package/dist/components/Breadcrumbs/Breadcrumbs.js.map +1 -1
  13. package/dist/components/CannedRejectionDialog/CannedRejectionDialog.js.map +1 -1
  14. package/dist/components/ChallengeSubmission/SubmissionDirectoryList.js.map +1 -1
  15. package/dist/components/CopyToClipboardString/CopyToClipboardString.d.ts +7 -1
  16. package/dist/components/CopyToClipboardString/CopyToClipboardString.d.ts.map +1 -1
  17. package/dist/components/CopyToClipboardString/CopyToClipboardString.js +22 -16
  18. package/dist/components/CopyToClipboardString/CopyToClipboardString.js.map +1 -1
  19. package/dist/components/EntityFinder/tree/EntityTree.js.map +1 -1
  20. package/dist/components/EntityFinder/tree/VirtualizedTree.js.map +1 -1
  21. package/dist/components/EntityIcon.d.ts +1 -0
  22. package/dist/components/EntityIcon.d.ts.map +1 -1
  23. package/dist/components/EntityIcon.js +37 -36
  24. package/dist/components/EntityIcon.js.map +1 -1
  25. package/dist/components/EntitySidebar/EntitySidebar.css +1 -0
  26. package/dist/components/EntitySidebar/EntitySidebar.d.ts +7 -0
  27. package/dist/components/EntitySidebar/EntitySidebar.d.ts.map +1 -0
  28. package/dist/components/EntitySidebar/EntitySidebar.js +62 -0
  29. package/dist/components/EntitySidebar/EntitySidebar.js.map +1 -0
  30. package/dist/components/EntitySidebar/EntitySidebar.module.js +17 -0
  31. package/dist/components/EntitySidebar/EntitySidebar.module.js.map +1 -0
  32. package/dist/components/EntitySidebar/EntitySidebar.module.scss +87 -0
  33. package/dist/components/Evaluation/EvaluationRoundEditorList.js.map +1 -1
  34. package/dist/components/GenericCard/BioregistryRules.js.map +1 -1
  35. package/dist/components/IconSvg/IconSvg.d.ts +2 -1
  36. package/dist/components/IconSvg/IconSvg.d.ts.map +1 -1
  37. package/dist/components/IconSvg/IconSvg.js +112 -89
  38. package/dist/components/IconSvg/IconSvg.js.map +1 -1
  39. package/dist/components/Plot/UpsetPlot.js.map +1 -1
  40. package/dist/components/SchemaDrivenAnnotationEditor/field/AdditionalPropertiesSchemaField.js.map +1 -1
  41. package/dist/components/StorybookComponentWrapper.js.map +1 -1
  42. package/dist/components/SynapseTable/TablePagination.js.map +1 -1
  43. package/dist/components/SynapseTable/datasets/DatasetItemsEditor.js.map +1 -1
  44. package/dist/components/TableColumnSchemaEditor/TableColumnSchemaForm.js.map +1 -1
  45. package/dist/components/TanStackTable/StyledTanStackTable.js.map +1 -1
  46. package/dist/components/entity/page/breadcrumbs/EntityPageBreadcrumbs.d.ts +1 -8
  47. package/dist/components/entity/page/breadcrumbs/EntityPageBreadcrumbs.d.ts.map +1 -1
  48. package/dist/components/entity/page/breadcrumbs/EntityPageBreadcrumbs.js +17 -12
  49. package/dist/components/entity/page/breadcrumbs/EntityPageBreadcrumbs.js.map +1 -1
  50. package/dist/components/entity/page/title_bar/EntityPageTitleBar.d.ts.map +1 -1
  51. package/dist/components/entity/page/title_bar/EntityPageTitleBar.js +20 -16
  52. package/dist/components/entity/page/title_bar/EntityPageTitleBar.js.map +1 -1
  53. package/dist/components/entity/page/title_bar/TitleBarProperties.d.ts +0 -1
  54. package/dist/components/entity/page/title_bar/TitleBarProperties.d.ts.map +1 -1
  55. package/dist/components/entity/page/title_bar/TitleBarProperties.js +17 -82
  56. package/dist/components/entity/page/title_bar/TitleBarProperties.js.map +1 -1
  57. package/dist/components/entity/page/title_bar/useGetEntityTitleBarProperties.d.ts.map +1 -1
  58. package/dist/components/entity/page/title_bar/useGetEntityTitleBarProperties.js +3 -4
  59. package/dist/components/entity/page/title_bar/useGetEntityTitleBarProperties.js.map +1 -1
  60. package/dist/components/menu/ComplexMenu.d.ts.map +1 -1
  61. package/dist/components/menu/ComplexMenu.js +5 -3
  62. package/dist/components/menu/ComplexMenu.js.map +1 -1
  63. package/dist/testutils/ReactQueryMockUtils.js.map +1 -1
  64. package/dist/tsconfig.build.tsbuildinfo +1 -1
  65. package/dist/types/ThemeTypes.d.ts +2 -2
  66. package/dist/utils/functions/IsEqualTreatUndefinedAsMissing.js.map +1 -1
  67. package/dist/utils/hooks/useDebouncedEffect.js.map +1 -1
  68. package/dist/utils/hooks/useDetectSSOCode.js.map +1 -1
  69. package/dist/utils/hooks/useImmutableTableQuery/useImmutableTableQuery.js.map +1 -1
  70. package/dist/utils/hooks/useLogin.js.map +1 -1
  71. package/package.json +5 -5
@@ -1 +1 @@
1
- {"version":3,"file":"CannedRejectionDialog.js","names":[],"sources":["../../../src/components/CannedRejectionDialog/CannedRejectionDialog.tsx"],"sourcesContent":["import { useGetFullTableQueryResults } from '@/synapse-queries'\nimport { BUNDLE_MASK_QUERY_RESULTS } from '@/utils/SynapseConstants'\nimport {\n Checkbox,\n Collapse,\n FormControl,\n FormControlLabel,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { SynapseClientError } from '@sage-bionetworks/synapse-client/util/SynapseClientError'\nimport { QueryResultBundle, Row } from '@sage-bionetworks/synapse-types'\nimport { UseQueryResult } from '@tanstack/react-query'\nimport { Set } from 'immutable'\nimport { Dispatch, ReactNode, SetStateAction, useEffect, useState } from 'react'\nimport ConfirmationDialog from '../ConfirmationDialog'\nimport { ErrorBanner } from '../error/ErrorBanner'\nimport FullWidthAlert from '../FullWidthAlert/FullWidthAlert'\nimport IconSvg from '../IconSvg/IconSvg'\nimport { SynapseSpinner } from '../LoadingScreen/LoadingScreen'\n\nconst CATEGORY_COLUMN_NAME = 'category'\nconst CATEGORY_SECTION_EMAIL_TEXT_COLUMN_NAME = 'category email prompt'\nconst REJECTION_REASON_FORM_TEXT_COLUMN_NAME = 'rejection reason'\nconst REJECTION_REASON_EMAIL_TEXT_COLUMN_NAME = 'email text'\n\nexport type CannedRejectionDialogProps = {\n open: boolean\n /* SynID of the table which contains the email responses which should populate this modal */\n tableId: string\n /* Callback invoked when the rejection is confirmed */\n onConfirm: (message: string) => void\n onClose: () => void\n /** Text injected into the beginning of the email template */\n defaultMessagePrefix?: string\n /** Text appended onto the end of the email template */\n defaultMessageAppend?: string\n /** Text to display above the canned rejection reasons */\n rejectionFormPromptCopy?: string\n /** Error to display, in case one occurred in the callback to `onReject` */\n error: SynapseClientError | null\n /** Optional children, can be used to extend the form as needed on step 2 */\n children?: ReactNode\n}\n\ntype RejectionCategoryProps = {\n category: string\n rows: Row[]\n rejectionReasonFormTextIndex: number\n selectedRowIds: Set<number>\n setSelectedRowIds: Dispatch<SetStateAction<Set<number>>>\n initialIsExpanded?: boolean\n}\n\ntype SelectRejectionReasonsFormProps = {\n tableQuery: UseQueryResult<QueryResultBundle, SynapseClientError>\n selectedRowIds: Set<number>\n setSelectedRowIds: Dispatch<SetStateAction<Set<number>>>\n rejectionFormPromptCopy?: string\n}\n\n/**\n * Renders a rejection reason category and checkboxes for each reason in the category\n */\nfunction RejectionCategory(props: RejectionCategoryProps) {\n const {\n category,\n rows,\n selectedRowIds,\n setSelectedRowIds,\n rejectionReasonFormTextIndex,\n initialIsExpanded = false,\n } = props\n const [isExpanded, setIsExpanded] = useState(initialIsExpanded)\n\n return (\n <>\n <Typography\n variant=\"body1\"\n onClick={() => setIsExpanded(!isExpanded)}\n sx={{ fontWeight: 700, cursor: 'pointer', my: 1 }}\n >\n <IconSvg\n icon={isExpanded ? 'expandMore' : 'chevronRight'}\n sx={{ color: 'grey.700' }}\n wrap={false}\n />\n {category}\n </Typography>\n <Collapse in={isExpanded}>\n <Stack sx={{ ml: 3 }}>\n {(rows ?? []).map(row => (\n <FormControlLabel\n key={row.rowId}\n control={\n <Checkbox\n checked={selectedRowIds.has(row.rowId!)}\n size={'small'}\n onChange={event => {\n if (event.target.checked) {\n setSelectedRowIds(selectedRowIds.add(row.rowId!))\n } else {\n setSelectedRowIds(selectedRowIds.remove(row.rowId!))\n }\n }}\n />\n }\n label={\n <Typography variant={'smallText1'}>\n {row.values[rejectionReasonFormTextIndex]}\n </Typography>\n }\n />\n ))}\n </Stack>\n </Collapse>\n </>\n )\n}\n\n/**\n * Renders a form for selecting individual rejection reasons grouped by category.\n * The rejection reason data comes from the table whose data is fetched in the tableQuery prop.\n */\nfunction SelectRejectionReasonsForm(props: SelectRejectionReasonsFormProps) {\n const {\n tableQuery,\n selectedRowIds,\n setSelectedRowIds,\n rejectionFormPromptCopy,\n } = props\n const { data: tableData, isLoading, error } = tableQuery\n const categoryIndex = tableData?.queryResult?.queryResults.headers.findIndex(\n header => header.name.toLowerCase() === CATEGORY_COLUMN_NAME,\n )\n const rejectionReasonFormTextIndex =\n tableData?.queryResult?.queryResults.headers.findIndex(\n header =>\n header.name.toLowerCase() === REJECTION_REASON_FORM_TEXT_COLUMN_NAME,\n )\n\n const rowsGroupedByCategory =\n tableData &&\n tableData.queryResult &&\n tableData.queryResult.queryResults.rows.reduce(\n (acc: Record<string, Row[]>, row) => {\n const category: string = row.values[categoryIndex!]!\n acc[category] = [...(acc[category] || []), row]\n return acc\n },\n {},\n )\n return (\n <>\n <Typography variant=\"headline3\" gutterBottom>\n Reasons for rejecting\n </Typography>\n {rejectionFormPromptCopy && (\n <Typography variant=\"body1\" gutterBottom>\n {rejectionFormPromptCopy}\n </Typography>\n )}\n {isLoading && (\n <Stack sx={{ my: 2 }}>\n <SynapseSpinner size={30} />\n </Stack>\n )}\n {error && <ErrorBanner error={error} />}\n {rowsGroupedByCategory && (\n <FormControl>\n {Object.keys(rowsGroupedByCategory).map(category => (\n <RejectionCategory\n key={category}\n category={category}\n rows={rowsGroupedByCategory[category]}\n selectedRowIds={selectedRowIds}\n setSelectedRowIds={setSelectedRowIds}\n rejectionReasonFormTextIndex={rejectionReasonFormTextIndex!}\n initialIsExpanded={\n Object.keys(rowsGroupedByCategory).length === 1\n }\n />\n ))}\n </FormControl>\n )}\n <Typography variant=\"headline3\" sx={{ mt: 1 }} gutterBottom>\n We’ll generate a response email message based on your selections.\n </Typography>\n <Typography variant=\"body1\" gutterBottom>\n If your reasons for rejecting are not shown here, that’s okay! You can\n edit the complete text of the message on the next screen before sending\n it.\n </Typography>\n </>\n )\n}\n\ntype DraftRejectionMessageProps = {\n emailText: string\n setEmailText: Dispatch<SetStateAction<string>>\n}\n\n/**\n * Presents a text field form that can be used to directly modify the rejection message.\n */\nfunction DraftRejectionMessage(props: DraftRejectionMessageProps) {\n const { emailText, setEmailText } = props\n return (\n <>\n <Typography variant=\"headline3\" gutterBottom>\n Edit the text of the rejection message\n </Typography>\n <Typography variant=\"body1\" gutterBottom>\n This message will be sent to the requester. You may edit it, or add\n custom text to the message.\n </Typography>\n <TextField\n multiline\n fullWidth\n rows={15}\n value={emailText}\n onChange={event => {\n setEmailText(event.target.value)\n }}\n />\n </>\n )\n}\ntype RejectionMessageObject = {\n [category: string]: {\n sectionText: string\n reasons: string[]\n }\n}\n\n/**\n * Modal component presented to a data access submission reviewer when they decide to reject a request.\n * The modal contains a form for selecting rejection reasons and a text field for editing the rejection message.\n * After crafting a message, the user can reject the submission and send the message to the requester.\n */\nexport function CannedRejectionDialog(props: CannedRejectionDialogProps) {\n const {\n open,\n tableId,\n onClose,\n onConfirm: onReject,\n defaultMessagePrefix = '',\n defaultMessageAppend = '',\n rejectionFormPromptCopy,\n children,\n error,\n } = props\n\n const [step, setStep] = useState<1 | 2>(1)\n // selectedRowIds are the row IDs of the canned responses the user selected:\n const [emailText, setEmailText] = useState('')\n const [selectedRowIds, setSelectedRowIds] = useState(Set<number>())\n\n // Fetch the table data\n const tableQuery = useGetFullTableQueryResults({\n entityId: tableId,\n query: {\n sql: `SELECT * FROM ${tableId}`,\n },\n partMask: BUNDLE_MASK_QUERY_RESULTS,\n concreteType: 'org.sagebionetworks.repo.model.table.QueryBundleRequest',\n })\n\n const { data } = tableQuery\n\n const categoryIndex = data?.queryResult?.queryResults.headers.findIndex(\n header => header.name.toLowerCase() === CATEGORY_COLUMN_NAME,\n )\n const categorySectionEmailTextIndex =\n data?.queryResult?.queryResults.headers.findIndex(\n header =>\n header.name.toLowerCase() === CATEGORY_SECTION_EMAIL_TEXT_COLUMN_NAME,\n )\n\n const reasonEmailTextIndex =\n data?.queryResult?.queryResults.headers.findIndex(\n header =>\n header.name.toLowerCase() === REJECTION_REASON_EMAIL_TEXT_COLUMN_NAME,\n )\n\n // Transform the selected rejection reasons into an object that can be easily transformed into an email\n const defaultEmailMessageObject: RejectionMessageObject | undefined =\n data &&\n data.queryResult &&\n selectedRowIds.reduce((messageObject: RejectionMessageObject, rowId) => {\n const row = data.queryResult!.queryResults.rows.find(\n row => row.rowId === rowId,\n )!\n const category = row.values[categoryIndex!]!\n const sectionText = row.values[categorySectionEmailTextIndex!]!\n const reasonText = row.values[reasonEmailTextIndex!]!\n if (messageObject[category]) {\n messageObject[category].reasons = [\n ...messageObject[category].reasons,\n reasonText,\n ]\n } else {\n messageObject[category] = {\n sectionText: sectionText,\n reasons: [reasonText],\n }\n }\n return messageObject\n }, {})\n\n // Using the rejection message object, create the email template.\n const defaultEmailMessage =\n defaultEmailMessageObject &&\n defaultMessagePrefix +\n Object.keys(defaultEmailMessageObject).reduce((message, key) => {\n message += '\\n'\n const sectionText = defaultEmailMessageObject[key].sectionText\n if (sectionText) {\n message += sectionText + '\\n'\n }\n\n for (const reason of defaultEmailMessageObject[key].reasons) {\n if (reason != null) {\n message += '\\n* ' + reason.replaceAll('\\\\n', '\\n') + '\\n'\n }\n }\n return message\n }, '') +\n defaultMessageAppend\n\n /* If the selected rows change, then reset the email text. */\n useEffect(() => {\n if (defaultEmailMessage) {\n setEmailText(defaultEmailMessage)\n }\n // Specifically fire on update to just selectedRowIds\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [selectedRowIds])\n\n // If fetching/processing the table fails, gracefully fall back to just show the email template\n return (\n <ConfirmationDialog\n open={open}\n onCancel={() => {\n if (step === 1) {\n onClose()\n }\n if (step === 2) {\n setStep(1)\n }\n }}\n maxWidth={'md'}\n fullWidth\n title=\"Reject Request?\"\n content={\n <>\n {step === 1 && (\n <SelectRejectionReasonsForm\n tableQuery={tableQuery}\n selectedRowIds={selectedRowIds}\n setSelectedRowIds={setSelectedRowIds}\n rejectionFormPromptCopy={rejectionFormPromptCopy}\n />\n )}\n {step === 2 && (\n <>\n <DraftRejectionMessage\n emailText={emailText}\n setEmailText={setEmailText}\n />\n {children}\n </>\n )}\n {error && (\n <FullWidthAlert\n variant={'danger'}\n description={error.reason}\n isGlobal={false}\n />\n )}\n </>\n }\n onConfirm={() => {\n if (step === 1) {\n setStep(2)\n }\n if (step === 2) {\n onReject(emailText)\n }\n }}\n confirmButtonProps={{\n children: step === 1 ? 'Generate Email' : 'Reject and Notify Requester',\n }}\n cancelButtonProps={{\n children: step === 1 ? 'Cancel' : 'Back',\n }}\n />\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"CannedRejectionDialog.js","names":[],"sources":["../../../src/components/CannedRejectionDialog/CannedRejectionDialog.tsx"],"sourcesContent":["import { useGetFullTableQueryResults } from '@/synapse-queries'\nimport { BUNDLE_MASK_QUERY_RESULTS } from '@/utils/SynapseConstants'\nimport {\n Checkbox,\n Collapse,\n FormControl,\n FormControlLabel,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { SynapseClientError } from '@sage-bionetworks/synapse-client/util/SynapseClientError'\nimport { QueryResultBundle, Row } from '@sage-bionetworks/synapse-types'\nimport { UseQueryResult } from '@tanstack/react-query'\nimport { Set } from 'immutable'\nimport { Dispatch, ReactNode, SetStateAction, useEffect, useState } from 'react'\nimport ConfirmationDialog from '../ConfirmationDialog'\nimport { ErrorBanner } from '../error/ErrorBanner'\nimport FullWidthAlert from '../FullWidthAlert/FullWidthAlert'\nimport IconSvg from '../IconSvg/IconSvg'\nimport { SynapseSpinner } from '../LoadingScreen/LoadingScreen'\n\nconst CATEGORY_COLUMN_NAME = 'category'\nconst CATEGORY_SECTION_EMAIL_TEXT_COLUMN_NAME = 'category email prompt'\nconst REJECTION_REASON_FORM_TEXT_COLUMN_NAME = 'rejection reason'\nconst REJECTION_REASON_EMAIL_TEXT_COLUMN_NAME = 'email text'\n\nexport type CannedRejectionDialogProps = {\n open: boolean\n /* SynID of the table which contains the email responses which should populate this modal */\n tableId: string\n /* Callback invoked when the rejection is confirmed */\n onConfirm: (message: string) => void\n onClose: () => void\n /** Text injected into the beginning of the email template */\n defaultMessagePrefix?: string\n /** Text appended onto the end of the email template */\n defaultMessageAppend?: string\n /** Text to display above the canned rejection reasons */\n rejectionFormPromptCopy?: string\n /** Error to display, in case one occurred in the callback to `onReject` */\n error: SynapseClientError | null\n /** Optional children, can be used to extend the form as needed on step 2 */\n children?: ReactNode\n}\n\ntype RejectionCategoryProps = {\n category: string\n rows: Row[]\n rejectionReasonFormTextIndex: number\n selectedRowIds: Set<number>\n setSelectedRowIds: Dispatch<SetStateAction<Set<number>>>\n initialIsExpanded?: boolean\n}\n\ntype SelectRejectionReasonsFormProps = {\n tableQuery: UseQueryResult<QueryResultBundle, SynapseClientError>\n selectedRowIds: Set<number>\n setSelectedRowIds: Dispatch<SetStateAction<Set<number>>>\n rejectionFormPromptCopy?: string\n}\n\n/**\n * Renders a rejection reason category and checkboxes for each reason in the category\n */\nfunction RejectionCategory(props: RejectionCategoryProps) {\n const {\n category,\n rows,\n selectedRowIds,\n setSelectedRowIds,\n rejectionReasonFormTextIndex,\n initialIsExpanded = false,\n } = props\n const [isExpanded, setIsExpanded] = useState(initialIsExpanded)\n\n return (\n <>\n <Typography\n variant=\"body1\"\n onClick={() => setIsExpanded(!isExpanded)}\n sx={{ fontWeight: 700, cursor: 'pointer', my: 1 }}\n >\n <IconSvg\n icon={isExpanded ? 'expandMore' : 'chevronRight'}\n sx={{ color: 'grey.700' }}\n wrap={false}\n />\n {category}\n </Typography>\n <Collapse in={isExpanded}>\n <Stack sx={{ ml: 3 }}>\n {(rows ?? []).map(row => (\n <FormControlLabel\n key={row.rowId}\n control={\n <Checkbox\n checked={selectedRowIds.has(row.rowId!)}\n size={'small'}\n onChange={event => {\n if (event.target.checked) {\n setSelectedRowIds(selectedRowIds.add(row.rowId!))\n } else {\n setSelectedRowIds(selectedRowIds.remove(row.rowId!))\n }\n }}\n />\n }\n label={\n <Typography variant={'smallText1'}>\n {row.values[rejectionReasonFormTextIndex]}\n </Typography>\n }\n />\n ))}\n </Stack>\n </Collapse>\n </>\n )\n}\n\n/**\n * Renders a form for selecting individual rejection reasons grouped by category.\n * The rejection reason data comes from the table whose data is fetched in the tableQuery prop.\n */\nfunction SelectRejectionReasonsForm(props: SelectRejectionReasonsFormProps) {\n const {\n tableQuery,\n selectedRowIds,\n setSelectedRowIds,\n rejectionFormPromptCopy,\n } = props\n const { data: tableData, isLoading, error } = tableQuery\n const categoryIndex = tableData?.queryResult?.queryResults.headers.findIndex(\n header => header.name.toLowerCase() === CATEGORY_COLUMN_NAME,\n )\n const rejectionReasonFormTextIndex =\n tableData?.queryResult?.queryResults.headers.findIndex(\n header =>\n header.name.toLowerCase() === REJECTION_REASON_FORM_TEXT_COLUMN_NAME,\n )\n\n const rowsGroupedByCategory =\n tableData &&\n tableData.queryResult &&\n tableData.queryResult.queryResults.rows.reduce(\n (acc: Record<string, Row[]>, row) => {\n const category: string = row.values[categoryIndex!]!\n acc[category] = [...(acc[category] || []), row]\n return acc\n },\n {},\n )\n return (\n <>\n <Typography variant=\"headline3\" gutterBottom>\n Reasons for rejecting\n </Typography>\n {rejectionFormPromptCopy && (\n <Typography variant=\"body1\" gutterBottom>\n {rejectionFormPromptCopy}\n </Typography>\n )}\n {isLoading && (\n <Stack sx={{ my: 2 }}>\n <SynapseSpinner size={30} />\n </Stack>\n )}\n {error && <ErrorBanner error={error} />}\n {rowsGroupedByCategory && (\n <FormControl>\n {Object.keys(rowsGroupedByCategory).map(category => (\n <RejectionCategory\n key={category}\n category={category}\n rows={rowsGroupedByCategory[category]}\n selectedRowIds={selectedRowIds}\n setSelectedRowIds={setSelectedRowIds}\n rejectionReasonFormTextIndex={rejectionReasonFormTextIndex!}\n initialIsExpanded={\n Object.keys(rowsGroupedByCategory).length === 1\n }\n />\n ))}\n </FormControl>\n )}\n <Typography variant=\"headline3\" sx={{ mt: 1 }} gutterBottom>\n We’ll generate a response email message based on your selections.\n </Typography>\n <Typography variant=\"body1\" gutterBottom>\n If your reasons for rejecting are not shown here, that’s okay! You can\n edit the complete text of the message on the next screen before sending\n it.\n </Typography>\n </>\n )\n}\n\ntype DraftRejectionMessageProps = {\n emailText: string\n setEmailText: Dispatch<SetStateAction<string>>\n}\n\n/**\n * Presents a text field form that can be used to directly modify the rejection message.\n */\nfunction DraftRejectionMessage(props: DraftRejectionMessageProps) {\n const { emailText, setEmailText } = props\n return (\n <>\n <Typography variant=\"headline3\" gutterBottom>\n Edit the text of the rejection message\n </Typography>\n <Typography variant=\"body1\" gutterBottom>\n This message will be sent to the requester. You may edit it, or add\n custom text to the message.\n </Typography>\n <TextField\n multiline\n fullWidth\n rows={15}\n value={emailText}\n onChange={event => {\n setEmailText(event.target.value)\n }}\n />\n </>\n )\n}\ntype RejectionMessageObject = {\n [category: string]: {\n sectionText: string\n reasons: string[]\n }\n}\n\n/**\n * Modal component presented to a data access submission reviewer when they decide to reject a request.\n * The modal contains a form for selecting rejection reasons and a text field for editing the rejection message.\n * After crafting a message, the user can reject the submission and send the message to the requester.\n */\nexport function CannedRejectionDialog(props: CannedRejectionDialogProps) {\n const {\n open,\n tableId,\n onClose,\n onConfirm: onReject,\n defaultMessagePrefix = '',\n defaultMessageAppend = '',\n rejectionFormPromptCopy,\n children,\n error,\n } = props\n\n const [step, setStep] = useState<1 | 2>(1)\n // selectedRowIds are the row IDs of the canned responses the user selected:\n const [emailText, setEmailText] = useState('')\n const [selectedRowIds, setSelectedRowIds] = useState(Set<number>())\n\n // Fetch the table data\n const tableQuery = useGetFullTableQueryResults({\n entityId: tableId,\n query: {\n sql: `SELECT * FROM ${tableId}`,\n },\n partMask: BUNDLE_MASK_QUERY_RESULTS,\n concreteType: 'org.sagebionetworks.repo.model.table.QueryBundleRequest',\n })\n\n const { data } = tableQuery\n\n const categoryIndex = data?.queryResult?.queryResults.headers.findIndex(\n header => header.name.toLowerCase() === CATEGORY_COLUMN_NAME,\n )\n const categorySectionEmailTextIndex =\n data?.queryResult?.queryResults.headers.findIndex(\n header =>\n header.name.toLowerCase() === CATEGORY_SECTION_EMAIL_TEXT_COLUMN_NAME,\n )\n\n const reasonEmailTextIndex =\n data?.queryResult?.queryResults.headers.findIndex(\n header =>\n header.name.toLowerCase() === REJECTION_REASON_EMAIL_TEXT_COLUMN_NAME,\n )\n\n // Transform the selected rejection reasons into an object that can be easily transformed into an email\n const defaultEmailMessageObject: RejectionMessageObject | undefined =\n data &&\n data.queryResult &&\n selectedRowIds.reduce((messageObject: RejectionMessageObject, rowId) => {\n const row = data.queryResult!.queryResults.rows.find(\n row => row.rowId === rowId,\n )!\n const category = row.values[categoryIndex!]!\n const sectionText = row.values[categorySectionEmailTextIndex!]!\n const reasonText = row.values[reasonEmailTextIndex!]!\n if (messageObject[category]) {\n messageObject[category].reasons = [\n ...messageObject[category].reasons,\n reasonText,\n ]\n } else {\n messageObject[category] = {\n sectionText: sectionText,\n reasons: [reasonText],\n }\n }\n return messageObject\n }, {})\n\n // Using the rejection message object, create the email template.\n const defaultEmailMessage =\n defaultEmailMessageObject &&\n defaultMessagePrefix +\n Object.keys(defaultEmailMessageObject).reduce((message, key) => {\n message += '\\n'\n const sectionText = defaultEmailMessageObject[key].sectionText\n if (sectionText) {\n message += sectionText + '\\n'\n }\n\n for (const reason of defaultEmailMessageObject[key].reasons) {\n if (reason != null) {\n message += '\\n* ' + reason.replaceAll('\\\\n', '\\n') + '\\n'\n }\n }\n return message\n }, '') +\n defaultMessageAppend\n\n /* If the selected rows change, then reset the email text. */\n useEffect(() => {\n if (defaultEmailMessage) {\n setEmailText(defaultEmailMessage)\n }\n // Specifically fire on update to just selectedRowIds\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [selectedRowIds])\n\n // If fetching/processing the table fails, gracefully fall back to just show the email template\n return (\n <ConfirmationDialog\n open={open}\n onCancel={() => {\n if (step === 1) {\n onClose()\n }\n if (step === 2) {\n setStep(1)\n }\n }}\n maxWidth={'md'}\n fullWidth\n title=\"Reject Request?\"\n content={\n <>\n {step === 1 && (\n <SelectRejectionReasonsForm\n tableQuery={tableQuery}\n selectedRowIds={selectedRowIds}\n setSelectedRowIds={setSelectedRowIds}\n rejectionFormPromptCopy={rejectionFormPromptCopy}\n />\n )}\n {step === 2 && (\n <>\n <DraftRejectionMessage\n emailText={emailText}\n setEmailText={setEmailText}\n />\n {children}\n </>\n )}\n {error && (\n <FullWidthAlert\n variant={'danger'}\n description={error.reason}\n isGlobal={false}\n />\n )}\n </>\n }\n onConfirm={() => {\n if (step === 1) {\n setStep(2)\n }\n if (step === 2) {\n onReject(emailText)\n }\n }}\n confirmButtonProps={{\n children: step === 1 ? 'Generate Email' : 'Reject and Notify Requester',\n }}\n cancelButtonProps={{\n children: step === 1 ? 'Cancel' : 'Back',\n }}\n />\n )\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"SubmissionDirectoryList.js","names":[],"sources":["../../../src/components/ChallengeSubmission/SubmissionDirectoryList.tsx"],"sourcesContent":["import SynapseClient from '@/synapse-client'\nimport {\n invalidateAllQueriesForEntity,\n useGetEntities,\n useGetEntityChildren,\n} from '@/synapse-queries'\nimport { useSynapseContext } from '@/utils'\nimport { formatDate } from '@/utils/functions/DateFormatter'\nimport {\n BackendDestinationEnum,\n getEndpoint,\n} from '@/utils/functions/getEndpoint'\nimport { NotesTwoTone } from '@mui/icons-material'\nimport { Box, Button, Radio, Typography } from '@mui/material'\nimport { DataGrid, GridCellParams, GridColDef } from '@mui/x-data-grid'\nimport { EntityType } from '@sage-bionetworks/synapse-client'\nimport { SynapseClientError } from '@sage-bionetworks/synapse-client/util/SynapseClientError'\nimport {\n Direction,\n Entity,\n EntityChildrenRequest,\n EntityHeader,\n EntityLookupRequest,\n FILE_ENTITY_CONCRETE_TYPE_VALUE,\n FileEntity,\n FileUploadComplete,\n SortBy,\n UploadCallbackResp,\n} from '@sage-bionetworks/synapse-types'\nimport { useQueryClient } from '@tanstack/react-query'\nimport dayjs from 'dayjs'\nimport { useEffect, useMemo, useState } from 'react'\nimport { Link } from 'react-router'\nimport ConfirmationDialog from '../ConfirmationDialog'\nimport CopyToClipboardIcon from '../CopyToClipboardIcon'\nimport { ErrorBanner } from '../error/ErrorBanner'\nimport FileUpload from '../FileUpload'\nimport IconSvg from '../IconSvg'\nimport { EntityItem } from './ChallengeSubmission'\n\ntype SubmissionDirectoryRow = {\n id: string\n name: string\n modifiedOn: string\n}\n\ntype SubmissionDirectoryListProps = {\n pageSize: number\n challengeProjectId: string\n entityType: typeof EntityType.dockerrepo | typeof EntityType.file\n onItemSelected: (selected: EntityItem) => void\n}\n\ntype FileUploadAttempt = FileUploadComplete & {\n entityId?: string\n}\n\nfunction SubmissionDirectoryList({\n pageSize,\n challengeProjectId,\n entityType,\n onItemSelected,\n}: SubmissionDirectoryListProps) {\n const queryClient = useQueryClient()\n const { accessToken, keyFactory } = useSynapseContext()\n const [page, setPage] = useState<number>(0)\n const [selectedItem, setSelectedItem] = useState<EntityItem | undefined>()\n const [errorMessage, setErrorMessage] = useState<string>()\n const [canSubmit, setCanSubmit] = useState<boolean>()\n const [fetchedHeaders, setFetchedHeaders] = useState<EntityHeader[]>([])\n const [nextPageToken, setNextPageToken] = useState<string | undefined>()\n const [fetchNextPage, setFetchNextPage] = useState<boolean>(false)\n const [confirmOpen, setConfirmOpen] = useState<boolean>(false)\n const [uploadAttempt, setUploadAttempt] = useState<FileUploadAttempt>()\n\n const PER_PAGE = pageSize\n const HEADERS_PER_PAGE = 50\n const PROJECT_URL = `${getEndpoint(\n BackendDestinationEnum.PORTAL_ENDPOINT,\n )}Synapse:${challengeProjectId}`\n\n const request: EntityChildrenRequest = {\n parentId: challengeProjectId,\n nextPageToken: fetchNextPage ? nextPageToken : null,\n includeTypes: [entityType],\n includeTotalChildCount: true,\n sortBy: SortBy.MODIFIED_ON,\n sortDirection: Direction.DESC,\n }\n\n const { data: headerResults, refetch } = useGetEntityChildren(request, {\n enabled: !!challengeProjectId,\n throwOnError: true,\n })\n\n useEffect(() => {\n if (headerResults) {\n const newHeaders = [...fetchedHeaders]\n const headerPage = Math.floor(((page + 1) * PER_PAGE) / HEADERS_PER_PAGE)\n const start = headerPage * HEADERS_PER_PAGE\n newHeaders.splice(start, start + HEADERS_PER_PAGE, ...headerResults.page)\n setFetchedHeaders(newHeaders)\n setFetchNextPage(false)\n setNextPageToken(headerResults.nextPageToken)\n }\n // TODO: Temporary useEffect hook to remove onSuccess QueryOption for @tanstack/react-query v5\n // Refactor this component to remove this effect.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [headerResults])\n\n function getPageHeaders() {\n const pageStart = page * PER_PAGE\n const pageHeaders = fetchedHeaders.slice(pageStart, pageStart + PER_PAGE)\n return pageHeaders\n }\n\n function reset() {\n setErrorMessage(undefined)\n setCanSubmit(undefined)\n setFetchedHeaders([])\n setNextPageToken(undefined)\n setFetchNextPage(false)\n refetch()\n }\n\n useEffect(() => {\n reset()\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [entityType, pageSize])\n\n const queries = useGetEntities(getPageHeaders())\n const entities = useMemo(\n () =>\n queries\n .filter(q => q.status === 'success')\n .map(q => q.data) as EntityItem[],\n [queries],\n )\n const areEntitiesLoading = queries.some(q => q.isLoading)\n\n const entityChangeHandler = async (value: string) => {\n setCanSubmit(false)\n const entity = entities.find(entity => entity?.id === value)\n if (entity) {\n setSelectedItem(entity)\n if (entityType === EntityType.dockerrepo) {\n let commits\n try {\n commits = await SynapseClient.getDockerTag(\n entity.id!,\n accessToken,\n 0,\n 1,\n )\n } catch (e) {\n return setErrorMessage(e.message)\n }\n if (commits.totalNumberOfResults === 0) {\n return setErrorMessage(\n 'No commits have been made to this repository. Please select a repository with at least one commit.',\n )\n }\n }\n setCanSubmit(true)\n setErrorMessage(undefined)\n }\n }\n\n const columns: GridColDef[] = [\n {\n field: 'radiobutton',\n headerName: '',\n width: 25,\n sortable: false,\n filterable: false,\n hideable: false,\n disableColumnMenu: true,\n renderCell: params => {\n return (\n <Radio\n value={params.id}\n checked={params.id === selectedItem?.id}\n onChange={event => {\n entityChangeHandler(event.target.value)\n }}\n />\n )\n },\n },\n {\n field: 'name',\n headerName:\n entityType === EntityType.dockerrepo\n ? 'Docker Repository'\n : 'File Name',\n flex: 1,\n filterable: false,\n hideable: false,\n disableColumnMenu: true,\n renderCell: (params: GridCellParams<Entity, SubmissionDirectoryRow>) => (\n <Link\n to={{\n pathname: `${getEndpoint(\n BackendDestinationEnum.PORTAL_ENDPOINT,\n )}Synapse:${params.row.id}`,\n }}\n target=\"_blank\"\n >\n {params.row.name}\n </Link>\n ),\n },\n {\n field: 'modifiedOn',\n headerName: 'Updated On',\n width: 100,\n filterable: false,\n hideable: false,\n disableColumnMenu: true,\n },\n {\n field: 'id',\n headerName: 'ID',\n width: 200,\n filterable: false,\n hideable: false,\n disableColumnMenu: true,\n },\n ]\n\n const getRows = (entities: EntityItem[]) => {\n const newRows: SubmissionDirectoryRow[] = []\n entities.forEach(entity => {\n newRows.push({\n id: entity.id!,\n name:\n entityType === EntityType.dockerrepo\n ? (entity.repositoryName ?? entity.name)\n : entity.name,\n modifiedOn: formatDate(dayjs(entity.modifiedOn), 'MM/DD/YY'),\n })\n })\n return newRows\n }\n\n const handlePageChange = (newPageNum: number) => {\n const lastIndexNeeded = Math.min(\n headerResults?.totalChildCount ?? 0,\n (newPageNum + 1) * PER_PAGE,\n )\n if (lastIndexNeeded > fetchedHeaders.length) {\n setFetchNextPage(true)\n }\n setPage(newPageNum)\n }\n\n const itemSelectedHandler = () => {\n onItemSelected(selectedItem!)\n }\n\n function createEntity(file: FileUploadAttempt) {\n // Create Entity\n if (!file) return\n const { fileHandleId, fileName } = file\n const newFileEntity: FileEntity = {\n parentId: challengeProjectId,\n name: fileName,\n concreteType: FILE_ENTITY_CONCRETE_TYPE_VALUE,\n dataFileHandleId: fileHandleId,\n }\n SynapseClient.createEntity(newFileEntity, accessToken)\n .then(() => {\n reset()\n })\n .catch((err: SynapseClientError) => {\n setErrorMessage(err.reason)\n })\n }\n\n async function updateEntity() {\n if (!uploadAttempt) return\n const { fileHandleId, entityId } = uploadAttempt\n if (!entityId)\n return setErrorMessage('Error: missing entityId. Please try again.')\n // Get the entity\n let entity: FileEntity\n try {\n entity = await SynapseClient.getEntity(accessToken, entityId)\n } catch (err) {\n return setErrorMessage(`The entity ${entityId} could not be retrieved.`)\n }\n\n const updateRequest: FileEntity = {\n id: entity.id,\n name: entity.name,\n dataFileHandleId: fileHandleId,\n concreteType: FILE_ENTITY_CONCRETE_TYPE_VALUE,\n parentId: entity.parentId,\n etag: entity.etag,\n modifiedOn: entity.modifiedOn,\n }\n\n try {\n const updatedEntity = await SynapseClient.updateEntity(\n updateRequest,\n accessToken,\n true,\n )\n await invalidateAllQueriesForEntity(\n queryClient,\n keyFactory,\n updatedEntity.id!,\n )\n queryClient.setQueryData(\n keyFactory.getEntityQueryKey(updatedEntity.id!),\n updatedEntity,\n )\n reset()\n } catch (err) {\n setErrorMessage(err.reason)\n }\n }\n\n const handleUpload = async (response: UploadCallbackResp) => {\n if (response.success && response.resp) {\n const { fileName } = response.resp\n\n // Lookup entity\n const entityLookupRequest: EntityLookupRequest = {\n entityName: fileName,\n parentId: challengeProjectId,\n }\n try {\n const entityId = await SynapseClient.lookupChildEntity(\n entityLookupRequest,\n accessToken,\n )\n // Entity exists, prompt user to update it\n if (entityId) {\n setUploadAttempt({ ...response.resp, entityId: entityId.id })\n setConfirmOpen(true)\n }\n } catch (err) {\n // An existing entity was not found for this file, create it\n setUploadAttempt(undefined)\n createEntity(response.resp)\n }\n } else if (!response.success && response.error) {\n setErrorMessage(response.error.reason as string)\n } else {\n setErrorMessage('An unknown error occurred. Please try again.')\n }\n }\n\n return (\n <Box>\n <Box\n sx={{\n display: 'flex',\n backgroundColor: '#FBFBFC',\n padding: '10px',\n justifyContent: 'space-between',\n }}\n >\n <Typography\n variant=\"h6\"\n sx={{ fontSize: '18px', lineHeight: '20px', fontWeight: 700 }}\n >\n {entityType === EntityType.file\n ? 'Submit File'\n : 'Submit Docker Image'}\n </Typography>\n <Box sx={{ display: 'flex' }}>\n <Typography\n variant=\"body1\"\n sx={{ fontSize: '14px', color: '#71767F' }}\n >\n Your Project SynID:{' '}\n <Link to={{ pathname: PROJECT_URL }} target=\"_blank\">\n {challengeProjectId}\n </Link>\n </Typography>\n <CopyToClipboardIcon value={PROJECT_URL} sx={{ marginTop: '-4px' }} />\n </Box>\n </Box>\n <Box\n sx={{\n my: 3,\n display: 'flex',\n }}\n >\n <NotesTwoTone\n sx={{\n width: '16px',\n height: '16px',\n verticalAlign: 'text-bottom',\n }}\n />\n <Box\n sx={{\n ml: 1,\n }}\n >\n {entityType === EntityType.file && (\n <>\n Choose a prediction file from your submission directory below and\n click \"Submit Selection\" to pick an evaluation queue. If you need\n to add a new file, click \"Upload File\" first.\n </>\n )}\n {entityType === EntityType.dockerrepo && (\n <>\n Choose a Docker image from your submission directory below and\n click \"Submit Selection\" to pick an image version and evaluation\n queue. If you need to upload a new image to your project (\n <code>{challengeProjectId}</code>), see our{' '}\n <Link\n to=\"https://github.com/Sage-Bionetworks-Challenges/sample-model-templates#build-your-model\"\n target=\"_blank\"\n >\n Docker model submission guide\n </Link>{' '}\n for command-line instructions on how to build and upload.\n </>\n )}\n </Box>\n </Box>\n <Box>\n <DataGrid\n loading={areEntitiesLoading}\n columns={columns}\n rows={getRows(entities)}\n rowCount={headerResults?.totalChildCount ?? 0}\n pagination\n paginationMode=\"server\"\n paginationModel={{ page, pageSize: PER_PAGE }}\n pageSizeOptions={[PER_PAGE]}\n onPaginationModelChange={({ page }) => {\n handlePageChange(page)\n }}\n density=\"compact\"\n autoHeight\n sx={{\n fontSize: '14px',\n border: 'none',\n height: '100%',\n '& .MuiDataGrid-columnHeader': {\n backgroundColor: '#F1F3F5',\n },\n '& .Mui-odd': {\n backgroundColor: '#FBFBFC',\n },\n '.MuiDataGrid-columnHeaderTitleContainer': {\n justifyContent: 'space-between',\n },\n '.radio': {\n display: 'flex',\n alignItems: 'center',\n height: '100%',\n },\n }}\n getRowClassName={params =>\n params.indexRelativeToCurrentPage % 2 === 0 ? 'Mui-even' : 'Mui-odd'\n }\n />\n </Box>\n {errorMessage && <ErrorBanner error={errorMessage}></ErrorBanner>}\n <Box\n sx={{\n display: 'flex',\n justifyContent: 'space-between',\n }}\n >\n {entityType === EntityType.file && (\n <FileUpload\n label=\"Upload File\"\n buttonProps={{\n variant: 'outlined',\n endIcon: <IconSvg icon=\"upload\" />,\n sx: { lineHeight: 1 },\n }}\n onComplete={resp => {\n handleUpload(resp)\n }}\n />\n )}\n <Button\n color=\"primary\"\n variant=\"contained\"\n onClick={itemSelectedHandler}\n disabled={!canSubmit}\n >\n Submit Selection\n </Button>\n </Box>\n <ConfirmationDialog\n open={confirmOpen}\n title=\"File exists\"\n content={\n <Typography variant=\"body1\" sx={{ fontSize: '16px' }}>\n A file named &quot;{uploadAttempt?.fileName}&quot; (\n {uploadAttempt?.entityId}) already exists in this location. Do you\n want to update the existing file and create a new version?\n </Typography>\n }\n onCancel={() => setConfirmOpen(false)}\n onConfirm={() => {\n updateEntity()\n setConfirmOpen(false)\n }}\n />\n </Box>\n )\n}\n\nexport default SubmissionDirectoryList\n"],"mappings":"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"}
1
+ {"version":3,"file":"SubmissionDirectoryList.js","names":[],"sources":["../../../src/components/ChallengeSubmission/SubmissionDirectoryList.tsx"],"sourcesContent":["import SynapseClient from '@/synapse-client'\nimport {\n invalidateAllQueriesForEntity,\n useGetEntities,\n useGetEntityChildren,\n} from '@/synapse-queries'\nimport { useSynapseContext } from '@/utils'\nimport { formatDate } from '@/utils/functions/DateFormatter'\nimport {\n BackendDestinationEnum,\n getEndpoint,\n} from '@/utils/functions/getEndpoint'\nimport { NotesTwoTone } from '@mui/icons-material'\nimport { Box, Button, Radio, Typography } from '@mui/material'\nimport { DataGrid, GridCellParams, GridColDef } from '@mui/x-data-grid'\nimport { EntityType } from '@sage-bionetworks/synapse-client'\nimport { SynapseClientError } from '@sage-bionetworks/synapse-client/util/SynapseClientError'\nimport {\n Direction,\n Entity,\n EntityChildrenRequest,\n EntityHeader,\n EntityLookupRequest,\n FILE_ENTITY_CONCRETE_TYPE_VALUE,\n FileEntity,\n FileUploadComplete,\n SortBy,\n UploadCallbackResp,\n} from '@sage-bionetworks/synapse-types'\nimport { useQueryClient } from '@tanstack/react-query'\nimport dayjs from 'dayjs'\nimport { useEffect, useMemo, useState } from 'react'\nimport { Link } from 'react-router'\nimport ConfirmationDialog from '../ConfirmationDialog'\nimport CopyToClipboardIcon from '../CopyToClipboardIcon'\nimport { ErrorBanner } from '../error/ErrorBanner'\nimport FileUpload from '../FileUpload'\nimport IconSvg from '../IconSvg'\nimport { EntityItem } from './ChallengeSubmission'\n\ntype SubmissionDirectoryRow = {\n id: string\n name: string\n modifiedOn: string\n}\n\ntype SubmissionDirectoryListProps = {\n pageSize: number\n challengeProjectId: string\n entityType: typeof EntityType.dockerrepo | typeof EntityType.file\n onItemSelected: (selected: EntityItem) => void\n}\n\ntype FileUploadAttempt = FileUploadComplete & {\n entityId?: string\n}\n\nfunction SubmissionDirectoryList({\n pageSize,\n challengeProjectId,\n entityType,\n onItemSelected,\n}: SubmissionDirectoryListProps) {\n const queryClient = useQueryClient()\n const { accessToken, keyFactory } = useSynapseContext()\n const [page, setPage] = useState<number>(0)\n const [selectedItem, setSelectedItem] = useState<EntityItem | undefined>()\n const [errorMessage, setErrorMessage] = useState<string>()\n const [canSubmit, setCanSubmit] = useState<boolean>()\n const [fetchedHeaders, setFetchedHeaders] = useState<EntityHeader[]>([])\n const [nextPageToken, setNextPageToken] = useState<string | undefined>()\n const [fetchNextPage, setFetchNextPage] = useState<boolean>(false)\n const [confirmOpen, setConfirmOpen] = useState<boolean>(false)\n const [uploadAttempt, setUploadAttempt] = useState<FileUploadAttempt>()\n\n const PER_PAGE = pageSize\n const HEADERS_PER_PAGE = 50\n const PROJECT_URL = `${getEndpoint(\n BackendDestinationEnum.PORTAL_ENDPOINT,\n )}Synapse:${challengeProjectId}`\n\n const request: EntityChildrenRequest = {\n parentId: challengeProjectId,\n nextPageToken: fetchNextPage ? nextPageToken : null,\n includeTypes: [entityType],\n includeTotalChildCount: true,\n sortBy: SortBy.MODIFIED_ON,\n sortDirection: Direction.DESC,\n }\n\n const { data: headerResults, refetch } = useGetEntityChildren(request, {\n enabled: !!challengeProjectId,\n throwOnError: true,\n })\n\n useEffect(() => {\n if (headerResults) {\n const newHeaders = [...fetchedHeaders]\n const headerPage = Math.floor(((page + 1) * PER_PAGE) / HEADERS_PER_PAGE)\n const start = headerPage * HEADERS_PER_PAGE\n newHeaders.splice(start, start + HEADERS_PER_PAGE, ...headerResults.page)\n setFetchedHeaders(newHeaders)\n setFetchNextPage(false)\n setNextPageToken(headerResults.nextPageToken)\n }\n // TODO: Temporary useEffect hook to remove onSuccess QueryOption for @tanstack/react-query v5\n // Refactor this component to remove this effect.\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [headerResults])\n\n function getPageHeaders() {\n const pageStart = page * PER_PAGE\n const pageHeaders = fetchedHeaders.slice(pageStart, pageStart + PER_PAGE)\n return pageHeaders\n }\n\n function reset() {\n setErrorMessage(undefined)\n setCanSubmit(undefined)\n setFetchedHeaders([])\n setNextPageToken(undefined)\n setFetchNextPage(false)\n refetch()\n }\n\n useEffect(() => {\n reset()\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [entityType, pageSize])\n\n const queries = useGetEntities(getPageHeaders())\n const entities = useMemo(\n () =>\n queries\n .filter(q => q.status === 'success')\n .map(q => q.data) as EntityItem[],\n [queries],\n )\n const areEntitiesLoading = queries.some(q => q.isLoading)\n\n const entityChangeHandler = async (value: string) => {\n setCanSubmit(false)\n const entity = entities.find(entity => entity?.id === value)\n if (entity) {\n setSelectedItem(entity)\n if (entityType === EntityType.dockerrepo) {\n let commits\n try {\n commits = await SynapseClient.getDockerTag(\n entity.id!,\n accessToken,\n 0,\n 1,\n )\n } catch (e) {\n return setErrorMessage(e.message)\n }\n if (commits.totalNumberOfResults === 0) {\n return setErrorMessage(\n 'No commits have been made to this repository. Please select a repository with at least one commit.',\n )\n }\n }\n setCanSubmit(true)\n setErrorMessage(undefined)\n }\n }\n\n const columns: GridColDef[] = [\n {\n field: 'radiobutton',\n headerName: '',\n width: 25,\n sortable: false,\n filterable: false,\n hideable: false,\n disableColumnMenu: true,\n renderCell: params => {\n return (\n <Radio\n value={params.id}\n checked={params.id === selectedItem?.id}\n onChange={event => {\n entityChangeHandler(event.target.value)\n }}\n />\n )\n },\n },\n {\n field: 'name',\n headerName:\n entityType === EntityType.dockerrepo\n ? 'Docker Repository'\n : 'File Name',\n flex: 1,\n filterable: false,\n hideable: false,\n disableColumnMenu: true,\n renderCell: (params: GridCellParams<Entity, SubmissionDirectoryRow>) => (\n <Link\n to={{\n pathname: `${getEndpoint(\n BackendDestinationEnum.PORTAL_ENDPOINT,\n )}Synapse:${params.row.id}`,\n }}\n target=\"_blank\"\n >\n {params.row.name}\n </Link>\n ),\n },\n {\n field: 'modifiedOn',\n headerName: 'Updated On',\n width: 100,\n filterable: false,\n hideable: false,\n disableColumnMenu: true,\n },\n {\n field: 'id',\n headerName: 'ID',\n width: 200,\n filterable: false,\n hideable: false,\n disableColumnMenu: true,\n },\n ]\n\n const getRows = (entities: EntityItem[]) => {\n const newRows: SubmissionDirectoryRow[] = []\n entities.forEach(entity => {\n newRows.push({\n id: entity.id!,\n name:\n entityType === EntityType.dockerrepo\n ? (entity.repositoryName ?? entity.name)\n : entity.name,\n modifiedOn: formatDate(dayjs(entity.modifiedOn), 'MM/DD/YY'),\n })\n })\n return newRows\n }\n\n const handlePageChange = (newPageNum: number) => {\n const lastIndexNeeded = Math.min(\n headerResults?.totalChildCount ?? 0,\n (newPageNum + 1) * PER_PAGE,\n )\n if (lastIndexNeeded > fetchedHeaders.length) {\n setFetchNextPage(true)\n }\n setPage(newPageNum)\n }\n\n const itemSelectedHandler = () => {\n onItemSelected(selectedItem!)\n }\n\n function createEntity(file: FileUploadAttempt) {\n // Create Entity\n if (!file) return\n const { fileHandleId, fileName } = file\n const newFileEntity: FileEntity = {\n parentId: challengeProjectId,\n name: fileName,\n concreteType: FILE_ENTITY_CONCRETE_TYPE_VALUE,\n dataFileHandleId: fileHandleId,\n }\n SynapseClient.createEntity(newFileEntity, accessToken)\n .then(() => {\n reset()\n })\n .catch((err: SynapseClientError) => {\n setErrorMessage(err.reason)\n })\n }\n\n async function updateEntity() {\n if (!uploadAttempt) return\n const { fileHandleId, entityId } = uploadAttempt\n if (!entityId)\n return setErrorMessage('Error: missing entityId. Please try again.')\n // Get the entity\n let entity: FileEntity\n try {\n entity = await SynapseClient.getEntity(accessToken, entityId)\n } catch (err) {\n return setErrorMessage(`The entity ${entityId} could not be retrieved.`)\n }\n\n const updateRequest: FileEntity = {\n id: entity.id,\n name: entity.name,\n dataFileHandleId: fileHandleId,\n concreteType: FILE_ENTITY_CONCRETE_TYPE_VALUE,\n parentId: entity.parentId,\n etag: entity.etag,\n modifiedOn: entity.modifiedOn,\n }\n\n try {\n const updatedEntity = await SynapseClient.updateEntity(\n updateRequest,\n accessToken,\n true,\n )\n await invalidateAllQueriesForEntity(\n queryClient,\n keyFactory,\n updatedEntity.id!,\n )\n queryClient.setQueryData(\n keyFactory.getEntityQueryKey(updatedEntity.id!),\n updatedEntity,\n )\n reset()\n } catch (err) {\n setErrorMessage(err.reason)\n }\n }\n\n const handleUpload = async (response: UploadCallbackResp) => {\n if (response.success && response.resp) {\n const { fileName } = response.resp\n\n // Lookup entity\n const entityLookupRequest: EntityLookupRequest = {\n entityName: fileName,\n parentId: challengeProjectId,\n }\n try {\n const entityId = await SynapseClient.lookupChildEntity(\n entityLookupRequest,\n accessToken,\n )\n // Entity exists, prompt user to update it\n if (entityId) {\n setUploadAttempt({ ...response.resp, entityId: entityId.id })\n setConfirmOpen(true)\n }\n } catch (err) {\n // An existing entity was not found for this file, create it\n setUploadAttempt(undefined)\n createEntity(response.resp)\n }\n } else if (!response.success && response.error) {\n setErrorMessage(response.error.reason as string)\n } else {\n setErrorMessage('An unknown error occurred. Please try again.')\n }\n }\n\n return (\n <Box>\n <Box\n sx={{\n display: 'flex',\n backgroundColor: '#FBFBFC',\n padding: '10px',\n justifyContent: 'space-between',\n }}\n >\n <Typography\n variant=\"h6\"\n sx={{ fontSize: '18px', lineHeight: '20px', fontWeight: 700 }}\n >\n {entityType === EntityType.file\n ? 'Submit File'\n : 'Submit Docker Image'}\n </Typography>\n <Box sx={{ display: 'flex' }}>\n <Typography\n variant=\"body1\"\n sx={{ fontSize: '14px', color: '#71767F' }}\n >\n Your Project SynID:{' '}\n <Link to={{ pathname: PROJECT_URL }} target=\"_blank\">\n {challengeProjectId}\n </Link>\n </Typography>\n <CopyToClipboardIcon value={PROJECT_URL} sx={{ marginTop: '-4px' }} />\n </Box>\n </Box>\n <Box\n sx={{\n my: 3,\n display: 'flex',\n }}\n >\n <NotesTwoTone\n sx={{\n width: '16px',\n height: '16px',\n verticalAlign: 'text-bottom',\n }}\n />\n <Box\n sx={{\n ml: 1,\n }}\n >\n {entityType === EntityType.file && (\n <>\n Choose a prediction file from your submission directory below and\n click \"Submit Selection\" to pick an evaluation queue. If you need\n to add a new file, click \"Upload File\" first.\n </>\n )}\n {entityType === EntityType.dockerrepo && (\n <>\n Choose a Docker image from your submission directory below and\n click \"Submit Selection\" to pick an image version and evaluation\n queue. If you need to upload a new image to your project (\n <code>{challengeProjectId}</code>), see our{' '}\n <Link\n to=\"https://github.com/Sage-Bionetworks-Challenges/sample-model-templates#build-your-model\"\n target=\"_blank\"\n >\n Docker model submission guide\n </Link>{' '}\n for command-line instructions on how to build and upload.\n </>\n )}\n </Box>\n </Box>\n <Box>\n <DataGrid\n loading={areEntitiesLoading}\n columns={columns}\n rows={getRows(entities)}\n rowCount={headerResults?.totalChildCount ?? 0}\n pagination\n paginationMode=\"server\"\n paginationModel={{ page, pageSize: PER_PAGE }}\n pageSizeOptions={[PER_PAGE]}\n onPaginationModelChange={({ page }) => {\n handlePageChange(page)\n }}\n density=\"compact\"\n autoHeight\n sx={{\n fontSize: '14px',\n border: 'none',\n height: '100%',\n '& .MuiDataGrid-columnHeader': {\n backgroundColor: '#F1F3F5',\n },\n '& .Mui-odd': {\n backgroundColor: '#FBFBFC',\n },\n '.MuiDataGrid-columnHeaderTitleContainer': {\n justifyContent: 'space-between',\n },\n '.radio': {\n display: 'flex',\n alignItems: 'center',\n height: '100%',\n },\n }}\n getRowClassName={params =>\n params.indexRelativeToCurrentPage % 2 === 0 ? 'Mui-even' : 'Mui-odd'\n }\n />\n </Box>\n {errorMessage && <ErrorBanner error={errorMessage}></ErrorBanner>}\n <Box\n sx={{\n display: 'flex',\n justifyContent: 'space-between',\n }}\n >\n {entityType === EntityType.file && (\n <FileUpload\n label=\"Upload File\"\n buttonProps={{\n variant: 'outlined',\n endIcon: <IconSvg icon=\"upload\" />,\n sx: { lineHeight: 1 },\n }}\n onComplete={resp => {\n handleUpload(resp)\n }}\n />\n )}\n <Button\n color=\"primary\"\n variant=\"contained\"\n onClick={itemSelectedHandler}\n disabled={!canSubmit}\n >\n Submit Selection\n </Button>\n </Box>\n <ConfirmationDialog\n open={confirmOpen}\n title=\"File exists\"\n content={\n <Typography variant=\"body1\" sx={{ fontSize: '16px' }}>\n A file named &quot;{uploadAttempt?.fileName}&quot; (\n {uploadAttempt?.entityId}) already exists in this location. Do you\n want to update the existing file and create a new version?\n </Typography>\n }\n onCancel={() => setConfirmOpen(false)}\n onConfirm={() => {\n updateEntity()\n setConfirmOpen(false)\n }}\n />\n </Box>\n )\n}\n\nexport default SubmissionDirectoryList\n"],"mappings":"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"}
@@ -2,9 +2,15 @@ import { TypographyProps } from '@mui/material';
2
2
  export type CopyToClipboardStringProps = {
3
3
  value: string;
4
4
  typographyVariant?: TypographyProps['variant'];
5
+ /**
6
+ * When provided, the value is rendered as an external link to this URL in
7
+ * addition to the copy icon. Defaults to rendering the value as plain text.
8
+ */
9
+ href?: string;
10
+ useRoundedIcon?: boolean;
5
11
  };
6
12
  /**
7
- * Displays a string and a "Copy to Clipboard" icon, that, when clicked, will copy the contents of the string to the clipboard.
13
+ * Displays a string or link and a "Copy to Clipboard" icon, that, when clicked, will copy the contents of the string to the clipboard.
8
14
  */
9
15
  export declare function CopyToClipboardString(props: CopyToClipboardStringProps): import("react/jsx-runtime").JSX.Element;
10
16
  export default CopyToClipboardString;
@@ -1 +1 @@
1
- {"version":3,"file":"CopyToClipboardString.d.ts","sourceRoot":"","sources":["../../../src/components/CopyToClipboardString/CopyToClipboardString.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA8B,eAAe,EAAE,MAAM,eAAe,CAAA;AAK3E,MAAM,MAAM,0BAA0B,GAAG;IACvC,KAAK,EAAE,MAAM,CAAA;IACb,iBAAiB,CAAC,EAAE,eAAe,CAAC,SAAS,CAAC,CAAA;CAC/C,CAAA;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,0BAA0B,2CAmCtE;AAED,eAAe,qBAAqB,CAAA"}
1
+ {"version":3,"file":"CopyToClipboardString.d.ts","sourceRoot":"","sources":["../../../src/components/CopyToClipboardString/CopyToClipboardString.tsx"],"names":[],"mappings":"AAAA,OAAO,EAKL,eAAe,EAChB,MAAM,eAAe,CAAA;AAKtB,MAAM,MAAM,0BAA0B,GAAG;IACvC,KAAK,EAAE,MAAM,CAAA;IACb,iBAAiB,CAAC,EAAE,eAAe,CAAC,SAAS,CAAC,CAAA;IAC9C;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB,CAAA;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,0BAA0B,2CAuDtE;AAED,eAAe,qBAAqB,CAAA"}
@@ -1,35 +1,41 @@
1
1
  import { copyStringToClipboard as e } from "../../utils/functions/StringUtils.js";
2
2
  import t from "../IconSvg/IconSvg.js";
3
3
  import { useState as n } from "react";
4
- import { Stack as r, Tooltip as i, Typography as a } from "@mui/material";
5
- import { jsx as o, jsxs as s } from "react/jsx-runtime";
4
+ import { Link as r, Stack as i, Tooltip as a, Typography as o } from "@mui/material";
5
+ import { jsx as s, jsxs as c } from "react/jsx-runtime";
6
6
  //#region src/components/CopyToClipboardString/CopyToClipboardString.tsx
7
- function c(c) {
8
- let { value: l, typographyVariant: u = "smallText1" } = c, [d, f] = n("Copy to clipboard");
9
- return /* @__PURE__ */ s(r, {
7
+ function l(l) {
8
+ let { value: u, typographyVariant: d = "smallText1", href: f, useRoundedIcon: p = !1 } = l, [m, h] = n("Copy to clipboard");
9
+ return /* @__PURE__ */ c(i, {
10
10
  direction: "row",
11
11
  spacing: .5,
12
12
  sx: {
13
13
  alignItems: "center",
14
14
  display: "inline-flex"
15
15
  },
16
- children: [/* @__PURE__ */ o(a, {
16
+ children: [f ? /* @__PURE__ */ s(r, {
17
+ href: f,
18
+ rel: "noopener noreferrer",
19
+ target: "_blank",
20
+ variant: d,
21
+ children: u
22
+ }) : /* @__PURE__ */ s(o, {
17
23
  component: "span",
18
- variant: u,
19
- children: l
20
- }), /* @__PURE__ */ o(i, {
21
- title: d,
24
+ variant: d,
25
+ children: u
26
+ }), /* @__PURE__ */ s(a, {
27
+ title: m,
22
28
  placement: "top",
23
- children: /* @__PURE__ */ o("span", {
29
+ children: /* @__PURE__ */ s("span", {
24
30
  role: "button",
25
31
  style: { cursor: "pointer" },
26
32
  onClick: (t) => {
27
- t.preventDefault(), e(l).then(() => {
28
- f("Copied to clipboard");
33
+ t.preventDefault(), e(u).then(() => {
34
+ h("Copied to clipboard");
29
35
  });
30
36
  },
31
- children: /* @__PURE__ */ o(t, {
32
- icon: "contentCopy",
37
+ children: /* @__PURE__ */ s(t, {
38
+ icon: p ? "contentCopyRounded" : "contentCopy",
33
39
  wrap: !1,
34
40
  sx: { width: "16px" }
35
41
  })
@@ -38,6 +44,6 @@ function c(c) {
38
44
  });
39
45
  }
40
46
  //#endregion
41
- export { c as CopyToClipboardString, c as default };
47
+ export { l as CopyToClipboardString, l as default };
42
48
 
43
49
  //# sourceMappingURL=CopyToClipboardString.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"CopyToClipboardString.js","names":[],"sources":["../../../src/components/CopyToClipboardString/CopyToClipboardString.tsx"],"sourcesContent":["import { Stack, Tooltip, Typography, TypographyProps } from '@mui/material'\nimport { SyntheticEvent, useState } from 'react'\nimport IconSvg from '../IconSvg/IconSvg'\nimport { copyStringToClipboard } from '@/utils/functions/StringUtils'\n\nexport type CopyToClipboardStringProps = {\n value: string\n typographyVariant?: TypographyProps['variant']\n}\n\n/**\n * Displays a string and a \"Copy to Clipboard\" icon, that, when clicked, will copy the contents of the string to the clipboard.\n */\nexport function CopyToClipboardString(props: CopyToClipboardStringProps) {\n const { value, typographyVariant = 'smallText1' } = props\n const [tooltipText, setTooltipText] = useState('Copy to clipboard')\n\n const copyToClipboard = (event: SyntheticEvent) => {\n event.preventDefault()\n\n copyStringToClipboard(value).then(() => {\n setTooltipText('Copied to clipboard')\n })\n }\n\n return (\n <Stack\n direction={'row'}\n spacing={0.5}\n sx={{\n alignItems: 'center',\n display: 'inline-flex',\n }}\n >\n <Typography component={'span'} variant={typographyVariant}>\n {value}\n </Typography>\n <Tooltip title={tooltipText} placement={'top'}>\n <span\n role=\"button\"\n style={{ cursor: 'pointer' }}\n onClick={copyToClipboard}\n >\n <IconSvg icon=\"contentCopy\" wrap={false} sx={{ width: '16px' }} />\n </span>\n </Tooltip>\n </Stack>\n )\n}\n\nexport default CopyToClipboardString\n"],"mappings":";;;;;;AAaA,SAAgB,EAAsB,GAAmC;CACvE,IAAM,EAAE,UAAO,uBAAoB,iBAAiB,GAC9C,CAAC,GAAa,KAAkB,EAAS,mBAAmB;CAUlE,OACE,kBAAC,GAAD;EACE,WAAW;EACX,SAAS;EACT,IAAI;GACF,YAAY;GACZ,SAAS;EACX;YANF,CAQE,kBAAC,GAAD;GAAY,WAAW;GAAQ,SAAS;aACrC;EACS,CAAA,GACZ,kBAAC,GAAD;GAAS,OAAO;GAAa,WAAW;aACtC,kBAAC,QAAD;IACE,MAAK;IACL,OAAO,EAAE,QAAQ,UAAU;IAC3B,UAxBiB,MAA0B;KAGjD,AAFA,EAAM,eAAe,GAErB,EAAsB,CAAK,EAAE,WAAW;MACtC,EAAe,qBAAqB;KACtC,CAAC;IACH;cAoBQ,kBAAC,GAAD;KAAS,MAAK;KAAc,MAAM;KAAO,IAAI,EAAE,OAAO,OAAO;IAAI,CAAA;GAC7D,CAAA;EACC,CAAA,CACJ;;AAEX"}
1
+ {"version":3,"file":"CopyToClipboardString.js","names":[],"sources":["../../../src/components/CopyToClipboardString/CopyToClipboardString.tsx"],"sourcesContent":["import {\n Link,\n Stack,\n Tooltip,\n Typography,\n TypographyProps,\n} from '@mui/material'\nimport { SyntheticEvent, useState } from 'react'\nimport IconSvg from '../IconSvg/IconSvg'\nimport { copyStringToClipboard } from '@/utils/functions/StringUtils'\n\nexport type CopyToClipboardStringProps = {\n value: string\n typographyVariant?: TypographyProps['variant']\n /**\n * When provided, the value is rendered as an external link to this URL in\n * addition to the copy icon. Defaults to rendering the value as plain text.\n */\n href?: string\n useRoundedIcon?: boolean\n}\n\n/**\n * Displays a string or link and a \"Copy to Clipboard\" icon, that, when clicked, will copy the contents of the string to the clipboard.\n */\nexport function CopyToClipboardString(props: CopyToClipboardStringProps) {\n const {\n value,\n typographyVariant = 'smallText1',\n href,\n useRoundedIcon = false,\n } = props\n const [tooltipText, setTooltipText] = useState('Copy to clipboard')\n\n const copyToClipboard = (event: SyntheticEvent) => {\n event.preventDefault()\n\n copyStringToClipboard(value).then(() => {\n setTooltipText('Copied to clipboard')\n })\n }\n\n return (\n <Stack\n direction={'row'}\n spacing={0.5}\n sx={{\n alignItems: 'center',\n display: 'inline-flex',\n }}\n >\n {href ? (\n <Link\n href={href}\n rel={'noopener noreferrer'}\n target={'_blank'}\n variant={typographyVariant}\n >\n {value}\n </Link>\n ) : (\n <Typography component={'span'} variant={typographyVariant}>\n {value}\n </Typography>\n )}\n <Tooltip title={tooltipText} placement={'top'}>\n <span\n role=\"button\"\n style={{ cursor: 'pointer' }}\n onClick={copyToClipboard}\n >\n <IconSvg\n icon={useRoundedIcon ? 'contentCopyRounded' : 'contentCopy'}\n wrap={false}\n sx={{ width: '16px' }}\n />\n </span>\n </Tooltip>\n </Stack>\n )\n}\n\nexport default CopyToClipboardString\n"],"mappings":";;;;;;AAyBA,SAAgB,EAAsB,GAAmC;CACvE,IAAM,EACJ,UACA,uBAAoB,cACpB,SACA,oBAAiB,OACf,GACE,CAAC,GAAa,KAAkB,EAAS,mBAAmB;CAUlE,OACE,kBAAC,GAAD;EACE,WAAW;EACX,SAAS;EACT,IAAI;GACF,YAAY;GACZ,SAAS;EACX;YANF,CAQG,IACC,kBAAC,GAAD;GACQ;GACN,KAAK;GACL,QAAQ;GACR,SAAS;aAER;EACG,CAAA,IAEN,kBAAC,GAAD;GAAY,WAAW;GAAQ,SAAS;aACrC;EACS,CAAA,GAEd,kBAAC,GAAD;GAAS,OAAO;GAAa,WAAW;aACtC,kBAAC,QAAD;IACE,MAAK;IACL,OAAO,EAAE,QAAQ,UAAU;IAC3B,UAnCiB,MAA0B;KAGjD,AAFA,EAAM,eAAe,GAErB,EAAsB,CAAK,EAAE,WAAW;MACtC,EAAe,qBAAqB;KACtC,CAAC;IACH;cA+BQ,kBAAC,GAAD;KACE,MAAM,IAAiB,uBAAuB;KAC9C,MAAM;KACN,IAAI,EAAE,OAAO,OAAO;IACrB,CAAA;GACG,CAAA;EACC,CAAA,CACJ;;AAEX"}
@@ -1 +1 @@
1
- {"version":3,"file":"EntityTree.js","names":[],"sources":["../../../../src/components/EntityFinder/tree/EntityTree.tsx"],"sourcesContent":["import {\n useGetEntityHeader,\n useGetEntityPath,\n useGetFavorites,\n useGetProjectsInfinite,\n} from '@/synapse-queries'\nimport useGetEntityBundle from '@/synapse-queries/entity/useEntityBundle'\nimport { convertToEntityType } from '@/utils/functions/EntityTypeUtils'\nimport { SYNAPSE_ENTITY_ID_REGEX } from '@/utils/functions/RegularExpressions'\nimport { EntityType } from '@sage-bionetworks/synapse-client'\nimport {\n ALL_ENTITY_BUNDLE_FIELDS,\n EntityPath,\n Reference,\n} from '@sage-bionetworks/synapse-types'\nimport {\n Dispatch,\n SetStateAction,\n useCallback,\n useEffect,\n useMemo,\n useState,\n} from 'react'\nimport DropdownSelect from '../../DropdownSelect/DropdownSelect'\nimport { SynapseSpinner } from '../../LoadingScreen/LoadingScreen'\nimport { displayToast } from '../../ToastMessage/ToastMessage'\nimport { BreadcrumbItem } from '../Breadcrumbs'\nimport { toEntityHeader } from '../details/configurations/ProjectListDetails'\nimport {\n EntityDetailsListDataConfiguration,\n EntityDetailsListDataConfigurationType,\n} from '../details/EntityDetailsList'\nimport { EntityFinderHeader } from '../EntityFinderHeader'\nimport { EntitySelectionMapType } from '../useEntitySelection'\nimport {\n EntityTreeNodeType,\n RootNodeConfiguration,\n VirtualizedTree,\n} from './VirtualizedTree'\n\nconst isEntityIdInPath = (entityId: string, path: EntityPath): boolean => {\n for (const eh of path.path) {\n if (entityId === eh.id) {\n return true\n }\n }\n return false\n}\n\nexport enum FinderScope {\n CURRENT_PROJECT = 'Current Project',\n ALL_PROJECTS = 'All Projects',\n CREATED_BY_ME = 'Projects Created By Me',\n FAVORITES = 'My Favorites',\n}\n\nfunction getScopeOptionDefaultContainer(\n scope: FinderScope,\n): EntityTreeContainer {\n switch (scope) {\n case FinderScope.FAVORITES:\n return 'root'\n case FinderScope.CURRENT_PROJECT:\n case FinderScope.ALL_PROJECTS:\n case FinderScope.CREATED_BY_ME:\n return null\n }\n}\n\nfunction getScopeOptionNodeName(scope: FinderScope): string {\n switch (scope) {\n case FinderScope.CURRENT_PROJECT:\n case FinderScope.ALL_PROJECTS:\n case FinderScope.CREATED_BY_ME:\n return 'Projects'\n case FinderScope.FAVORITES:\n return 'Favorites'\n }\n}\n\n/**\n * Indicates which container is selected when the tree is used to control another component. If 'root', then the top-level entities\n * should be shown in the other component. If a synID, then that entity's children should be shown in the other component. If null,\n * then a selection has not been made.\n */\nexport type EntityTreeContainer = string | 'root' | null\n\n// if the first item is selected (matching the dropdown), then output a configuration. otherwise, output a synId\nexport type EntityTreeProps = {\n initialScope?: FinderScope\n /** To show the current project, projectId must be defined */\n projectId?: string\n initialContainer: EntityTreeContainer\n currentContainer: EntityTreeContainer\n setCurrentContainer: Dispatch<SetStateAction<EntityTreeContainer>>\n showDropdown: boolean\n selectedEntities: EntitySelectionMapType\n visibleTypes?: EntityType[]\n toggleSelection?: (entity: Reference) => void\n setDetailsViewConfiguration?: (\n configuration: EntityDetailsListDataConfiguration,\n ) => void\n setBreadcrumbItems?: (items: BreadcrumbItem[]) => void\n /** Determines whether to show the root node corresponding to the selected scope */\n showScopeAsRootNode?: boolean\n treeNodeType: EntityTreeNodeType\n /** The entity types that may be selected. */\n selectableTypes: EntityType[]\n hideScopeSelector?: boolean\n}\n\n/**\n * The TreeView displays a user's entities hierarchically, allowing a user to quickly dive into an entity tree.\n *\n * The tree view currently can only be used to drive a DetailsView using the `setDetailsViewConfiguration` property.\n */\nexport function EntityTree(props: EntityTreeProps) {\n const {\n initialScope = FinderScope.CURRENT_PROJECT,\n projectId,\n initialContainer = null,\n currentContainer,\n setCurrentContainer,\n visibleTypes = [EntityType.project, EntityType.folder],\n toggleSelection,\n selectedEntities,\n setDetailsViewConfiguration,\n setBreadcrumbItems,\n showScopeAsRootNode = true,\n treeNodeType,\n selectableTypes,\n hideScopeSelector = false,\n } = props\n\n const DEFAULT_CONFIGURATION: EntityDetailsListDataConfiguration = {\n type: EntityDetailsListDataConfigurationType.PROMPT,\n }\n\n const [scope, setScope] = useState(initialScope)\n\n useEffect(() => {\n if (setDetailsViewConfiguration) {\n setDetailsViewConfiguration(DEFAULT_CONFIGURATION)\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [])\n\n const setSelectedId = useCallback(\n (entityId: string) => {\n if (toggleSelection) {\n toggleSelection({ targetId: entityId })\n }\n setCurrentContainer(entityId)\n },\n [setCurrentContainer, toggleSelection],\n )\n\n // For these scopes, use the `useGetProjectsInfinite` hook\n const useProjectData =\n scope === FinderScope.ALL_PROJECTS || scope === FinderScope.CREATED_BY_ME\n\n const {\n data: projectData,\n fetchNextPage: fetchNextPageProjects,\n hasNextPage: hasNextPageProjects,\n isLoading: isLoadingProjects,\n } = useGetProjectsInfinite(\n scope === FinderScope.CREATED_BY_ME\n ? { filter: 'CREATED', sort: 'PROJECT_NAME', sortDirection: 'ASC' }\n : { sort: 'PROJECT_NAME', sortDirection: 'ASC' },\n {\n enabled: useProjectData,\n // Don't refetch the projects. Updating the entity headers will drop all the children that VirtualizedTree has fetched\n refetchInterval: Infinity,\n },\n )\n\n const { data: currentContainerBundle, isSuccess: isSuccessBundle } =\n useGetEntityBundle(currentContainer!, undefined, ALL_ENTITY_BUNDLE_FIELDS, {\n enabled: !!currentContainer && currentContainer !== 'root',\n throwOnError: true,\n })\n\n const { data: favorites, isLoading: isLoadingFavorites } = useGetFavorites(\n 'NAME',\n 'ASC',\n {\n select: data =>\n data.filter(eh => visibleTypes.includes(convertToEntityType(eh.type))),\n // Don't refetch the projects. Updating the entity headers will drop all the children that VirtualizedTree has fetched\n refetchInterval: Infinity,\n throwOnError: true,\n },\n )\n\n const { data: initialContainerPath } = useGetEntityPath(initialContainer!, {\n enabled: !!(\n projectId &&\n initialContainer &&\n initialContainer.match(SYNAPSE_ENTITY_ID_REGEX)\n ),\n refetchInterval: Infinity,\n throwOnError: true,\n })\n\n const { data: projectHeader, isLoading: isLoadingProjectHeader } =\n useGetEntityHeader(projectId, undefined, {\n enabled: !!(projectId ?? initialContainerPath?.path[1]?.id),\n refetchInterval: Infinity,\n })\n\n useEffect(() => {\n if (\n FinderScope.CURRENT_PROJECT === scope &&\n projectId &&\n !isLoadingProjectHeader &&\n !projectHeader\n ) {\n // The header wasn't returned, so the user doesn't have access to the current project\n // Let's change the scope to something else\n displayToast(\n `You don't have access to the current project (${projectId}).`,\n 'warning',\n )\n setScope(FinderScope.CREATED_BY_ME)\n }\n }, [isLoadingProjectHeader, projectHeader, projectId, scope])\n\n // Populates the first level of entities in the tree view\n const {\n topLevelEntities,\n isLoading,\n }: {\n topLevelEntities: Pick<EntityFinderHeader, 'name' | 'id' | 'type'>[]\n isLoading: boolean\n } = useMemo(() => {\n let topLevelEntities: Pick<EntityFinderHeader, 'name' | 'id' | 'type'>[] =\n []\n let isLoading: boolean = false\n switch (scope) {\n case FinderScope.ALL_PROJECTS:\n case FinderScope.CREATED_BY_ME:\n if (projectData) {\n topLevelEntities = projectData.pages\n .flatMap(page => page.results)\n .map(toEntityHeader)\n }\n isLoading = isLoadingProjects\n break\n case FinderScope.FAVORITES: {\n topLevelEntities = favorites ?? []\n isLoading = isLoadingFavorites\n break\n }\n case FinderScope.CURRENT_PROJECT:\n if (projectHeader) {\n // use projectHeader as topLevelEntities\n topLevelEntities = [projectHeader]\n isLoading = isLoadingProjectHeader\n }\n break\n default:\n throw new Error('No scope selected')\n }\n return { topLevelEntities, isLoading }\n }, [\n favorites,\n isLoadingFavorites,\n isLoadingProjectHeader,\n isLoadingProjects,\n projectData,\n projectHeader,\n scope,\n ])\n\n // Creates the configuration for the details view and invokes the callback\n useEffect(() => {\n if (setDetailsViewConfiguration || setBreadcrumbItems) {\n let detailsViewConfig: EntityDetailsListDataConfiguration\n let breadcrumbItems: BreadcrumbItem[] = []\n if (currentContainer === null) {\n detailsViewConfig = {\n type: EntityDetailsListDataConfigurationType.PROMPT,\n }\n breadcrumbItems = []\n } else if (currentContainer === 'root') {\n switch (scope) {\n case FinderScope.ALL_PROJECTS:\n detailsViewConfig = {\n type: EntityDetailsListDataConfigurationType.USER_PROJECTS,\n }\n break\n case FinderScope.CREATED_BY_ME:\n detailsViewConfig = {\n type: EntityDetailsListDataConfigurationType.USER_PROJECTS,\n getProjectParams: {\n filter: 'CREATED',\n },\n }\n break\n case FinderScope.CURRENT_PROJECT:\n detailsViewConfig = {\n type: EntityDetailsListDataConfigurationType.HEADER_LIST,\n headerList: topLevelEntities,\n }\n break\n case FinderScope.FAVORITES:\n detailsViewConfig = {\n type: EntityDetailsListDataConfigurationType.USER_FAVORITES,\n }\n break\n }\n breadcrumbItems = [\n {\n name: scope,\n isCurrent: true,\n action: () => {\n setCurrentContainer('root')\n },\n },\n ]\n } else {\n detailsViewConfig = {\n type: EntityDetailsListDataConfigurationType.PARENT_CONTAINER,\n parentContainerId: currentContainer,\n }\n if (currentContainerBundle) {\n breadcrumbItems = [\n {\n name: scope,\n isCurrent: false,\n action: () => {\n setCurrentContainer('root')\n },\n },\n ...currentContainerBundle.path.path\n .slice(1) // Remove the root entity, syn4489\n .map(entity => {\n return {\n name: entity.name ?? entity.id,\n isCurrent: entity.id === currentContainer,\n action: () => {\n setCurrentContainer(entity.id)\n },\n }\n }),\n ]\n }\n }\n if (setDetailsViewConfiguration) {\n setDetailsViewConfiguration(detailsViewConfig)\n }\n if (setBreadcrumbItems) {\n setBreadcrumbItems(breadcrumbItems)\n }\n }\n }, [\n scope,\n currentContainer,\n topLevelEntities,\n setDetailsViewConfiguration,\n setBreadcrumbItems,\n currentContainerBundle,\n isSuccessBundle,\n setCurrentContainer,\n ])\n\n const rootNodeConfiguration: RootNodeConfiguration = useMemo(\n () => ({\n show: showScopeAsRootNode,\n nodeText: getScopeOptionNodeName(scope),\n children: topLevelEntities,\n fetchNextPage: async () => {\n await fetchNextPageProjects()\n },\n hasNextPage: useProjectData && hasNextPageProjects && !isLoadingProjects,\n }),\n [\n showScopeAsRootNode,\n scope,\n topLevelEntities,\n useProjectData,\n hasNextPageProjects,\n isLoadingProjects,\n fetchNextPageProjects,\n ],\n )\n\n const shouldAutoExpand = useCallback(\n (entityId: string) => {\n if (entityId === 'root') {\n return true\n } else {\n return !!(\n scope === FinderScope.CURRENT_PROJECT &&\n initialContainerPath &&\n isEntityIdInPath(entityId, initialContainerPath)\n )\n }\n },\n [scope, initialContainerPath],\n )\n\n const filteredOptions = Object.values(FinderScope).filter(\n scopeOption =>\n !(scopeOption === FinderScope.CURRENT_PROJECT && projectId == null),\n )\n\n const selectedIndex = filteredOptions.indexOf(scope)\n\n return (\n <div\n className={`TreeView ${\n treeNodeType === EntityTreeNodeType.SINGLE_PANE\n ? 'SelectTree'\n : 'BrowseTree'\n }`}\n >\n {!hideScopeSelector && (\n <div className=\"Header\" onClick={e => e.stopPropagation()}>\n <DropdownSelect\n variant={'outlined'}\n options={filteredOptions}\n selectedIndex={selectedIndex}\n setSelectedIndex={index => {\n const selectedScope = filteredOptions[index]\n if (scope !== selectedScope) {\n setScope(selectedScope)\n setCurrentContainer(\n getScopeOptionDefaultContainer(selectedScope),\n )\n }\n }}\n size=\"small\"\n fullWidth\n />\n </div>\n )}\n {isLoading ? (\n <div className=\"Placeholder\">\n <SynapseSpinner size={30} />\n </div>\n ) : (\n <div className=\"Tree\" role=\"tree\">\n <VirtualizedTree\n selected={selectedEntities}\n visibleTypes={visibleTypes}\n autoExpand={shouldAutoExpand}\n rootNodeConfiguration={rootNodeConfiguration}\n treeNodeType={treeNodeType}\n selectableTypes={selectableTypes}\n currentContainer={currentContainer}\n setSelectedId={setSelectedId}\n />\n </div>\n )}\n </div>\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"EntityTree.js","names":[],"sources":["../../../../src/components/EntityFinder/tree/EntityTree.tsx"],"sourcesContent":["import {\n useGetEntityHeader,\n useGetEntityPath,\n useGetFavorites,\n useGetProjectsInfinite,\n} from '@/synapse-queries'\nimport useGetEntityBundle from '@/synapse-queries/entity/useEntityBundle'\nimport { convertToEntityType } from '@/utils/functions/EntityTypeUtils'\nimport { SYNAPSE_ENTITY_ID_REGEX } from '@/utils/functions/RegularExpressions'\nimport { EntityType } from '@sage-bionetworks/synapse-client'\nimport {\n ALL_ENTITY_BUNDLE_FIELDS,\n EntityPath,\n Reference,\n} from '@sage-bionetworks/synapse-types'\nimport {\n Dispatch,\n SetStateAction,\n useCallback,\n useEffect,\n useMemo,\n useState,\n} from 'react'\nimport DropdownSelect from '../../DropdownSelect/DropdownSelect'\nimport { SynapseSpinner } from '../../LoadingScreen/LoadingScreen'\nimport { displayToast } from '../../ToastMessage/ToastMessage'\nimport { BreadcrumbItem } from '../Breadcrumbs'\nimport { toEntityHeader } from '../details/configurations/ProjectListDetails'\nimport {\n EntityDetailsListDataConfiguration,\n EntityDetailsListDataConfigurationType,\n} from '../details/EntityDetailsList'\nimport { EntityFinderHeader } from '../EntityFinderHeader'\nimport { EntitySelectionMapType } from '../useEntitySelection'\nimport {\n EntityTreeNodeType,\n RootNodeConfiguration,\n VirtualizedTree,\n} from './VirtualizedTree'\n\nconst isEntityIdInPath = (entityId: string, path: EntityPath): boolean => {\n for (const eh of path.path) {\n if (entityId === eh.id) {\n return true\n }\n }\n return false\n}\n\nexport enum FinderScope {\n CURRENT_PROJECT = 'Current Project',\n ALL_PROJECTS = 'All Projects',\n CREATED_BY_ME = 'Projects Created By Me',\n FAVORITES = 'My Favorites',\n}\n\nfunction getScopeOptionDefaultContainer(\n scope: FinderScope,\n): EntityTreeContainer {\n switch (scope) {\n case FinderScope.FAVORITES:\n return 'root'\n case FinderScope.CURRENT_PROJECT:\n case FinderScope.ALL_PROJECTS:\n case FinderScope.CREATED_BY_ME:\n return null\n }\n}\n\nfunction getScopeOptionNodeName(scope: FinderScope): string {\n switch (scope) {\n case FinderScope.CURRENT_PROJECT:\n case FinderScope.ALL_PROJECTS:\n case FinderScope.CREATED_BY_ME:\n return 'Projects'\n case FinderScope.FAVORITES:\n return 'Favorites'\n }\n}\n\n/**\n * Indicates which container is selected when the tree is used to control another component. If 'root', then the top-level entities\n * should be shown in the other component. If a synID, then that entity's children should be shown in the other component. If null,\n * then a selection has not been made.\n */\nexport type EntityTreeContainer = string | 'root' | null\n\n// if the first item is selected (matching the dropdown), then output a configuration. otherwise, output a synId\nexport type EntityTreeProps = {\n initialScope?: FinderScope\n /** To show the current project, projectId must be defined */\n projectId?: string\n initialContainer: EntityTreeContainer\n currentContainer: EntityTreeContainer\n setCurrentContainer: Dispatch<SetStateAction<EntityTreeContainer>>\n showDropdown: boolean\n selectedEntities: EntitySelectionMapType\n visibleTypes?: EntityType[]\n toggleSelection?: (entity: Reference) => void\n setDetailsViewConfiguration?: (\n configuration: EntityDetailsListDataConfiguration,\n ) => void\n setBreadcrumbItems?: (items: BreadcrumbItem[]) => void\n /** Determines whether to show the root node corresponding to the selected scope */\n showScopeAsRootNode?: boolean\n treeNodeType: EntityTreeNodeType\n /** The entity types that may be selected. */\n selectableTypes: EntityType[]\n hideScopeSelector?: boolean\n}\n\n/**\n * The TreeView displays a user's entities hierarchically, allowing a user to quickly dive into an entity tree.\n *\n * The tree view currently can only be used to drive a DetailsView using the `setDetailsViewConfiguration` property.\n */\nexport function EntityTree(props: EntityTreeProps) {\n const {\n initialScope = FinderScope.CURRENT_PROJECT,\n projectId,\n initialContainer = null,\n currentContainer,\n setCurrentContainer,\n visibleTypes = [EntityType.project, EntityType.folder],\n toggleSelection,\n selectedEntities,\n setDetailsViewConfiguration,\n setBreadcrumbItems,\n showScopeAsRootNode = true,\n treeNodeType,\n selectableTypes,\n hideScopeSelector = false,\n } = props\n\n const DEFAULT_CONFIGURATION: EntityDetailsListDataConfiguration = {\n type: EntityDetailsListDataConfigurationType.PROMPT,\n }\n\n const [scope, setScope] = useState(initialScope)\n\n useEffect(() => {\n if (setDetailsViewConfiguration) {\n setDetailsViewConfiguration(DEFAULT_CONFIGURATION)\n }\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [])\n\n const setSelectedId = useCallback(\n (entityId: string) => {\n if (toggleSelection) {\n toggleSelection({ targetId: entityId })\n }\n setCurrentContainer(entityId)\n },\n [setCurrentContainer, toggleSelection],\n )\n\n // For these scopes, use the `useGetProjectsInfinite` hook\n const useProjectData =\n scope === FinderScope.ALL_PROJECTS || scope === FinderScope.CREATED_BY_ME\n\n const {\n data: projectData,\n fetchNextPage: fetchNextPageProjects,\n hasNextPage: hasNextPageProjects,\n isLoading: isLoadingProjects,\n } = useGetProjectsInfinite(\n scope === FinderScope.CREATED_BY_ME\n ? { filter: 'CREATED', sort: 'PROJECT_NAME', sortDirection: 'ASC' }\n : { sort: 'PROJECT_NAME', sortDirection: 'ASC' },\n {\n enabled: useProjectData,\n // Don't refetch the projects. Updating the entity headers will drop all the children that VirtualizedTree has fetched\n refetchInterval: Infinity,\n },\n )\n\n const { data: currentContainerBundle, isSuccess: isSuccessBundle } =\n useGetEntityBundle(currentContainer!, undefined, ALL_ENTITY_BUNDLE_FIELDS, {\n enabled: !!currentContainer && currentContainer !== 'root',\n throwOnError: true,\n })\n\n const { data: favorites, isLoading: isLoadingFavorites } = useGetFavorites(\n 'NAME',\n 'ASC',\n {\n select: data =>\n data.filter(eh => visibleTypes.includes(convertToEntityType(eh.type))),\n // Don't refetch the projects. Updating the entity headers will drop all the children that VirtualizedTree has fetched\n refetchInterval: Infinity,\n throwOnError: true,\n },\n )\n\n const { data: initialContainerPath } = useGetEntityPath(initialContainer!, {\n enabled: !!(\n projectId &&\n initialContainer &&\n initialContainer.match(SYNAPSE_ENTITY_ID_REGEX)\n ),\n refetchInterval: Infinity,\n throwOnError: true,\n })\n\n const { data: projectHeader, isLoading: isLoadingProjectHeader } =\n useGetEntityHeader(projectId, undefined, {\n enabled: !!(projectId ?? initialContainerPath?.path[1]?.id),\n refetchInterval: Infinity,\n })\n\n useEffect(() => {\n if (\n FinderScope.CURRENT_PROJECT === scope &&\n projectId &&\n !isLoadingProjectHeader &&\n !projectHeader\n ) {\n // The header wasn't returned, so the user doesn't have access to the current project\n // Let's change the scope to something else\n displayToast(\n `You don't have access to the current project (${projectId}).`,\n 'warning',\n )\n setScope(FinderScope.CREATED_BY_ME)\n }\n }, [isLoadingProjectHeader, projectHeader, projectId, scope])\n\n // Populates the first level of entities in the tree view\n const {\n topLevelEntities,\n isLoading,\n }: {\n topLevelEntities: Pick<EntityFinderHeader, 'name' | 'id' | 'type'>[]\n isLoading: boolean\n } = useMemo(() => {\n let topLevelEntities: Pick<EntityFinderHeader, 'name' | 'id' | 'type'>[] =\n []\n let isLoading: boolean = false\n switch (scope) {\n case FinderScope.ALL_PROJECTS:\n case FinderScope.CREATED_BY_ME:\n if (projectData) {\n topLevelEntities = projectData.pages\n .flatMap(page => page.results)\n .map(toEntityHeader)\n }\n isLoading = isLoadingProjects\n break\n case FinderScope.FAVORITES: {\n topLevelEntities = favorites ?? []\n isLoading = isLoadingFavorites\n break\n }\n case FinderScope.CURRENT_PROJECT:\n if (projectHeader) {\n // use projectHeader as topLevelEntities\n topLevelEntities = [projectHeader]\n isLoading = isLoadingProjectHeader\n }\n break\n default:\n throw new Error('No scope selected')\n }\n return { topLevelEntities, isLoading }\n }, [\n favorites,\n isLoadingFavorites,\n isLoadingProjectHeader,\n isLoadingProjects,\n projectData,\n projectHeader,\n scope,\n ])\n\n // Creates the configuration for the details view and invokes the callback\n useEffect(() => {\n if (setDetailsViewConfiguration || setBreadcrumbItems) {\n let detailsViewConfig: EntityDetailsListDataConfiguration\n let breadcrumbItems: BreadcrumbItem[] = []\n if (currentContainer === null) {\n detailsViewConfig = {\n type: EntityDetailsListDataConfigurationType.PROMPT,\n }\n breadcrumbItems = []\n } else if (currentContainer === 'root') {\n switch (scope) {\n case FinderScope.ALL_PROJECTS:\n detailsViewConfig = {\n type: EntityDetailsListDataConfigurationType.USER_PROJECTS,\n }\n break\n case FinderScope.CREATED_BY_ME:\n detailsViewConfig = {\n type: EntityDetailsListDataConfigurationType.USER_PROJECTS,\n getProjectParams: {\n filter: 'CREATED',\n },\n }\n break\n case FinderScope.CURRENT_PROJECT:\n detailsViewConfig = {\n type: EntityDetailsListDataConfigurationType.HEADER_LIST,\n headerList: topLevelEntities,\n }\n break\n case FinderScope.FAVORITES:\n detailsViewConfig = {\n type: EntityDetailsListDataConfigurationType.USER_FAVORITES,\n }\n break\n }\n breadcrumbItems = [\n {\n name: scope,\n isCurrent: true,\n action: () => {\n setCurrentContainer('root')\n },\n },\n ]\n } else {\n detailsViewConfig = {\n type: EntityDetailsListDataConfigurationType.PARENT_CONTAINER,\n parentContainerId: currentContainer,\n }\n if (currentContainerBundle) {\n breadcrumbItems = [\n {\n name: scope,\n isCurrent: false,\n action: () => {\n setCurrentContainer('root')\n },\n },\n ...currentContainerBundle.path.path\n .slice(1) // Remove the root entity, syn4489\n .map(entity => {\n return {\n name: entity.name ?? entity.id,\n isCurrent: entity.id === currentContainer,\n action: () => {\n setCurrentContainer(entity.id)\n },\n }\n }),\n ]\n }\n }\n if (setDetailsViewConfiguration) {\n setDetailsViewConfiguration(detailsViewConfig)\n }\n if (setBreadcrumbItems) {\n setBreadcrumbItems(breadcrumbItems)\n }\n }\n }, [\n scope,\n currentContainer,\n topLevelEntities,\n setDetailsViewConfiguration,\n setBreadcrumbItems,\n currentContainerBundle,\n isSuccessBundle,\n setCurrentContainer,\n ])\n\n const rootNodeConfiguration: RootNodeConfiguration = useMemo(\n () => ({\n show: showScopeAsRootNode,\n nodeText: getScopeOptionNodeName(scope),\n children: topLevelEntities,\n fetchNextPage: async () => {\n await fetchNextPageProjects()\n },\n hasNextPage: useProjectData && hasNextPageProjects && !isLoadingProjects,\n }),\n [\n showScopeAsRootNode,\n scope,\n topLevelEntities,\n useProjectData,\n hasNextPageProjects,\n isLoadingProjects,\n fetchNextPageProjects,\n ],\n )\n\n const shouldAutoExpand = useCallback(\n (entityId: string) => {\n if (entityId === 'root') {\n return true\n } else {\n return !!(\n scope === FinderScope.CURRENT_PROJECT &&\n initialContainerPath &&\n isEntityIdInPath(entityId, initialContainerPath)\n )\n }\n },\n [scope, initialContainerPath],\n )\n\n const filteredOptions = Object.values(FinderScope).filter(\n scopeOption =>\n !(scopeOption === FinderScope.CURRENT_PROJECT && projectId == null),\n )\n\n const selectedIndex = filteredOptions.indexOf(scope)\n\n return (\n <div\n className={`TreeView ${\n treeNodeType === EntityTreeNodeType.SINGLE_PANE\n ? 'SelectTree'\n : 'BrowseTree'\n }`}\n >\n {!hideScopeSelector && (\n <div className=\"Header\" onClick={e => e.stopPropagation()}>\n <DropdownSelect\n variant={'outlined'}\n options={filteredOptions}\n selectedIndex={selectedIndex}\n setSelectedIndex={index => {\n const selectedScope = filteredOptions[index]\n if (scope !== selectedScope) {\n setScope(selectedScope)\n setCurrentContainer(\n getScopeOptionDefaultContainer(selectedScope),\n )\n }\n }}\n size=\"small\"\n fullWidth\n />\n </div>\n )}\n {isLoading ? (\n <div className=\"Placeholder\">\n <SynapseSpinner size={30} />\n </div>\n ) : (\n <div className=\"Tree\" role=\"tree\">\n <VirtualizedTree\n selected={selectedEntities}\n visibleTypes={visibleTypes}\n autoExpand={shouldAutoExpand}\n rootNodeConfiguration={rootNodeConfiguration}\n treeNodeType={treeNodeType}\n selectableTypes={selectableTypes}\n currentContainer={currentContainer}\n setSelectedId={setSelectedId}\n />\n </div>\n )}\n </div>\n )\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"VirtualizedTree.js","names":[],"sources":["../../../../src/components/EntityFinder/tree/VirtualizedTree.tsx"],"sourcesContent":["import React from 'react'\nimport { getEntityChildren } from '@/synapse-client'\nimport { useSynapseContext } from '@/utils'\nimport { formatDate } from '@/utils/functions/DateFormatter'\nimport {\n getEntityTypeFromHeader,\n isContainerType,\n} from '@/utils/functions/EntityTypeUtils'\nimport { Writable } from '@/utils/types/Writable'\nimport { ChevronRight, ExpandMore } from '@mui/icons-material'\nimport { Skeleton, Tooltip, Typography } from '@mui/material'\nimport { EntityType } from '@sage-bionetworks/synapse-client'\nimport { EntityChildrenRequest } from '@sage-bionetworks/synapse-types'\nimport { useQueryClient } from '@tanstack/react-query'\nimport dayjs from 'dayjs'\nimport { cloneDeep } from 'lodash-es'\nimport { ReactNode, useCallback, useEffect, useRef, useState } from 'react'\nimport { useInView } from 'react-intersection-observer'\nimport AutoSizer from 'react-virtualized-auto-sizer'\nimport {\n TreeWalker,\n TreeWalkerValue,\n VariableSizeNodeData,\n VariableSizeNodePublicState,\n VariableSizeTree,\n} from 'react-vtree'\nimport { NodeComponentProps } from 'react-vtree/dist/es/Tree'\nimport { EntityBadgeIcons } from '../../EntityBadgeIcons'\nimport { EntityTypeIcon } from '../../EntityIcon'\nimport { SynapseSpinner } from '../../LoadingScreen/LoadingScreen'\nimport { UserBadge } from '../../UserCard/UserBadge'\nimport { EntityFinderHeader } from '../EntityFinderHeader'\nimport { EntitySelectionMapType } from '../useEntitySelection'\n\nexport enum EntityTreeNodeType {\n /** The tree component's appearance and interactions will facilitate selection. Nodes will be larger and styles will indicate primary selection */\n SINGLE_PANE,\n /** The tree component's appearance and interactions will facilitate browsing. Nodes will be smaller and styles will indicate secondary selection */\n DUAL_PANE,\n}\n\ntype NodeChildren = Readonly<{\n /** The node's children. If undefined, then children have not been fetched. */\n children?: EntityHeaderNode[]\n /** The token used to fetch the next page of children for this entity. If this property is nullish and children is defined, then all children have been fetched. */\n childrenNextPageToken?: string | null\n}>\n\nexport type EntityHeaderNode = (\n | EntityFinderHeader\n // Only will have a subset of fields if the node is fetched via the entity path:\n | Pick<EntityFinderHeader, 'id' | 'name' | 'type'>\n) &\n NodeChildren\ntype PaginationNode = { __paginationNode: true }\n\ntype TreeNode = EntityHeaderNode | RootNodeConfiguration | PaginationNode\n\nfunction getTreeNodeType(\n node: TreeNode,\n): 'entityHeader' | 'rootNodeConfiguration' | 'pagination' {\n if ('__paginationNode' in node) {\n return 'pagination'\n }\n return 'id' in node ? 'entityHeader' : 'rootNodeConfiguration'\n}\n\nfunction isPaginationNode(node: TreeNode): node is PaginationNode {\n return getTreeNodeType(node) === 'pagination'\n}\n\nfunction isEntityHeaderNode(node: TreeNode): node is EntityHeaderNode {\n return getTreeNodeType(node) === 'entityHeader'\n}\n\nfunction isRootNodeConfiguration(\n node: TreeNode,\n): node is RootNodeConfiguration {\n return getTreeNodeType(node) === 'rootNodeConfiguration'\n}\n\nfunction hasMoreChildren(node: TreeNode) {\n if (isPaginationNode(node)) {\n return false\n } else if (isRootNodeConfiguration(node)) {\n return node.hasNextPage\n }\n\n return node.children == null || node.childrenNextPageToken != null\n}\n\nfunction isLeafNode(node: TreeNode) {\n if (isPaginationNode(node)) {\n return true\n } else if (isRootNodeConfiguration(node)) {\n return false\n } else {\n return (\n // Entity is not a container\n !isContainerType(getEntityTypeFromHeader(node)) ||\n // OR Children have been fetched (nonnull) and there are 0 children\n (node.children != null && node.children.length === 0)\n )\n }\n}\n\nfunction maybeHasChildren(\n node: TreeNode,\n): node is RootNodeConfiguration | EntityHeaderNode {\n if (isPaginationNode(node)) {\n return false\n } else if (isRootNodeConfiguration(node)) {\n return node.children.length > 0 || node.hasNextPage\n } else {\n return !isLeafNode(node)\n }\n}\n\nexport type TreeData = VariableSizeNodeData &\n Readonly<{\n node: TreeNode\n getNextPageOfChildren: () => Promise<void>\n isLeaf: boolean\n nestingLevel: number\n setSelectedId: (entityId: string) => void\n treeNodeType: EntityTreeNodeType\n isSelected: boolean\n isDisabled: boolean\n }>\n\ntype NodeMeta = Readonly<{\n nestingLevel: number\n node: TreeNode\n}>\n\nexport type RootNodeConfiguration = {\n show: boolean\n nodeText: string\n children: EntityHeaderNode[]\n /** If undefined, no more entities to fetch */\n fetchNextPage: () => Promise<void>\n hasNextPage: boolean\n}\n\n/**\n * Converts a TreeNode and related data into a TreeWalkerValue that react-vtree can use to render the tree.\n * Exported for testing purposes only.\n * @param config\n * @returns\n */\nexport const getNodeData = (config: {\n node: TreeNode\n nestingLevel: number\n getNextPageOfChildren: () => Promise<void>\n setSelectedId: (entityId: string) => void\n treeNodeType: EntityTreeNodeType\n selected: EntitySelectionMapType\n selectableTypes: EntityType[]\n autoExpand: (entityId: string) => boolean\n defaultHeight: number\n currentContainer?: string | 'root' | null\n}): TreeWalkerValue<TreeData, NodeMeta> => {\n const {\n node,\n nestingLevel,\n getNextPageOfChildren,\n setSelectedId,\n treeNodeType,\n selected,\n selectableTypes,\n autoExpand,\n defaultHeight,\n currentContainer,\n } = config\n\n if (isPaginationNode(node)) {\n throw new Error('Cannot create data for a pagination node')\n }\n\n const id = isRootNodeConfiguration(node) ? 'root' : node.id\n\n const isSelected =\n treeNodeType === EntityTreeNodeType.SINGLE_PANE\n ? selected.has(id)\n : currentContainer === id\n const isDisabled =\n !isRootNodeConfiguration(node) &&\n !selectableTypes.includes(getEntityTypeFromHeader(node))\n\n const isOpenByDefault = autoExpand(id)\n /*\n * If the node is open by default and we haven't fetched its children,\n * fetch the first page (otherwise we won't fetch unless re-toggled)\n */\n if (isOpenByDefault && node.children == null && hasMoreChildren(node)) {\n getNextPageOfChildren()\n }\n\n return {\n data: {\n node,\n getNextPageOfChildren,\n id: id,\n isLeaf: isLeafNode(node),\n isOpenByDefault: isOpenByDefault,\n nestingLevel,\n setSelectedId,\n treeNodeType,\n isSelected,\n isDisabled,\n defaultHeight,\n },\n nestingLevel,\n node,\n }\n}\n\n/**\n * Node component for the react-vtree virtualized tree. Exported only for testing purposes\n */\nexport function Node(\n props: NodeComponentProps<TreeData, VariableSizeNodePublicState<TreeData>>,\n) {\n const {\n data: {\n node,\n getNextPageOfChildren,\n isLeaf,\n id,\n nestingLevel,\n setSelectedId,\n treeNodeType,\n isSelected,\n isDisabled,\n },\n isOpen,\n style,\n setOpen,\n } = props\n\n const [isLoading, setLoading] = useState(false)\n\n const nodeText = isEntityHeaderNode(node) ? (\n <span>{node.name}</span>\n ) : isRootNodeConfiguration(node) ? (\n <span>{node.nodeText}</span>\n ) : (\n // Pagination node\n <Skeleton width={100} />\n )\n\n // We only use this for pagination nodes. If the pagination node comes into view, then immediately call `getNextPageOfChildren`\n const { ref, inView } = useInView()\n\n useEffect(() => {\n if (isPaginationNode(node) && inView) {\n getNextPageOfChildren()\n }\n }, [node, inView, getNextPageOfChildren])\n\n const toggleExpand = useCallback(async () => {\n if (hasMoreChildren(node)) {\n setLoading(true)\n await getNextPageOfChildren()\n await setOpen(!isOpen)\n setLoading(false)\n } else {\n await setOpen(!isOpen)\n }\n }, [getNextPageOfChildren, node, isOpen, setOpen])\n\n /**\n * If in the dual-pane view, expand a node when it becomes selected\n */\n useEffect(() => {\n if (\n treeNodeType === EntityTreeNodeType.DUAL_PANE &&\n isSelected &&\n !isOpen\n ) {\n toggleExpand()\n }\n // Intentionally only toggle the expanded state when isSelected changes, otherwise the node cannot be un-expanded\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isSelected])\n\n /**\n * If the height is 0, the node is purposefully hidden. Just render a fragment.\n */\n if ('height' in style && style.height === 0) {\n return <></>\n }\n\n let tooltipContent: ReactNode = ''\n if (isEntityHeaderNode(node)) {\n tooltipContent = (\n <div style={{ textAlign: 'center' }}>\n <Typography\n component=\"span\"\n variant=\"smallText1\"\n sx={{\n display: 'inline',\n }}\n >\n {node.name}\n <br />\n <b>ID: </b>\n {node.id}\n {'modifiedBy' in node && (\n <>\n <br />\n <b>Modified By: </b>\n <UserBadge\n userId={node.modifiedBy.toString()}\n showFullName\n disableLink\n showCardOnHover={false}\n />\n </>\n )}\n {'modifiedOn' in node && (\n <>\n <br />\n <b>Modified On: </b>\n {formatDate(dayjs(node.modifiedOn))}\n </>\n )}\n </Typography>\n </div>\n )\n }\n\n const ExpandIcon = isOpen ? ExpandMore : ChevronRight\n\n return (\n <div\n className={`Node ${\n treeNodeType === EntityTreeNodeType.SINGLE_PANE\n ? 'SelectNode'\n : 'BrowseNode'\n }`}\n aria-selected={isSelected}\n aria-disabled={isDisabled}\n onClick={event => {\n event.stopPropagation()\n if (!isDisabled) {\n setSelectedId(id)\n }\n }}\n style={{\n ...style,\n paddingLeft: `${nestingLevel * 20}px`,\n }}\n >\n {!isLeaf && (\n <>\n {isLoading ? (\n <SynapseSpinner size={10} />\n ) : (\n <button\n aria-label={isOpen ? 'Collapse' : 'Expand'}\n onClick={event => {\n event.stopPropagation()\n toggleExpand()\n }}\n >\n <ExpandIcon className=\"ExpandButton\" />\n </button>\n )}\n </>\n )}\n {treeNodeType === EntityTreeNodeType.SINGLE_PANE && ( // SWC-5592\n <div className=\"EntityIcon\">\n {isEntityHeaderNode(node) && (\n <EntityTypeIcon type={getEntityTypeFromHeader(node)} />\n )}\n </div>\n )}\n\n <Tooltip title={tooltipContent} placement=\"right\">\n <div className=\"EntityName\" ref={ref}>\n {nodeText}\n </div>\n </Tooltip>\n\n {treeNodeType === EntityTreeNodeType.SINGLE_PANE && (\n <EntityBadgeIcons\n entityId={id}\n showHasDiscussionThread={false}\n showHasWiki={false}\n showUnlink={false}\n canOpenModal={false}\n />\n )}\n </div>\n )\n}\n\n/**\n * Extracted from the React component for testing purposes\n * @returns a generator function compatible with react-vtree\n */\nexport function getTreeWalkerFunction(\n rootNode: RootNodeConfiguration,\n setSelectedId: VirtualizedTreeProps['setSelectedId'],\n treeNodeType: VirtualizedTreeProps['treeNodeType'],\n selected: VirtualizedTreeProps['selected'],\n selectableTypes: VirtualizedTreeProps['selectableTypes'],\n autoExpand: VirtualizedTreeProps['autoExpand'],\n itemSize: (index?: number) => number,\n currentContainer: VirtualizedTreeProps['currentContainer'],\n fetchNextPageOfChildren: (node: Writable<EntityHeaderNode>) => Promise<void>,\n): TreeWalker<TreeData, NodeMeta> {\n return function* treeWalker() {\n // Step [1]: Define the root node of our tree.\n yield getNodeData({\n node: rootNode,\n nestingLevel: 0,\n getNextPageOfChildren: rootNode.fetchNextPage,\n setSelectedId,\n treeNodeType,\n selected,\n selectableTypes,\n autoExpand,\n defaultHeight: itemSize(),\n currentContainer,\n })\n\n while (true) {\n // Step [2]: Get the parent node and nesting level, which will be passed into the Generator.next function by react-vtree.\n // Yielding `undefined` indicates to react-vtree that we've finished iterating children from the last time we were in the loop.\n const parentMeta = yield\n\n if (maybeHasChildren(parentMeta.node)) {\n // Step [3]: Yielding all the known children of the parent\n if (parentMeta.node.children) {\n for (let i = 0; i < parentMeta.node.children.length; i++) {\n const childNode = parentMeta.node.children[i]\n\n yield getNodeData({\n node: childNode,\n nestingLevel: parentMeta.nestingLevel + 1,\n getNextPageOfChildren: () => fetchNextPageOfChildren(childNode),\n setSelectedId,\n treeNodeType,\n selected,\n selectableTypes,\n autoExpand,\n defaultHeight: itemSize(),\n currentContainer,\n })\n }\n }\n\n // Step [4] - If the parent node has more children, render a \"pagination node\" that will fetch more children when it comes into view (via intersection observer)\n if (hasMoreChildren(parentMeta.node)) {\n const paginationNode: PaginationNode = {\n __paginationNode: true,\n }\n const paginationTreeWalkerValue: TreeWalkerValue<TreeData, NodeMeta> =\n {\n data: {\n id: parentMeta.data.id + '-pagination',\n node: paginationNode,\n isOpenByDefault: false,\n getNextPageOfChildren: parentMeta.data.getNextPageOfChildren,\n isLeaf: true,\n isSelected: false,\n defaultHeight: itemSize(),\n isDisabled: true,\n nestingLevel: parentMeta.nestingLevel + 1,\n setSelectedId: () => {},\n treeNodeType,\n },\n nestingLevel: parentMeta.nestingLevel + 1,\n node: {\n __paginationNode: true,\n },\n }\n yield paginationTreeWalkerValue\n }\n }\n }\n }\n}\n\nexport type VirtualizedTreeProps = Readonly<{\n treeNodeType: EntityTreeNodeType\n rootNodeConfiguration: RootNodeConfiguration\n setSelectedId: (entityId: string) => void\n selected: EntitySelectionMapType\n /* currentContainer is the container whose contents are shown on in the right pane in dual-pane configuration, and may only be defined when treeNodeType is DUAL_PANE */\n currentContainer?: string | 'root' | null\n autoExpand: (entityId: string) => boolean\n selectableTypes: EntityType[]\n visibleTypes: EntityType[]\n}>\n\n/**\n * Component that utilizes react-vtree to efficiently display a tree of entities. react-vtree utilizes react-window\n * to only render visible nodes, which eliminates performance issues as seen in SWC-5978.\n */\nexport const VirtualizedTree = (\n props: VirtualizedTreeProps,\n): React.ReactNode => {\n const {\n rootNodeConfiguration,\n setSelectedId,\n treeNodeType,\n selected,\n currentContainer,\n selectableTypes,\n visibleTypes,\n autoExpand,\n } = props\n\n const { accessToken, keyFactory } = useSynapseContext()\n const queryClient = useQueryClient()\n\n const [rootNode, setRootNode] = useState<RootNodeConfiguration>(\n rootNodeConfiguration,\n )\n\n const treeInstance = useRef<VariableSizeTree<TreeData>>(null)\n\n useEffect(() => {\n setRootNode(rootNodeConfiguration)\n }, [rootNodeConfiguration, rootNodeConfiguration.children])\n\n // This function is used by VariableSizeTree to identify the size of each node in the tree.\n const itemSize = useCallback(\n (index?: number) => {\n /**\n * We must have a root node, but we don't always want to show it.\n * In those cases, set the height of the root node to 0.\n */\n if (index === 0 && !rootNodeConfiguration.show) {\n return 0\n }\n /**\n * The height of all other nodes in the tree varies depending on the tree node type.\n */\n return treeNodeType === EntityTreeNodeType.DUAL_PANE ? 30 : 60\n },\n [treeNodeType, rootNodeConfiguration.show],\n )\n\n /**\n * Fetches the next page of children for a given node. After fetching the children, this method will make a deep clone of the\n * full tree, including the new data, and updates the state of the component\n */\n const fetchNextPageOfChildren = useCallback(\n // Because we update the root node with a copy at the end of this function, we can write to the node under update.\n async (node: Writable<EntityHeaderNode>) => {\n // Fetch the children of the node\n const entityChildrenRequest: EntityChildrenRequest = {\n parentId: node.id,\n nextPageToken: node.childrenNextPageToken,\n includeTypes: visibleTypes,\n }\n const children = await queryClient.fetchQuery({\n queryKey: keyFactory.getEntityChildrenQueryKey(\n entityChildrenRequest,\n false,\n ),\n queryFn: () => getEntityChildren(entityChildrenRequest, accessToken),\n })\n // Update the node data -- add the children and store the nextPageToken\n if (node.children) {\n for (const newChild of children.page) {\n // SWC-6751 - Prevent adding duplicate entries in case this function is called twice\n if (!node.children.find(child => child.id == newChild.id)) {\n node.children.push(newChild)\n }\n }\n } else {\n node.children = children.page\n }\n node.childrenNextPageToken = children.nextPageToken\n\n // Ensure the node for which we fetched children is expanded/open\n if (treeInstance.current) {\n await treeInstance.current.recomputeTree({\n [node.id]: {\n open: true,\n },\n })\n }\n\n // cloneDeep is required to re-render the tree with the new children\n setRootNode(cloneDeep(rootNode))\n },\n [visibleTypes, queryClient, keyFactory, rootNode, accessToken],\n )\n\n /**\n * treeWalker is a generator function used by react-vtree to generate the tree structure. The tree is re-built when the function is updated,\n * so the dependencies specified in useCallback are important.\n */\n const memoizedTreeWalker = useCallback(\n getTreeWalkerFunction(\n rootNode,\n setSelectedId,\n treeNodeType,\n selected,\n selectableTypes,\n autoExpand,\n itemSize,\n currentContainer,\n fetchNextPageOfChildren,\n ),\n [\n rootNode,\n setSelectedId,\n treeNodeType,\n selected,\n selectableTypes,\n autoExpand,\n currentContainer,\n fetchNextPageOfChildren,\n itemSize,\n ],\n )\n\n return (\n <AutoSizer disableWidth>\n {({ height }: { height: number }) => (\n <VariableSizeTree\n ref={treeInstance}\n treeWalker={memoizedTreeWalker}\n itemSize={itemSize}\n height={height}\n async={true}\n width=\"100%\"\n >\n {Node}\n </VariableSizeTree>\n )}\n </AutoSizer>\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"VirtualizedTree.js","names":[],"sources":["../../../../src/components/EntityFinder/tree/VirtualizedTree.tsx"],"sourcesContent":["import React from 'react'\nimport { getEntityChildren } from '@/synapse-client'\nimport { useSynapseContext } from '@/utils'\nimport { formatDate } from '@/utils/functions/DateFormatter'\nimport {\n getEntityTypeFromHeader,\n isContainerType,\n} from '@/utils/functions/EntityTypeUtils'\nimport { Writable } from '@/utils/types/Writable'\nimport { ChevronRight, ExpandMore } from '@mui/icons-material'\nimport { Skeleton, Tooltip, Typography } from '@mui/material'\nimport { EntityType } from '@sage-bionetworks/synapse-client'\nimport { EntityChildrenRequest } from '@sage-bionetworks/synapse-types'\nimport { useQueryClient } from '@tanstack/react-query'\nimport dayjs from 'dayjs'\nimport { cloneDeep } from 'lodash-es'\nimport { ReactNode, useCallback, useEffect, useRef, useState } from 'react'\nimport { useInView } from 'react-intersection-observer'\nimport AutoSizer from 'react-virtualized-auto-sizer'\nimport {\n TreeWalker,\n TreeWalkerValue,\n VariableSizeNodeData,\n VariableSizeNodePublicState,\n VariableSizeTree,\n} from 'react-vtree'\nimport { NodeComponentProps } from 'react-vtree/dist/es/Tree'\nimport { EntityBadgeIcons } from '../../EntityBadgeIcons'\nimport { EntityTypeIcon } from '../../EntityIcon'\nimport { SynapseSpinner } from '../../LoadingScreen/LoadingScreen'\nimport { UserBadge } from '../../UserCard/UserBadge'\nimport { EntityFinderHeader } from '../EntityFinderHeader'\nimport { EntitySelectionMapType } from '../useEntitySelection'\n\nexport enum EntityTreeNodeType {\n /** The tree component's appearance and interactions will facilitate selection. Nodes will be larger and styles will indicate primary selection */\n SINGLE_PANE,\n /** The tree component's appearance and interactions will facilitate browsing. Nodes will be smaller and styles will indicate secondary selection */\n DUAL_PANE,\n}\n\ntype NodeChildren = Readonly<{\n /** The node's children. If undefined, then children have not been fetched. */\n children?: EntityHeaderNode[]\n /** The token used to fetch the next page of children for this entity. If this property is nullish and children is defined, then all children have been fetched. */\n childrenNextPageToken?: string | null\n}>\n\nexport type EntityHeaderNode = (\n | EntityFinderHeader\n // Only will have a subset of fields if the node is fetched via the entity path:\n | Pick<EntityFinderHeader, 'id' | 'name' | 'type'>\n) &\n NodeChildren\ntype PaginationNode = { __paginationNode: true }\n\ntype TreeNode = EntityHeaderNode | RootNodeConfiguration | PaginationNode\n\nfunction getTreeNodeType(\n node: TreeNode,\n): 'entityHeader' | 'rootNodeConfiguration' | 'pagination' {\n if ('__paginationNode' in node) {\n return 'pagination'\n }\n return 'id' in node ? 'entityHeader' : 'rootNodeConfiguration'\n}\n\nfunction isPaginationNode(node: TreeNode): node is PaginationNode {\n return getTreeNodeType(node) === 'pagination'\n}\n\nfunction isEntityHeaderNode(node: TreeNode): node is EntityHeaderNode {\n return getTreeNodeType(node) === 'entityHeader'\n}\n\nfunction isRootNodeConfiguration(\n node: TreeNode,\n): node is RootNodeConfiguration {\n return getTreeNodeType(node) === 'rootNodeConfiguration'\n}\n\nfunction hasMoreChildren(node: TreeNode) {\n if (isPaginationNode(node)) {\n return false\n } else if (isRootNodeConfiguration(node)) {\n return node.hasNextPage\n }\n\n return node.children == null || node.childrenNextPageToken != null\n}\n\nfunction isLeafNode(node: TreeNode) {\n if (isPaginationNode(node)) {\n return true\n } else if (isRootNodeConfiguration(node)) {\n return false\n } else {\n return (\n // Entity is not a container\n !isContainerType(getEntityTypeFromHeader(node)) ||\n // OR Children have been fetched (nonnull) and there are 0 children\n (node.children != null && node.children.length === 0)\n )\n }\n}\n\nfunction maybeHasChildren(\n node: TreeNode,\n): node is RootNodeConfiguration | EntityHeaderNode {\n if (isPaginationNode(node)) {\n return false\n } else if (isRootNodeConfiguration(node)) {\n return node.children.length > 0 || node.hasNextPage\n } else {\n return !isLeafNode(node)\n }\n}\n\nexport type TreeData = VariableSizeNodeData &\n Readonly<{\n node: TreeNode\n getNextPageOfChildren: () => Promise<void>\n isLeaf: boolean\n nestingLevel: number\n setSelectedId: (entityId: string) => void\n treeNodeType: EntityTreeNodeType\n isSelected: boolean\n isDisabled: boolean\n }>\n\ntype NodeMeta = Readonly<{\n nestingLevel: number\n node: TreeNode\n}>\n\nexport type RootNodeConfiguration = {\n show: boolean\n nodeText: string\n children: EntityHeaderNode[]\n /** If undefined, no more entities to fetch */\n fetchNextPage: () => Promise<void>\n hasNextPage: boolean\n}\n\n/**\n * Converts a TreeNode and related data into a TreeWalkerValue that react-vtree can use to render the tree.\n * Exported for testing purposes only.\n * @param config\n * @returns\n */\nexport const getNodeData = (config: {\n node: TreeNode\n nestingLevel: number\n getNextPageOfChildren: () => Promise<void>\n setSelectedId: (entityId: string) => void\n treeNodeType: EntityTreeNodeType\n selected: EntitySelectionMapType\n selectableTypes: EntityType[]\n autoExpand: (entityId: string) => boolean\n defaultHeight: number\n currentContainer?: string | 'root' | null\n}): TreeWalkerValue<TreeData, NodeMeta> => {\n const {\n node,\n nestingLevel,\n getNextPageOfChildren,\n setSelectedId,\n treeNodeType,\n selected,\n selectableTypes,\n autoExpand,\n defaultHeight,\n currentContainer,\n } = config\n\n if (isPaginationNode(node)) {\n throw new Error('Cannot create data for a pagination node')\n }\n\n const id = isRootNodeConfiguration(node) ? 'root' : node.id\n\n const isSelected =\n treeNodeType === EntityTreeNodeType.SINGLE_PANE\n ? selected.has(id)\n : currentContainer === id\n const isDisabled =\n !isRootNodeConfiguration(node) &&\n !selectableTypes.includes(getEntityTypeFromHeader(node))\n\n const isOpenByDefault = autoExpand(id)\n /*\n * If the node is open by default and we haven't fetched its children,\n * fetch the first page (otherwise we won't fetch unless re-toggled)\n */\n if (isOpenByDefault && node.children == null && hasMoreChildren(node)) {\n getNextPageOfChildren()\n }\n\n return {\n data: {\n node,\n getNextPageOfChildren,\n id: id,\n isLeaf: isLeafNode(node),\n isOpenByDefault: isOpenByDefault,\n nestingLevel,\n setSelectedId,\n treeNodeType,\n isSelected,\n isDisabled,\n defaultHeight,\n },\n nestingLevel,\n node,\n }\n}\n\n/**\n * Node component for the react-vtree virtualized tree. Exported only for testing purposes\n */\nexport function Node(\n props: NodeComponentProps<TreeData, VariableSizeNodePublicState<TreeData>>,\n) {\n const {\n data: {\n node,\n getNextPageOfChildren,\n isLeaf,\n id,\n nestingLevel,\n setSelectedId,\n treeNodeType,\n isSelected,\n isDisabled,\n },\n isOpen,\n style,\n setOpen,\n } = props\n\n const [isLoading, setLoading] = useState(false)\n\n const nodeText = isEntityHeaderNode(node) ? (\n <span>{node.name}</span>\n ) : isRootNodeConfiguration(node) ? (\n <span>{node.nodeText}</span>\n ) : (\n // Pagination node\n <Skeleton width={100} />\n )\n\n // We only use this for pagination nodes. If the pagination node comes into view, then immediately call `getNextPageOfChildren`\n const { ref, inView } = useInView()\n\n useEffect(() => {\n if (isPaginationNode(node) && inView) {\n getNextPageOfChildren()\n }\n }, [node, inView, getNextPageOfChildren])\n\n const toggleExpand = useCallback(async () => {\n if (hasMoreChildren(node)) {\n setLoading(true)\n await getNextPageOfChildren()\n await setOpen(!isOpen)\n setLoading(false)\n } else {\n await setOpen(!isOpen)\n }\n }, [getNextPageOfChildren, node, isOpen, setOpen])\n\n /**\n * If in the dual-pane view, expand a node when it becomes selected\n */\n useEffect(() => {\n if (\n treeNodeType === EntityTreeNodeType.DUAL_PANE &&\n isSelected &&\n !isOpen\n ) {\n toggleExpand()\n }\n // Intentionally only toggle the expanded state when isSelected changes, otherwise the node cannot be un-expanded\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [isSelected])\n\n /**\n * If the height is 0, the node is purposefully hidden. Just render a fragment.\n */\n if ('height' in style && style.height === 0) {\n return <></>\n }\n\n let tooltipContent: ReactNode = ''\n if (isEntityHeaderNode(node)) {\n tooltipContent = (\n <div style={{ textAlign: 'center' }}>\n <Typography\n component=\"span\"\n variant=\"smallText1\"\n sx={{\n display: 'inline',\n }}\n >\n {node.name}\n <br />\n <b>ID: </b>\n {node.id}\n {'modifiedBy' in node && (\n <>\n <br />\n <b>Modified By: </b>\n <UserBadge\n userId={node.modifiedBy.toString()}\n showFullName\n disableLink\n showCardOnHover={false}\n />\n </>\n )}\n {'modifiedOn' in node && (\n <>\n <br />\n <b>Modified On: </b>\n {formatDate(dayjs(node.modifiedOn))}\n </>\n )}\n </Typography>\n </div>\n )\n }\n\n const ExpandIcon = isOpen ? ExpandMore : ChevronRight\n\n return (\n <div\n className={`Node ${\n treeNodeType === EntityTreeNodeType.SINGLE_PANE\n ? 'SelectNode'\n : 'BrowseNode'\n }`}\n aria-selected={isSelected}\n aria-disabled={isDisabled}\n onClick={event => {\n event.stopPropagation()\n if (!isDisabled) {\n setSelectedId(id)\n }\n }}\n style={{\n ...style,\n paddingLeft: `${nestingLevel * 20}px`,\n }}\n >\n {!isLeaf && (\n <>\n {isLoading ? (\n <SynapseSpinner size={10} />\n ) : (\n <button\n aria-label={isOpen ? 'Collapse' : 'Expand'}\n onClick={event => {\n event.stopPropagation()\n toggleExpand()\n }}\n >\n <ExpandIcon className=\"ExpandButton\" />\n </button>\n )}\n </>\n )}\n {treeNodeType === EntityTreeNodeType.SINGLE_PANE && ( // SWC-5592\n <div className=\"EntityIcon\">\n {isEntityHeaderNode(node) && (\n <EntityTypeIcon type={getEntityTypeFromHeader(node)} />\n )}\n </div>\n )}\n\n <Tooltip title={tooltipContent} placement=\"right\">\n <div className=\"EntityName\" ref={ref}>\n {nodeText}\n </div>\n </Tooltip>\n\n {treeNodeType === EntityTreeNodeType.SINGLE_PANE && (\n <EntityBadgeIcons\n entityId={id}\n showHasDiscussionThread={false}\n showHasWiki={false}\n showUnlink={false}\n canOpenModal={false}\n />\n )}\n </div>\n )\n}\n\n/**\n * Extracted from the React component for testing purposes\n * @returns a generator function compatible with react-vtree\n */\nexport function getTreeWalkerFunction(\n rootNode: RootNodeConfiguration,\n setSelectedId: VirtualizedTreeProps['setSelectedId'],\n treeNodeType: VirtualizedTreeProps['treeNodeType'],\n selected: VirtualizedTreeProps['selected'],\n selectableTypes: VirtualizedTreeProps['selectableTypes'],\n autoExpand: VirtualizedTreeProps['autoExpand'],\n itemSize: (index?: number) => number,\n currentContainer: VirtualizedTreeProps['currentContainer'],\n fetchNextPageOfChildren: (node: Writable<EntityHeaderNode>) => Promise<void>,\n): TreeWalker<TreeData, NodeMeta> {\n return function* treeWalker() {\n // Step [1]: Define the root node of our tree.\n yield getNodeData({\n node: rootNode,\n nestingLevel: 0,\n getNextPageOfChildren: rootNode.fetchNextPage,\n setSelectedId,\n treeNodeType,\n selected,\n selectableTypes,\n autoExpand,\n defaultHeight: itemSize(),\n currentContainer,\n })\n\n while (true) {\n // Step [2]: Get the parent node and nesting level, which will be passed into the Generator.next function by react-vtree.\n // Yielding `undefined` indicates to react-vtree that we've finished iterating children from the last time we were in the loop.\n const parentMeta = yield\n\n if (maybeHasChildren(parentMeta.node)) {\n // Step [3]: Yielding all the known children of the parent\n if (parentMeta.node.children) {\n for (let i = 0; i < parentMeta.node.children.length; i++) {\n const childNode = parentMeta.node.children[i]\n\n yield getNodeData({\n node: childNode,\n nestingLevel: parentMeta.nestingLevel + 1,\n getNextPageOfChildren: () => fetchNextPageOfChildren(childNode),\n setSelectedId,\n treeNodeType,\n selected,\n selectableTypes,\n autoExpand,\n defaultHeight: itemSize(),\n currentContainer,\n })\n }\n }\n\n // Step [4] - If the parent node has more children, render a \"pagination node\" that will fetch more children when it comes into view (via intersection observer)\n if (hasMoreChildren(parentMeta.node)) {\n const paginationNode: PaginationNode = {\n __paginationNode: true,\n }\n const paginationTreeWalkerValue: TreeWalkerValue<TreeData, NodeMeta> =\n {\n data: {\n id: parentMeta.data.id + '-pagination',\n node: paginationNode,\n isOpenByDefault: false,\n getNextPageOfChildren: parentMeta.data.getNextPageOfChildren,\n isLeaf: true,\n isSelected: false,\n defaultHeight: itemSize(),\n isDisabled: true,\n nestingLevel: parentMeta.nestingLevel + 1,\n setSelectedId: () => {},\n treeNodeType,\n },\n nestingLevel: parentMeta.nestingLevel + 1,\n node: {\n __paginationNode: true,\n },\n }\n yield paginationTreeWalkerValue\n }\n }\n }\n }\n}\n\nexport type VirtualizedTreeProps = Readonly<{\n treeNodeType: EntityTreeNodeType\n rootNodeConfiguration: RootNodeConfiguration\n setSelectedId: (entityId: string) => void\n selected: EntitySelectionMapType\n /* currentContainer is the container whose contents are shown on in the right pane in dual-pane configuration, and may only be defined when treeNodeType is DUAL_PANE */\n currentContainer?: string | 'root' | null\n autoExpand: (entityId: string) => boolean\n selectableTypes: EntityType[]\n visibleTypes: EntityType[]\n}>\n\n/**\n * Component that utilizes react-vtree to efficiently display a tree of entities. react-vtree utilizes react-window\n * to only render visible nodes, which eliminates performance issues as seen in SWC-5978.\n */\nexport const VirtualizedTree = (\n props: VirtualizedTreeProps,\n): React.ReactNode => {\n const {\n rootNodeConfiguration,\n setSelectedId,\n treeNodeType,\n selected,\n currentContainer,\n selectableTypes,\n visibleTypes,\n autoExpand,\n } = props\n\n const { accessToken, keyFactory } = useSynapseContext()\n const queryClient = useQueryClient()\n\n const [rootNode, setRootNode] = useState<RootNodeConfiguration>(\n rootNodeConfiguration,\n )\n\n const treeInstance = useRef<VariableSizeTree<TreeData>>(null)\n\n useEffect(() => {\n setRootNode(rootNodeConfiguration)\n }, [rootNodeConfiguration, rootNodeConfiguration.children])\n\n // This function is used by VariableSizeTree to identify the size of each node in the tree.\n const itemSize = useCallback(\n (index?: number) => {\n /**\n * We must have a root node, but we don't always want to show it.\n * In those cases, set the height of the root node to 0.\n */\n if (index === 0 && !rootNodeConfiguration.show) {\n return 0\n }\n /**\n * The height of all other nodes in the tree varies depending on the tree node type.\n */\n return treeNodeType === EntityTreeNodeType.DUAL_PANE ? 30 : 60\n },\n [treeNodeType, rootNodeConfiguration.show],\n )\n\n /**\n * Fetches the next page of children for a given node. After fetching the children, this method will make a deep clone of the\n * full tree, including the new data, and updates the state of the component\n */\n const fetchNextPageOfChildren = useCallback(\n // Because we update the root node with a copy at the end of this function, we can write to the node under update.\n async (node: Writable<EntityHeaderNode>) => {\n // Fetch the children of the node\n const entityChildrenRequest: EntityChildrenRequest = {\n parentId: node.id,\n nextPageToken: node.childrenNextPageToken,\n includeTypes: visibleTypes,\n }\n const children = await queryClient.fetchQuery({\n queryKey: keyFactory.getEntityChildrenQueryKey(\n entityChildrenRequest,\n false,\n ),\n queryFn: () => getEntityChildren(entityChildrenRequest, accessToken),\n })\n // Update the node data -- add the children and store the nextPageToken\n if (node.children) {\n for (const newChild of children.page) {\n // SWC-6751 - Prevent adding duplicate entries in case this function is called twice\n if (!node.children.find(child => child.id == newChild.id)) {\n node.children.push(newChild)\n }\n }\n } else {\n node.children = children.page\n }\n node.childrenNextPageToken = children.nextPageToken\n\n // Ensure the node for which we fetched children is expanded/open\n if (treeInstance.current) {\n await treeInstance.current.recomputeTree({\n [node.id]: {\n open: true,\n },\n })\n }\n\n // cloneDeep is required to re-render the tree with the new children\n setRootNode(cloneDeep(rootNode))\n },\n [visibleTypes, queryClient, keyFactory, rootNode, accessToken],\n )\n\n /**\n * treeWalker is a generator function used by react-vtree to generate the tree structure. The tree is re-built when the function is updated,\n * so the dependencies specified in useCallback are important.\n */\n const memoizedTreeWalker = useCallback(\n getTreeWalkerFunction(\n rootNode,\n setSelectedId,\n treeNodeType,\n selected,\n selectableTypes,\n autoExpand,\n itemSize,\n currentContainer,\n fetchNextPageOfChildren,\n ),\n [\n rootNode,\n setSelectedId,\n treeNodeType,\n selected,\n selectableTypes,\n autoExpand,\n currentContainer,\n fetchNextPageOfChildren,\n itemSize,\n ],\n )\n\n return (\n <AutoSizer disableWidth>\n {({ height }: { height: number }) => (\n <VariableSizeTree\n ref={treeInstance}\n treeWalker={memoizedTreeWalker}\n itemSize={itemSize}\n height={height}\n async={true}\n width=\"100%\"\n >\n {Node}\n </VariableSizeTree>\n )}\n </AutoSizer>\n )\n}\n"],"mappings":"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"}
@@ -7,6 +7,7 @@ type EntityTypeIconProps = {
7
7
  className?: string;
8
8
  includeTooltip?: boolean;
9
9
  wrap?: boolean;
10
+ variant?: 'twoTone' | 'outlined';
10
11
  };
11
12
  export declare function EntityTypeIcon(props: Omit<IconSvgProps, 'icon'> & EntityTypeIconProps): import("react/jsx-runtime").JSX.Element;
12
13
  export default EntityTypeIcon;
@@ -1 +1 @@
1
- {"version":3,"file":"EntityIcon.d.ts","sourceRoot":"","sources":["../../src/components/EntityIcon.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,kCAAkC,CAAA;AAC7D,OAAO,EAAE,aAAa,EAAY,MAAM,OAAO,CAAA;AAC/C,OAAgB,EAEd,YAAY,EAEb,MAAM,mBAAmB,CAAA;AAwB1B,KAAK,mBAAmB,GAAG;IACzB,IAAI,EAAE,UAAU,CAAA;IAChB,KAAK,CAAC,EAAE,aAAa,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,IAAI,CAAC,EAAE,OAAO,CAAA;CACf,CAAA;AAED,wBAAgB,cAAc,CAC5B,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,GAAG,mBAAmB,2CA6BxD;AAED,eAAe,cAAc,CAAA"}
1
+ {"version":3,"file":"EntityIcon.d.ts","sourceRoot":"","sources":["../../src/components/EntityIcon.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,kCAAkC,CAAA;AAC7D,OAAO,EAAE,aAAa,EAAY,MAAM,OAAO,CAAA;AAC/C,OAAgB,EAEd,YAAY,EAGb,MAAM,mBAAmB,CAAA;AA6B1B,KAAK,mBAAmB,GAAG;IACzB,IAAI,EAAE,UAAU,CAAA;IAChB,KAAK,CAAC,EAAE,aAAa,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,OAAO,CAAC,EAAE,SAAS,GAAG,UAAU,CAAA;CACjC,CAAA;AAED,wBAAgB,cAAc,CAC5B,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,GAAG,mBAAmB,2CA8BxD;AAED,eAAe,cAAc,CAAA"}