diff --git a/apps/portals/elportal/src/config/resources.ts b/apps/portals/elportal/src/config/resources.ts index 6a10fbf3301..fc3c7913bc4 100644 --- a/apps/portals/elportal/src/config/resources.ts +++ b/apps/portals/elportal/src/config/resources.ts @@ -16,6 +16,8 @@ export const cohortBuilderFilesSql = 'SELECT * FROM syn52234677' export const partnersSql = 'SELECT * FROM syn62661043 order by organizationName desc' export const whatWeDoSql = 'SELECT * FROM syn64130706' +export const featuredResearchSql = + 'SELECT * FROM syn64542019 ORDER BY order ASC' export const defaultSearchConfiguration = { fullTextSearchHelpURL: 'https://help.eliteportal.org/help/search-tips', diff --git a/apps/portals/elportal/src/pages/HomePageV2.tsx b/apps/portals/elportal/src/pages/HomePageV2.tsx index 098b3bf1b87..24a621c8aca 100644 --- a/apps/portals/elportal/src/pages/HomePageV2.tsx +++ b/apps/portals/elportal/src/pages/HomePageV2.tsx @@ -3,10 +3,15 @@ import { RecentPublicationsGrid, ImageCardGridWithLinks, PortalFeatureHighlights, + FeaturedResearch, } from 'synapse-react-client' import ELContributeYourData from '@sage-bionetworks/synapse-portal-framework/components/elportal/ELContributeYourData' import ELGettingStarted from '@sage-bionetworks/synapse-portal-framework/components/elportal/ELGettingStarted' -import { topPublicationsSql, whatWeDoSql } from '../config/resources' +import { + topPublicationsSql, + whatWeDoSql, + featuredResearchSql, +} from '../config/resources' import { Link, Typography } from '@mui/material' import analyzetheclouds from '../assets/analyzetheclouds.png' import computationaltools from '../assets/computationaltools.png' @@ -45,6 +50,7 @@ export default function HomePage() { title="What We Do" summaryText="We provide multi-omic datasets, software tools, and publications that empower researchers to discover the latest health-promoting therapeutics." /> + {/* Commented out for release (see EC-485) */} {/*
*/} {/* Note - this script is run automatically as part of the build command. + +## Testing Synapse React Client (SRC) in Synapse Web Client (SWC) + +This guide explains how to test changes made in the Synapse React Client (SRC) on the Synapse Web Client (SWC) using **Chrome DevTools**. + +1. Make Your Changes in SRC + +Edit the code in the `synapse-react-client` package as needed. + +2. Build the UMD Files + +Run the following command to build the UMD files for SRC: + +```bash +pnpm build:umd +``` + +3. Go to https://www.synapse.org in Chrome and open the **Developer Tools**. + +4. Locate the synapse-react-client.production.min.js file in the Network tab and right click on the file and select **Override Content**. + +5. Go to the **Sources** tab in Developer Tools and go to the **Overrides** section. Make sure **Enabled Local Overrides** is checked. + +6. Right-click cdn-www.synapse.org/generated or synapse-react-client.production.min.js and select **Open Folder** or **Open Containing Folder** respectively + +7. In your file explore, navigate to packages/synapse-react-client/dist/umd/ and find the updated synapse-react-client.production.min.js file. Drag and drop this file to the generated folder to replace the old synapse-react-client.production.min.js file. + +8. Refresh https://www.synapse.org and the site should now be using your override file. + +9. Repeat steps above for any SRC changes you want to see in https://www.synapse.org. diff --git a/packages/synapse-react-client/src/components/FeaturedResearch/FeaturedResearch.test.tsx b/packages/synapse-react-client/src/components/FeaturedResearch/FeaturedResearch.test.tsx new file mode 100644 index 00000000000..5a504bb3e65 --- /dev/null +++ b/packages/synapse-react-client/src/components/FeaturedResearch/FeaturedResearch.test.tsx @@ -0,0 +1,185 @@ +import FeaturedResearch, { FeaturedResearchProps } from './FeaturedResearch' +import { screen, render, waitFor } from '@testing-library/react' +import { createWrapper } from '../../testutils/TestingLibraryUtils' +import useGetQueryResultBundle from '../../synapse-queries/entity/useGetQueryResultBundle' +import { + BatchFileResult, + ColumnTypeEnum, + QueryResultBundle, +} from '@sage-bionetworks/synapse-types' +import { getUseQuerySuccessMock } from '../../testutils/ReactQueryMockUtils' +import { SynapseClient } from '../../index' + +jest.mock('../../synapse-queries/entity/useGetQueryResultBundle') +const mockUseGetQueryResultBundle = jest.mocked(useGetQueryResultBundle) + +describe('FeaturedResearch Tests', () => { + const mockProps: FeaturedResearchProps = { + sql: 'SELECT * FROM syn64542019', + } + + const mockQueryResult: QueryResultBundle = { + concreteType: 'org.sagebionetworks.repo.model.table.QueryResultBundle', + queryResult: { + concreteType: 'org.sagebionetworks.repo.model.table.QueryResult', + queryResults: { + concreteType: 'org.sagebionetworks.repo.model.table.RowSet', + tableId: 'syn64542019', + etag: 'DEFAULT', + headers: [ + { + name: 'title', + columnType: ColumnTypeEnum.STRING, + id: '1', + }, + { + name: 'description', + columnType: ColumnTypeEnum.STRING, + id: '2', + }, + { + name: 'publicationDate', + columnType: ColumnTypeEnum.DATE, + id: '3', + }, + { + name: 'tags', + columnType: ColumnTypeEnum.STRING_LIST, + id: '4', + }, + { + name: 'affiliation', + columnType: ColumnTypeEnum.STRING, + id: '5', + }, + { + name: 'image', + columnType: ColumnTypeEnum.FILEHANDLEID, + id: '6', + }, + { name: 'link', columnType: ColumnTypeEnum.LINK, id: '7' }, + { name: 'order', columnType: ColumnTypeEnum.INTEGER, id: '8' }, + ], + rows: [ + { + rowId: 1, + values: [ + 'Title 1', + 'Description 1', + '1726164997000', + '["tag1_1", "tag1_2"]', + 'affiliation 1', + '151525812', + 'https://mockurl.com/data-release-1', + '2', + ], + }, + { + rowId: 2, + values: [ + 'Title 2', + 'Description 2', + '1726164997000', + '["tag2_1"]', + 'affiliation 2', + '151468828', + 'https://mockurl.com/data-release-2', + '2', + ], + }, + ], + }, + }, + selectColumns: [ + { + name: 'title', + columnType: ColumnTypeEnum.STRING, + id: '1', + }, + { + name: 'description', + columnType: ColumnTypeEnum.STRING, + id: '2', + }, + { + name: 'publicationDate', + columnType: ColumnTypeEnum.DATE, + id: '3', + }, + { + name: 'tags', + columnType: ColumnTypeEnum.STRING_LIST, + id: '4', + }, + { + name: 'affiliation', + columnType: ColumnTypeEnum.STRING, + id: '5', + }, + { + name: 'image', + columnType: ColumnTypeEnum.FILEHANDLEID, + id: '6', + }, + { name: 'link', columnType: ColumnTypeEnum.LINK, id: '7' }, + { name: 'order', columnType: ColumnTypeEnum.INTEGER, id: '8' }, + ], + } + + const mockFileResult = [ + { + fileHandleId: '151525812', + preSignedURL: 'https://mockurl.com/cat1.jpeg', + }, + { + fileHandleId: '151468828', + preSignedURL: 'https://mockurl.com/cat2.jpeg', + }, + ] + + const mockBatchFileResult: BatchFileResult = { + requestedFiles: mockFileResult, + } + + beforeEach(() => { + jest.clearAllMocks() + jest.spyOn(SynapseClient, 'getFiles').mockResolvedValue(mockBatchFileResult) + mockUseGetQueryResultBundle.mockReturnValue( + getUseQuerySuccessMock(mockQueryResult), + ) + }) + + function renderComponent(props: FeaturedResearchProps) { + return render(, { + wrapper: createWrapper(), + }) + } + + it('fetches and displays research cards', async () => { + mockUseGetQueryResultBundle.mockReturnValue( + getUseQuerySuccessMock(mockQueryResult), + ) + renderComponent(mockProps) + + await waitFor(() => + expect(mockUseGetQueryResultBundle).toHaveBeenCalledTimes(1), + ) + + expect(screen.getByText('Featured Research')).toBeInTheDocument() + expect(screen.getByText('Read more')).toBeInTheDocument() + expect(screen.getByText('Title 1')).toBeInTheDocument() + expect(screen.getByText('Description 1')).toBeInTheDocument() + expect(screen.getByText('tag1_1')).toBeInTheDocument() + expect(screen.getByText('affiliation 1')).toBeInTheDocument() + + expect(screen.getByText('Title 2')).toBeInTheDocument() + expect(screen.getByText('tag2_1')).toBeInTheDocument() + expect(screen.getByText('affiliation 2')).toBeInTheDocument() + expect(screen.getByText('September, 2024')).toBeInTheDocument() + + await waitFor(() => { + const images = document.querySelectorAll('.MuiCardMedia-root') + expect(images).toHaveLength(2) + }) + }) +}) diff --git a/packages/synapse-react-client/src/components/FeaturedResearch/FeaturedResearch.tsx b/packages/synapse-react-client/src/components/FeaturedResearch/FeaturedResearch.tsx new file mode 100644 index 00000000000..c8f9174a29c --- /dev/null +++ b/packages/synapse-react-client/src/components/FeaturedResearch/FeaturedResearch.tsx @@ -0,0 +1,364 @@ +import { + Box, + CardMedia, + Link, + Stack, + Typography, + Skeleton, +} from '@mui/material' +import { + FileHandleAssociateType, + FileHandleAssociation, + QueryBundleRequest, + Row, +} from '@sage-bionetworks/synapse-types' +import useGetQueryResultBundle from '../../synapse-queries/entity/useGetQueryResultBundle' +import { useGetStablePresignedUrl } from '../../synapse-queries' +import { getFieldIndex } from '../../utils/functions/queryUtils' +import * as SynapseConstants from '../../utils/SynapseConstants' +import { parseEntityIdFromSqlStatement } from '../../utils/functions/SqlFunctions' +import { formatDate } from '../../utils/functions/DateFormatter' +import dayjs from 'dayjs' + +export type FeaturedResearchProps = { + sql: string +} + +export type FeaturedResearchCardProps = { + research: Row + entityId: string + isLoading: boolean + affiliationColIndex: number + publicationDateColIndex: number + titleColIndex: number + descriptionColIndex: number + tagsColIndex: number + linkColIndex: number + imageColIndex: number +} + +const useImageUrl = (fileId: string, entityId: string) => { + const fha: FileHandleAssociation = { + associateObjectId: entityId, + associateObjectType: FileHandleAssociateType.TableEntity, + fileHandleId: fileId || '', + } + const stablePresignedUrl = useGetStablePresignedUrl(fha, false, { + enabled: !!fileId, + }) + const dataUrl = stablePresignedUrl?.dataUrl + + return dataUrl +} + +const parseTags = ( + tagsColIndex: number, + research: { values: (string | null)[] }, +): string[] => { + try { + const tags = ( + research.values[tagsColIndex] + ? JSON.parse(research.values[tagsColIndex] || '') + : [] + ) as string[] + return tags + } catch (e) { + console.error(e) + return [] + } +} + +const FeaturedResearchCard = ({ + research, + entityId, + affiliationColIndex, + publicationDateColIndex, + titleColIndex, + tagsColIndex, + linkColIndex, + imageColIndex, + isLoading, +}: FeaturedResearchCardProps) => { + const fileId = research.values[imageColIndex] ?? '' + const url = useImageUrl(fileId ?? '', entityId) + if (isLoading) { + return + } + return ( + + + + {research.values[affiliationColIndex]} + + + + {research.values[titleColIndex]} + + + + {parseTags(tagsColIndex, research)[0] && ( + + {parseTags(tagsColIndex, research)[0] || ''} + + )} + + {research.values[publicationDateColIndex] && + formatDate( + dayjs(Number(research.values[publicationDateColIndex])), + 'MMMM, YYYY', + )} + + + + + ) +} + +const FeaturedResearchTopCard = ({ + research, + entityId, + affiliationColIndex, + titleColIndex, + descriptionColIndex, + tagsColIndex, + linkColIndex, + imageColIndex, + isLoading, +}: FeaturedResearchCardProps) => { + const fileId = research.values[imageColIndex] ?? '' + const url = useImageUrl(fileId || '', entityId) + if (isLoading) { + return ( + + ) + } + return ( +
+
+ ) +} + +function FeaturedResearch(props: FeaturedResearchProps) { + const { sql } = props + + const entityId = parseEntityIdFromSqlStatement(sql) + + const queryBundleRequest: QueryBundleRequest = { + partMask: + SynapseConstants.BUNDLE_MASK_QUERY_SELECT_COLUMNS | + SynapseConstants.BUNDLE_MASK_QUERY_RESULTS, + concreteType: 'org.sagebionetworks.repo.model.table.QueryBundleRequest', + entityId, + query: { + sql, + }, + } + const { data: queryResultBundle, isLoading } = + useGetQueryResultBundle(queryBundleRequest) + + const dataRows = queryResultBundle?.queryResult!.queryResults.rows ?? [] + + enum ExpectedColumns { + TITLE = 'title', + DESCRIPTION = 'description', + PUBLICATION_DATE = 'publicationDate', + TAGS = 'tags', + AFFILIATION = 'affiliation', + IMAGE = 'image', + LINK = 'link', + } + + const titleColIndex = getFieldIndex(ExpectedColumns.TITLE, queryResultBundle) + const descriptionColIndex = getFieldIndex( + ExpectedColumns.DESCRIPTION, + queryResultBundle, + ) + const publicationDateColIndex = getFieldIndex( + ExpectedColumns.PUBLICATION_DATE, + queryResultBundle, + ) + const tagsColIndex = getFieldIndex(ExpectedColumns.TAGS, queryResultBundle) + const affiliationColIndex = getFieldIndex( + ExpectedColumns.AFFILIATION, + queryResultBundle, + ) + const imageColIndex = getFieldIndex(ExpectedColumns.IMAGE, queryResultBundle) + const linkColIndex = getFieldIndex(ExpectedColumns.LINK, queryResultBundle) + + const topCard = dataRows[0] + const remainingCards = dataRows.slice(1) + + return ( + + + {topCard && ( + + )} + + + + Featured Research + + {remainingCards.map((research, index) => ( + + ))} + + + ) +} + +export default FeaturedResearch diff --git a/packages/synapse-react-client/src/components/FeaturedResearch/index.ts b/packages/synapse-react-client/src/components/FeaturedResearch/index.ts new file mode 100644 index 00000000000..b79455877e5 --- /dev/null +++ b/packages/synapse-react-client/src/components/FeaturedResearch/index.ts @@ -0,0 +1,4 @@ +import FeaturedResearch from './FeaturedResearch' +import type { FeaturedResearchProps } from './FeaturedResearch' +export { FeaturedResearch, FeaturedResearchProps } +export default FeaturedResearch diff --git a/packages/synapse-react-client/src/components/index.ts b/packages/synapse-react-client/src/components/index.ts index 6ed6945a7a7..55fe60c2f7f 100644 --- a/packages/synapse-react-client/src/components/index.ts +++ b/packages/synapse-react-client/src/components/index.ts @@ -86,6 +86,7 @@ export * from './SageResourcesPopover' export * from './RecentPublicationsGrid' export * from './ImageCardGridWithLinks' export * from './PortalFeatureHighlights' +export * from './FeaturedResearch' // TODO: Find a better way to expose Icon components export { Project as ProjectIcon } from '../assets/themed_icons/Project'