diff --git a/apps/portals/adknowledgeportal/src/config/synapseConfigs/publications.ts b/apps/portals/adknowledgeportal/src/config/synapseConfigs/publications.ts index 69f8b937c7..c52eafd358 100644 --- a/apps/portals/adknowledgeportal/src/config/synapseConfigs/publications.ts +++ b/apps/portals/adknowledgeportal/src/config/synapseConfigs/publications.ts @@ -1,6 +1,7 @@ import { SynapseConfig } from '@sage-bionetworks/synapse-portal-framework/types/portal-config' import { SynapseConstants } from 'synapse-react-client' import { publicationsSql } from '../resources' +import { Direction } from '@sage-bionetworks/synapse-types' const rgbIndex = 5 @@ -33,6 +34,7 @@ const publications: SynapseConfig = { sql: publicationsSql, name: 'Publications', shouldDeepLink: true, + facetValueSortConfigs: [{ columnName: 'year', direction: Direction.DESC }], facetsToPlot: ['Program', 'year', 'grant', 'journal'], cardConfiguration: publicationCardProps, columnAliases, diff --git a/apps/portals/bsmn/src/config/synapseConfigs/publications.ts b/apps/portals/bsmn/src/config/synapseConfigs/publications.ts index 484d83a51a..e1dbe4a7dc 100644 --- a/apps/portals/bsmn/src/config/synapseConfigs/publications.ts +++ b/apps/portals/bsmn/src/config/synapseConfigs/publications.ts @@ -2,6 +2,7 @@ import { SynapseConfig } from '@sage-bionetworks/synapse-portal-framework/types/ import { SynapseConstants } from 'synapse-react-client' import type { CardConfiguration } from 'synapse-react-client' import { publicationsSql } from '../resources' +import { Direction } from '@sage-bionetworks/synapse-types' const rgbIndex = 5 @@ -25,6 +26,7 @@ const publications: SynapseConfig = { name: 'Publications', cardConfiguration: publicationsCardConfiguration, sql: publicationsSql, + facetValueSortConfigs: [{ columnName: 'year', direction: Direction.DESC }], facetsToPlot: ['grantNumber', 'year', 'journal', 'projectTitle'], searchConfiguration: { searchable: ['title', 'authors', 'year', 'journal', 'grantNumber'], diff --git a/apps/portals/cancercomplexity/src/config/synapseConfigs/publications.ts b/apps/portals/cancercomplexity/src/config/synapseConfigs/publications.ts index a3bfc49aa7..ab99a46cdf 100644 --- a/apps/portals/cancercomplexity/src/config/synapseConfigs/publications.ts +++ b/apps/portals/cancercomplexity/src/config/synapseConfigs/publications.ts @@ -4,6 +4,7 @@ import type { GenericCardSchema } from 'synapse-react-client' import type { CardConfiguration } from 'synapse-react-client' import columnAliases from '../columnAliases' import { publicationSql } from '../resources' +import { Direction } from '@sage-bionetworks/synapse-types' const rgbIndex = 1 @@ -68,6 +69,9 @@ export const publications: SynapseConfig = { shouldDeepLink: true, name: 'Publications', columnAliases, + facetValueSortConfigs: [ + { columnName: 'publicationYear', direction: Direction.DESC }, + ], searchConfiguration: { searchable: [ 'publicationTitle', diff --git a/apps/portals/elportal/src/config/synapseConfigs/publications.ts b/apps/portals/elportal/src/config/synapseConfigs/publications.ts index b891134af0..48a393e3fd 100644 --- a/apps/portals/elportal/src/config/synapseConfigs/publications.ts +++ b/apps/portals/elportal/src/config/synapseConfigs/publications.ts @@ -1,6 +1,7 @@ import { SynapseConfig } from '@sage-bionetworks/synapse-portal-framework/types/portal-config' import { SynapseConstants } from 'synapse-react-client' import { defaultSearchConfiguration, publicationsSql } from '../resources' +import { Direction } from '@sage-bionetworks/synapse-types' const rgbIndex = 5 @@ -23,6 +24,7 @@ const publications: SynapseConfig = { name: 'Publications', shouldDeepLink: true, facetsToPlot: ['Program', 'Year', 'Grant', 'Journal'], + facetValueSortConfigs: [{ columnName: 'Year', direction: Direction.DESC }], cardConfiguration: publicationCardProps, searchConfiguration: defaultSearchConfiguration, }, diff --git a/apps/portals/nf/src/config/synapseConfigs/publications.ts b/apps/portals/nf/src/config/synapseConfigs/publications.ts index dc77ee2706..7864781ab7 100644 --- a/apps/portals/nf/src/config/synapseConfigs/publications.ts +++ b/apps/portals/nf/src/config/synapseConfigs/publications.ts @@ -4,6 +4,7 @@ import { SynapseConfig } from '@sage-bionetworks/synapse-portal-framework/types/ import { columnAliases } from './commonProps' import { publicationsSql } from '../resources' +import { Direction } from '@sage-bionetworks/synapse-types' export const newPublicationsSql = `${publicationsSql} order by ROW_ID desc limit 3` const type = SynapseConstants.GENERIC_CARD @@ -50,6 +51,7 @@ const publications: SynapseConfig = { name: 'Publications', cardConfiguration: publicationsCardConfiguration, columnAliases, + facetValueSortConfigs: [{ columnName: 'year', direction: Direction.DESC }], searchConfiguration: { searchable: [ 'title', diff --git a/packages/synapse-react-client/src/components/QueryWrapperPlotNav/QueryWrapperPlotNav.stories.tsx b/packages/synapse-react-client/src/components/QueryWrapperPlotNav/QueryWrapperPlotNav.stories.tsx index 32eb7a6ce8..4151e902eb 100644 --- a/packages/synapse-react-client/src/components/QueryWrapperPlotNav/QueryWrapperPlotNav.stories.tsx +++ b/packages/synapse-react-client/src/components/QueryWrapperPlotNav/QueryWrapperPlotNav.stories.tsx @@ -9,6 +9,7 @@ import { ColumnMultiValueFunction, ColumnSingleValueFilterOperator, ColumnSingleValueQueryFilter, + Direction, Query, } from '@sage-bionetworks/synapse-types' import QueryWrapperPlotNav, { @@ -76,6 +77,9 @@ export const Cards: Story = { defaultShowPlots: false, defaultShowSearchBox: true, shouldDeepLink: true, + facetValueSortConfigs: [ + { columnName: 'usageRequirements', direction: Direction.DESC }, + ], cardConfiguration: { type: GENERIC_CARD, titleLinkConfig: { diff --git a/packages/synapse-react-client/src/components/QueryWrapperPlotNav/QueryWrapperPlotNav.tsx b/packages/synapse-react-client/src/components/QueryWrapperPlotNav/QueryWrapperPlotNav.tsx index b2eb01688e..3c22742198 100644 --- a/packages/synapse-react-client/src/components/QueryWrapperPlotNav/QueryWrapperPlotNav.tsx +++ b/packages/synapse-react-client/src/components/QueryWrapperPlotNav/QueryWrapperPlotNav.tsx @@ -69,6 +69,11 @@ type QueryWrapperPlotNavOwnProps = { > facetsToPlot?: string[] availableFacets?: FacetFilterControlsProps['availableFacets'] + /** + * Controls the sort order (ascending or descending) of facet values for a particular column. + * Note: This parameter does not currently apply to the pie/bar chart visualizations, but it probably should. + */ + facetValueSortConfigs?: FacetFilterControlsProps['facetValueSortConfigs'] customPlots?: QueryWrapperSynapsePlotProps[] defaultColumn?: string defaultShowSearchBox?: boolean @@ -122,6 +127,7 @@ type QueryWrapperPlotNavContentsProps = Pick< | 'cardConfiguration' | 'facetsToPlot' | 'availableFacets' + | 'facetValueSortConfigs' | 'hideDownload' | 'hideQueryCount' | 'hideSqlEditorControl' @@ -146,6 +152,7 @@ function QueryWrapperPlotNavContents(props: QueryWrapperPlotNavContentsProps) { cardConfiguration, facetsToPlot, availableFacets, + facetValueSortConfigs, hideDownload, hideQueryCount, hideSqlEditorControl, @@ -231,7 +238,10 @@ function QueryWrapperPlotNavContents(props: QueryWrapperPlotNavContentsProps) { {isFaceted && ( <> - + )} { afterAll(() => server.close()) beforeEach(() => { jest.clearAllMocks() - // Replace clipboard.writeText with a mock + }) + + it('Copies short.io response to clipboard', async () => { Object.assign(navigator, { clipboard: { writeText: jest.fn().mockImplementation(() => Promise.resolve()), }, }) - }) - - it('Copies short.io response to clipboard', async () => { renderComponent({ shortIoPublicApiKey: 'abc' }) expect(screen.queryByRole('alert')).not.toBeInTheDocument() diff --git a/packages/synapse-react-client/src/components/widgets/query-filter/EnumFacetFilter/EnumFacetFilter.integration.test.tsx b/packages/synapse-react-client/src/components/widgets/query-filter/EnumFacetFilter/EnumFacetFilter.integration.test.tsx index 38b01c37bd..3ce7ad7d7f 100644 --- a/packages/synapse-react-client/src/components/widgets/query-filter/EnumFacetFilter/EnumFacetFilter.integration.test.tsx +++ b/packages/synapse-react-client/src/components/widgets/query-filter/EnumFacetFilter/EnumFacetFilter.integration.test.tsx @@ -3,6 +3,7 @@ import { EnumFacetFilter, EnumFacetFilterProps } from './EnumFacetFilter' import { ColumnModel, ColumnTypeEnum, + Direction, FacetColumnResultValueCount, FacetColumnResultValues, QueryBundleRequest, @@ -65,6 +66,16 @@ const entityFacetValues: FacetColumnResultValueCount[] = [ { value: mockTableEntity.id.replace('syn', ''), count: 1, isSelected: false }, ] +const integerFacetValues: FacetColumnResultValueCount[] = [ + { value: '20030', count: 2, isSelected: false }, + { value: '2010', count: 1, isSelected: true }, + { + value: SynapseConstants.VALUE_NOT_SET, + count: 1, + isSelected: false, + }, +] + const columnModel: ColumnModel = { columnType: ColumnTypeEnum.STRING, facetType: 'enumeration', @@ -79,6 +90,19 @@ const facet: FacetColumnResultValues = { concreteType: 'org.sagebionetworks.repo.model.table.FacetColumnResultValues', } +const integerColumnModel: ColumnModel = { + columnType: ColumnTypeEnum.INTEGER, + facetType: 'enumeration', + id: '86424', + name: 'Year', +} +const integerFacet: FacetColumnResultValues = { + columnName: 'Year', + facetType: 'enumeration', + facetValues: integerFacetValues, + concreteType: 'org.sagebionetworks.repo.model.table.FacetColumnResultValues', +} + function createTestProps( overrides?: Partial, ): EnumFacetFilterProps { @@ -193,6 +217,75 @@ describe('EnumFacetFilter', () => { expect(counts[2]).toHaveTextContent(`${stringFacetValues[2].count}`) }) + it('should set labels correctly for INTEGER type', async () => { + registerTableQueryResult(nextQueryRequest.query, { + ...mockQueryResponseData, + columnModels: [integerColumnModel], + }) + const { container } = await init({ facet: integerFacet }) + + const checkboxes = await screen.findAllByRole( + 'checkbox', + ) + const counts = container.querySelectorAll( + '.EnumFacetFilter__count', + ) + + expect(checkboxes).toHaveLength(4) + expect(counts).toHaveLength(3) + + expect(checkboxes[0]).toHaveAccessibleName('All') + + // Note: Facet values are resorted to numberical order! [1] should appear before [0] + expect(checkboxes[1]).toHaveAccessibleName( + `${integerFacetValues[1].value}`, + ) + expect(counts[0]).toHaveTextContent(`${integerFacetValues[1].count}`) + + expect(checkboxes[2]).toHaveAccessibleName( + `${integerFacetValues[0].value}`, + ) + expect(counts[1]).toHaveTextContent(`${integerFacetValues[0].count}`) + + expect(checkboxes[3]).toHaveAccessibleName(`Not Assigned`) + expect(counts[2]).toHaveTextContent(`${integerFacetValues[2].count}`) + }) + + it('should reverse sort order if configured', async () => { + const { container } = await init({ + sortConfig: { + columnName: 'MAKE', + direction: Direction.DESC, + }, + }) + + const checkboxes = await screen.findAllByRole( + 'checkbox', + ) + const counts = container.querySelectorAll( + '.EnumFacetFilter__count', + ) + + expect(checkboxes).toHaveLength(4) + expect(counts).toHaveLength(3) + + expect(checkboxes[0]).toHaveAccessibleName('All') + + // PORTALS-3252 note: Facet values are reverse sorted! [0] will appear before [1] again + expect(checkboxes[1]).toHaveAccessibleName( + `${stringFacetValues[0].value}`, + ) + expect(counts[0]).toHaveTextContent(`${stringFacetValues[0].count}`) + + expect(checkboxes[2]).toHaveAccessibleName( + `${stringFacetValues[1].value}`, + ) + expect(counts[1]).toHaveTextContent(`${stringFacetValues[1].count}`) + + expect(checkboxes[3]).toHaveAccessibleName(`Not Assigned`) + expect(counts[2]).toHaveTextContent(`${stringFacetValues[2].count}`) + }) + it('should set labels correctly for ENTITYID type', async () => { const entityColumnModel: ColumnModel = { ...columnModel, diff --git a/packages/synapse-react-client/src/components/widgets/query-filter/EnumFacetFilter/EnumFacetFilter.tsx b/packages/synapse-react-client/src/components/widgets/query-filter/EnumFacetFilter/EnumFacetFilter.tsx index e9e7ae24da..820b68d91a 100644 --- a/packages/synapse-react-client/src/components/widgets/query-filter/EnumFacetFilter/EnumFacetFilter.tsx +++ b/packages/synapse-react-client/src/components/widgets/query-filter/EnumFacetFilter/EnumFacetFilter.tsx @@ -2,6 +2,7 @@ import React, { Suspense, useMemo } from 'react' import useGetInfoFromIds from '../../../../utils/hooks/useGetInfoFromIds' import { ColumnTypeEnum, + Direction, EntityHeader, Evaluation, FacetColumnRequest, @@ -29,6 +30,12 @@ export type EnumFacetFilterProps = { containerAs?: 'Collapsible' | 'Dropdown' dropdownType?: 'Icon' | 'SelectBox' hideCollapsible?: boolean + sortConfig?: FacetValueSortConfig +} + +export type FacetValueSortConfig = { + columnName: string + direction: Direction } function _EnumFacetFilter(props: EnumFacetFilterProps) { @@ -37,6 +44,7 @@ function _EnumFacetFilter(props: EnumFacetFilterProps) { containerAs = 'Collapsible', dropdownType = 'Icon', hideCollapsible = false, + sortConfig, } = props const { nextQueryRequest, @@ -82,6 +90,17 @@ function _EnumFacetFilter(props: EnumFacetFilterProps) { ? getCorrespondingColumnForFacet(facet, queryMetadata.columnModels) : undefined + const isNumberColumnType = useMemo(() => { + switch (columnModel?.columnType) { + case ColumnTypeEnum.DOUBLE: + case ColumnTypeEnum.DATE: + case ColumnTypeEnum.INTEGER: + return true + default: + return false + } + }, [columnModel]) + const userIds = columnModel?.columnType === ColumnTypeEnum.USERID || columnModel?.columnType === ColumnTypeEnum.USERID_LIST @@ -137,8 +156,19 @@ function _EnumFacetFilter(props: EnumFacetFilterProps) { ) const valueNotSetFacetArray = partitions[0] const restOfFacetValuesArray = partitions[1] + let sortedValues: RenderedFacetValue[] + if (isNumberColumnType) { + sortedValues = sortBy(restOfFacetValuesArray, fv => Number(fv.value)) + } else { + sortedValues = sortBy(restOfFacetValuesArray, fv => + fv.displayText.toLowerCase(), + ) + } + + //PORTALS-3252: provide way to sort in descending order on the client-side + const sortDescending = sortConfig && sortConfig.direction == Direction.DESC return [ - ...sortBy(restOfFacetValuesArray, fv => fv.displayText.toLowerCase()), + ...(sortDescending ? sortedValues.reverse() : sortedValues), ...valueNotSetFacetArray, ] }, [ diff --git a/packages/synapse-react-client/src/components/widgets/query-filter/FacetFilterControls.tsx b/packages/synapse-react-client/src/components/widgets/query-filter/FacetFilterControls.tsx index 284b029ee0..067f4ca601 100644 --- a/packages/synapse-react-client/src/components/widgets/query-filter/FacetFilterControls.tsx +++ b/packages/synapse-react-client/src/components/widgets/query-filter/FacetFilterControls.tsx @@ -13,7 +13,10 @@ import { QueryBundleRequest, } from '@sage-bionetworks/synapse-types' import { useQueryContext } from '../../QueryContext' -import { EnumFacetFilter } from './EnumFacetFilter/EnumFacetFilter' +import { + EnumFacetFilter, + FacetValueSortConfig, +} from './EnumFacetFilter/EnumFacetFilter' import { FacetChip } from './FacetChip' import { RangeFacetFilter } from './RangeFacetFilter' import { groupBy, noop, sortBy } from 'lodash-es' @@ -29,6 +32,7 @@ export type FacetFilterControlsProps = { /* The set of faceted column names that should be shown in the Facet controls. If undefined, all faceted columns with at least one non-null value will be shown. */ availableFacets?: string[] + facetValueSortConfigs?: FacetValueSortConfig[] } const convertFacetToFacetColumnValuesRequest = ( @@ -98,7 +102,7 @@ export function applyChangesToValuesColumn( } function FacetFilterControls(props: FacetFilterControlsProps) { - const { availableFacets } = props + const { availableFacets, facetValueSortConfigs } = props const { getCurrentQueryRequest, combineRangeFacetConfig, @@ -226,10 +230,17 @@ function FacetFilterControls(props: FacetFilterControlsProps) { )} {shownTopLevelFacets.map(facet => { const columnModel = getCorrespondingColumnForFacet(facet, columnModels!) + const sortConfig = facetValueSortConfigs?.find( + config => config.columnName == facet.columnName, + ) return (
{facet.facetType === 'enumeration' && columnModel && ( - + )} {facet.facetType === 'range' && columnModel && (