From e568a5a6e781755fb3b33c6ae95f2ed1d0bf6b47 Mon Sep 17 00:00:00 2001 From: craigrbarnes Date: Mon, 11 Mar 2024 18:01:15 -0500 Subject: [PATCH 1/3] PXP-11281: Refine Discovery Page Study Details page (#149) * cherry pick changes from chore/match_brh_style * cherry pick changes from chore/match_brh_style * cherry pick changes from chore/match_brh_style * start SinglePageStudyDetail * start refactor of FieldItems to be more general * continue refactorFieldValue * fix StudyItems so items are visible. * add paragraph support * Fix Authorization indicator * refine Authorization indicator * fix lerna version --------- Co-authored-by: craigrbarnes --- package-lock.json | 2 +- package.json | 2 +- packages/core/rollup.config.mjs | 1 + packages/core/src/types/index.ts | 10 + .../Buttons/DropdownButtons/types.ts | 2 +- .../DropdownWithIcon/DropdownWithIcon.tsx | 5 +- .../components/Protected/ProtectedContent.tsx | 6 +- .../src/components/charts/EnumFacetChart.tsx | 6 +- packages/frontend/src/components/status.tsx | 2 +- .../src/features/CohortBuilder/types.ts | 2 +- .../DataLoaders/MDSAllLocal/DataLoader.ts | 2 +- .../src/features/Discovery/Discovery.tsx | 5 +- .../features/Discovery/DiscoveryProvider.tsx | 3 + .../src/features/Discovery/DiscoveryTable.tsx | 2 +- .../features/Discovery/Search/SearchInput.tsx | 2 +- .../Statistics/StatisticsRendererFactory.tsx | 2 +- .../Statistics/SummaryStatisticPanel.tsx | 27 +- .../features/Discovery/Statistics/index.ts | 7 +- .../StudyDetails/DetailsAuthorizationIcon.tsx | 20 +- .../StudyDetails/RendererFactory.tsx | 18 +- .../SinglePageStudyDetailsPanel.tsx | 89 +++++ .../Discovery/StudyDetails/StudyDetails.tsx | 6 +- .../Discovery/StudyDetails/StudyGroup.tsx | 4 +- .../Discovery/StudyDetails/StudyItems.tsx | 348 ++++++++++++------ .../TableRenderers/CellRendererFactory.tsx | 25 +- .../TableRenderers/CellRenderers.tsx | 45 ++- .../TableRenderers/RowRendererFactory.tsx | 15 +- .../Discovery/TableRenderers/RowRenderers.tsx | 6 +- .../Discovery/TableRenderers/types.tsx | 4 +- .../frontend/src/features/Discovery/index.ts | 5 +- .../frontend/src/features/Discovery/types.ts | 47 +-- .../frontend/src/features/Discovery/utils.ts | 4 +- .../src/features/Navigation/Footer.tsx | 4 +- packages/frontend/src/features/Query/data.ts | 3 - packages/frontend/src/features/Query/index.ts | 2 +- packages/frontend/src/index.ts | 3 +- packages/frontend/src/utils/focusStyle.ts | 2 +- packages/frontend/src/utils/values.ts | 23 ++ .../sampleCommons/config/brh/discovery.json | 27 +- .../sampleCommons/config/gen3/discovery.json | 1 + packages/sampleCommons/next.config.js | 6 +- packages/sampleCommons/package.json | 2 +- .../src/lib/Discovery/CustomCellRenderers.tsx | 19 +- .../src/lib/Discovery/CustomRowRenderers.tsx | 10 +- .../sampleCommons/src/pages/SamplePage.tsx | 2 +- 45 files changed, 591 insertions(+), 237 deletions(-) create mode 100644 packages/frontend/src/features/Discovery/StudyDetails/SinglePageStudyDetailsPanel.tsx delete mode 100644 packages/frontend/src/features/Query/data.ts create mode 100644 packages/frontend/src/utils/values.ts diff --git a/package-lock.json b/package-lock.json index 759b984b..0a3d6c1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37121,7 +37121,7 @@ "next": "^14.1.0", "react": "^18.2.0", "react-dom": "18.2.0", - "tailwindcss": "^3.2.4", + "tailwindcss": "^3.4.1", "ts-jest": "^29.0.3", "ts-node": "^10.9.1", "typescript": "5.0.2" diff --git a/package.json b/package.json index 6eb4a872..234e0d28 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "jest-canvas-mock": "^2.4.0", "jest-environment-jsdom": "^29.7.0", - "lerna": "^6.6.2", + "lerna": ">=6.6.2 <7.0.0", "prettier": "^2.7.1", "rollup":"^3.29.4", "rollup-plugin-swc3": "^0.10.2", diff --git a/packages/core/rollup.config.mjs b/packages/core/rollup.config.mjs index 80f8b11d..a8e49923 100644 --- a/packages/core/rollup.config.mjs +++ b/packages/core/rollup.config.mjs @@ -45,6 +45,7 @@ const config = [ json(), swc({ // All options are optional + "sourceMaps": true, include: /\.[mc]?[jt]sx?$/, // default exclude: /node_modules/, // default tsconfig: 'tsconfig.json', // default diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index dd920b1b..60877ee2 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -28,6 +28,16 @@ export const isJSONObject = (data: any): data is JSONObject => { return typeof data === 'object' && data !== null && !Array.isArray(data); }; +export const isJSONValue = (data: any): data is JSONValue => { + return ( + typeof data === 'string' || + typeof data === 'number' || + typeof data === 'boolean' || + Array.isArray(data) && data.every(isJSONValue) || + isJSONObject(data) + ); +}; + export type HistogramDataArray = Array; export const isHistogramData = (data: any): data is HistogramData => { diff --git a/packages/frontend/src/components/Buttons/DropdownButtons/types.ts b/packages/frontend/src/components/Buttons/DropdownButtons/types.ts index 167e24f2..9f9ceb41 100644 --- a/packages/frontend/src/components/Buttons/DropdownButtons/types.ts +++ b/packages/frontend/src/components/Buttons/DropdownButtons/types.ts @@ -10,6 +10,6 @@ export interface DownloadButtonProps { actionArgs?: Record; } -export interface DropdownButtonProps extends Omit { +export interface DropdownButtonProps extends Omit { dropdownItems: ReadonlyArray>; } diff --git a/packages/frontend/src/components/DropdownWithIcon/DropdownWithIcon.tsx b/packages/frontend/src/components/DropdownWithIcon/DropdownWithIcon.tsx index 4b6ab887..0eedfa8e 100644 --- a/packages/frontend/src/components/DropdownWithIcon/DropdownWithIcon.tsx +++ b/packages/frontend/src/components/DropdownWithIcon/DropdownWithIcon.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import { Button, Menu } from '@mantine/core'; import { FloatingPosition } from '@mantine/core/lib/Floating/types'; import { ReactNode } from 'react'; @@ -128,7 +129,7 @@ export const DropdownWithIcon = ({ className="border-1 border-secondary" > {menuLabelText && ( - <> + - + )} {dropdownElements.map(({ title, onClick, icon, disabled }, idx) => ( { if (pending) return ; else return ( - +
@@ -44,11 +44,11 @@ const ProtectedContent = ({ children, referer }: ProtectedContentProps) => {
- +
); } - return <>{children}; + return {children}; }; export default ProtectedContent; diff --git a/packages/frontend/src/components/charts/EnumFacetChart.tsx b/packages/frontend/src/components/charts/EnumFacetChart.tsx index 56fbfb32..618ec977 100644 --- a/packages/frontend/src/components/charts/EnumFacetChart.tsx +++ b/packages/frontend/src/components/charts/EnumFacetChart.tsx @@ -2,7 +2,7 @@ * A BarChart for Enumerated Facets. The as the chart is wrapped from EnumFacet it does not * require the Core Data hooks. */ - +import React from 'react'; import { useEffect, useState } from 'react'; import { Box, Loader, Tooltip } from '@mantine/core'; import { useElementSize } from '@mantine/hooks'; @@ -139,12 +139,12 @@ const EnumBarChartTooltip: React.FC = ({ + {datum?.x}

{datum?.y.toLocaleString()} {unitLabel}

- +
} withArrow opened diff --git a/packages/frontend/src/components/status.tsx b/packages/frontend/src/components/status.tsx index 053e719d..89f53f04 100644 --- a/packages/frontend/src/components/status.tsx +++ b/packages/frontend/src/components/status.tsx @@ -4,7 +4,7 @@ import { useGetStatus } from '@gen3/core'; import { Code } from '@mantine/core'; const Status = () => { - const { data, error } = useGetStatus(); + const { data } = useGetStatus(); return (

Status

diff --git a/packages/frontend/src/features/CohortBuilder/types.ts b/packages/frontend/src/features/CohortBuilder/types.ts index 7e07d30f..794eded9 100644 --- a/packages/frontend/src/features/CohortBuilder/types.ts +++ b/packages/frontend/src/features/CohortBuilder/types.ts @@ -80,7 +80,7 @@ export type ActionButtonWithArgsFunction = ( ) => Promise; -export interface DownloadButtonPropsWithAction extends Omit { +export interface DownloadButtonPropsWithAction extends Omit { actionFunction: ActionButtonWithArgsFunction; actionArgs: Record; diff --git a/packages/frontend/src/features/Discovery/DataLoaders/MDSAllLocal/DataLoader.ts b/packages/frontend/src/features/Discovery/DataLoaders/MDSAllLocal/DataLoader.ts index 040d496a..91ce51d9 100644 --- a/packages/frontend/src/features/Discovery/DataLoaders/MDSAllLocal/DataLoader.ts +++ b/packages/frontend/src/features/Discovery/DataLoaders/MDSAllLocal/DataLoader.ts @@ -263,7 +263,7 @@ const useSearchMetadata = ({ storeFields: [uidField], idField: uidField, extractField: extractValue, - // processTerm: (term) => suffixes(term, 3), + // processTerm: (term) => suffixes(term, 3), searchOptions: { processTerm: MiniSearch.getDefault('processTerm'), }, diff --git a/packages/frontend/src/features/Discovery/Discovery.tsx b/packages/frontend/src/features/Discovery/Discovery.tsx index 2993d8af..ebb1d102 100644 --- a/packages/frontend/src/features/Discovery/Discovery.tsx +++ b/packages/frontend/src/features/Discovery/Discovery.tsx @@ -76,10 +76,9 @@ const Discovery = ({ {discoveryConfig?.features?.pageTitle.text} ) : null } -
+
-
-
+
{ diff --git a/packages/frontend/src/features/Discovery/DiscoveryProvider.tsx b/packages/frontend/src/features/Discovery/DiscoveryProvider.tsx index 9140a993..ed6ef161 100644 --- a/packages/frontend/src/features/Discovery/DiscoveryProvider.tsx +++ b/packages/frontend/src/features/Discovery/DiscoveryProvider.tsx @@ -8,6 +8,9 @@ interface DiscoveryProviderValue { studyDetails: JSONObject; } + + + const DiscoveryContext = createContext({ discoveryConfig: {} as DiscoveryConfig, setStudyDetails: () => null, diff --git a/packages/frontend/src/features/Discovery/DiscoveryTable.tsx b/packages/frontend/src/features/Discovery/DiscoveryTable.tsx index 19cd995a..cff74237 100644 --- a/packages/frontend/src/features/Discovery/DiscoveryTable.tsx +++ b/packages/frontend/src/features/Discovery/DiscoveryTable.tsx @@ -120,7 +120,7 @@ const DiscoveryTable = ({ textAlign: 'center', padding: theme.spacing.md, fontWeight: 'bold', - fontSize: theme.fontSizes.xl, + fontSize: theme.fontSizes.lg, textTransform: 'uppercase', }; }, diff --git a/packages/frontend/src/features/Discovery/Search/SearchInput.tsx b/packages/frontend/src/features/Discovery/Search/SearchInput.tsx index 25ad45d3..cf2a7b21 100644 --- a/packages/frontend/src/features/Discovery/Search/SearchInput.tsx +++ b/packages/frontend/src/features/Discovery/Search/SearchInput.tsx @@ -14,7 +14,7 @@ const SearchInput = ({ searchChanged, placeholder }: SearchInputProps) => { icon={} placeholder={placeholder || 'Search...'} data-testid="textbox-search-bar" - aria-label="App Search Input" + aria-label="metadata search input" value={searchTerm} onChange={(event) => { searchChanged(event.target.value); diff --git a/packages/frontend/src/features/Discovery/Statistics/StatisticsRendererFactory.tsx b/packages/frontend/src/features/Discovery/Statistics/StatisticsRendererFactory.tsx index 73d77d56..6abfeffc 100644 --- a/packages/frontend/src/features/Discovery/Statistics/StatisticsRendererFactory.tsx +++ b/packages/frontend/src/features/Discovery/Statistics/StatisticsRendererFactory.tsx @@ -16,7 +16,7 @@ const defaultStatisticRenderer = ({ {value} - + {label} diff --git a/packages/frontend/src/features/Discovery/Statistics/SummaryStatisticPanel.tsx b/packages/frontend/src/features/Discovery/Statistics/SummaryStatisticPanel.tsx index 086801db..0e3f5a17 100644 --- a/packages/frontend/src/features/Discovery/Statistics/SummaryStatisticPanel.tsx +++ b/packages/frontend/src/features/Discovery/Statistics/SummaryStatisticPanel.tsx @@ -2,9 +2,6 @@ import React from 'react'; import { SummaryStatistics } from './types'; import StatisticRendererFactory from './StatisticsRendererFactory'; -// TODO remove this once stats are working -const SAMPLE_VALUES = [1023, 2392]; - const BuildSummaryStatisticPanel = (summaries: SummaryStatistics = []) => { return summaries.map((summary) => { const { name, field, type } = summary; @@ -15,15 +12,11 @@ const BuildSummaryStatisticPanel = (summaries: SummaryStatistics = []) => { 'default', ); - return element({ + return ( element({ value: summary.value ?? 'N/A', label: name, key: `stats-item-${name}-${field}-${type}`, - className: - summaries.length > 1 - ? 'px-5 border-accent-darker first:border-r-2 last:border-right-0' - : 'px-5', - }); + })); } }); }; @@ -32,9 +25,23 @@ interface SummaryStatisticPanelProps { summaries: SummaryStatistics; } +const columns = [ + 'grid-cols-0', + 'grid-cols-1', + 'grid-cols-2', + 'grid-cols-3', + 'grid-cols-4', + 'grid-cols-5', + 'grid-cols-6', + 'grid-cols-7', + 'grid-cols-8', + 'grid-cols-9', + 'grid-cols-10', +]; + const SummaryStatisticPanel = ({ summaries }: SummaryStatisticPanelProps) => { return ( -
+
{BuildSummaryStatisticPanel(summaries)}
); diff --git a/packages/frontend/src/features/Discovery/Statistics/index.ts b/packages/frontend/src/features/Discovery/Statistics/index.ts index fcb073fe..0f6f5f9b 100644 --- a/packages/frontend/src/features/Discovery/Statistics/index.ts +++ b/packages/frontend/src/features/Discovery/Statistics/index.ts @@ -1 +1,6 @@ -export * from './types'; +import { + type SummaryStatisticsConfig, + type StatisticsDataResponse, +} from './types'; + +export { type SummaryStatisticsConfig, type StatisticsDataResponse }; diff --git a/packages/frontend/src/features/Discovery/StudyDetails/DetailsAuthorizationIcon.tsx b/packages/frontend/src/features/Discovery/StudyDetails/DetailsAuthorizationIcon.tsx index d1c07b63..daf0a3ff 100644 --- a/packages/frontend/src/features/Discovery/StudyDetails/DetailsAuthorizationIcon.tsx +++ b/packages/frontend/src/features/Discovery/StudyDetails/DetailsAuthorizationIcon.tsx @@ -1,7 +1,7 @@ import { DataAuthorization, AccessLevel } from '../types'; import { JSONObject } from '@gen3/core'; -import { Badge } from '@mantine/core'; -import { FiUnlock as UnlockOutlined } from 'react-icons/fi'; +import { Badge } from '@mantine/core'; +import { FiUnlock as UnlockOutlined, FiLock as Locked } from 'react-icons/fi'; import { accessibleFieldName } from '../types'; interface DetailsAccessProps { @@ -11,18 +11,20 @@ interface DetailsAccessProps { const DetailsAuthorizationIcon = ({ studyData, dataAccess } : DetailsAccessProps) => { + const accessStyle = "flex w-full items-center rounded-sm border-2 py-3 px-1"; + return ( -
- {dataAccess.enabled && +
+ {(dataAccess.enabled && studyData[accessibleFieldName] - && (studyData[accessibleFieldName] === AccessLevel.ACCESSIBLE ? ( - }> + && (studyData[accessibleFieldName] === AccessLevel.ACCESSIBLE) ? ( +
You have access to this data. - +
) : ( - }> +
You do not have access to this data. - +
))}
); diff --git a/packages/frontend/src/features/Discovery/StudyDetails/RendererFactory.tsx b/packages/frontend/src/features/Discovery/StudyDetails/RendererFactory.tsx index 243b93dd..c8341062 100644 --- a/packages/frontend/src/features/Discovery/StudyDetails/RendererFactory.tsx +++ b/packages/frontend/src/features/Discovery/StudyDetails/RendererFactory.tsx @@ -1,10 +1,11 @@ import React, { ReactElement } from 'react'; -import { DiscoveryResource, StudyTabTagField } from '../types'; -import { JSONArray } from '@gen3/core'; +import { JSONValue } from '@gen3/core'; -export type FieldRendererFunction = ( - _1?: P1 , - _2?: P2, + +export type FieldRendererFunction = ( + fieldValue: JSONValue, + fieldLabel?: string, + params?: Record, //TODO - define the type of params ) => JSX.Element; const defaultStudyFieldRenderer = (): ReactElement => { @@ -32,6 +33,10 @@ export class StudyFieldRendererFactory { type: string, functionName: string, ): FieldRendererFunction { + if (!(type in StudyFieldRendererFactory.getInstance().fieldRendererCatalog)) { + console.log('No field renderer found for type: ', type); + return defaultStudyFieldRenderer; + } return ( StudyFieldRendererFactory.getInstance().fieldRendererCatalog[type][ functionName @@ -70,7 +75,6 @@ export class StudyFieldRendererFactory { * Retrieve the cell renderer function for the given type and function name * @param type * @param functionName - * @param params */ export const DiscoveryDetailsRenderer = ( type = 'string', @@ -80,5 +84,5 @@ export const DiscoveryDetailsRenderer = ( return defaultStudyFieldRenderer; } const func = StudyFieldRendererFactory.getRenderer(type, functionName); - return (arg1, arg2): ReactElement => func(arg1, arg2); + return (arg1, arg2, arg3): ReactElement => func(arg1, arg2, arg3); }; diff --git a/packages/frontend/src/features/Discovery/StudyDetails/SinglePageStudyDetailsPanel.tsx b/packages/frontend/src/features/Discovery/StudyDetails/SinglePageStudyDetailsPanel.tsx new file mode 100644 index 00000000..db8d0dc1 --- /dev/null +++ b/packages/frontend/src/features/Discovery/StudyDetails/SinglePageStudyDetailsPanel.tsx @@ -0,0 +1,89 @@ +/** + * Non tabbed version of the StudyDetailsPanel + */ +import React, { ReactElement } from 'react'; +import { useDeepCompareMemo } from 'use-deep-compare'; +import { JSONObject, JSONValue } from '@gen3/core'; +import { + DataAuthorization, + StudyPageConfig, +} from '../types'; +import DetailsAuthorizationIcon from './DetailsAuthorizationIcon'; +import { JSONPath } from 'jsonpath-plus'; +import { toString } from 'lodash'; +import { createFieldRendererElement } from './StudyItems'; + + +const StudyTitle = ({ title }: { title: string }): ReactElement => { + return ( +
+

{title}

+
+ ); +}; + +interface SinglePageStudyDetailsPanelProps { + readonly data: JSONObject; + readonly studyConfig?: StudyPageConfig; + readonly authorization?: DataAuthorization; +} + +const SinglePageStudyDetailsPanel = ({ + data, + studyConfig, + authorization, +}: SinglePageStudyDetailsPanelProps): ReactElement => { + + + let headerText = ''; + if (studyConfig?.header?.field) { + const res: JSONObject = JSONPath({ + path: '$..'.concat(studyConfig.header.field), + json: data, + }); + headerText = data.length ? toString(res[0]) : ''; + } + + const elements = useDeepCompareMemo(() => { + if (!studyConfig) { + return
Study Details Panel not configured
; + } + return studyConfig?.fieldsToShow.map((field) => { + return ( +
+
+
{field.groupName}
+ {field.fields.map((field) => { + const element = createFieldRendererElement(field, data as JSONValue); + if (element !== null) { + return ( +
+ {element} +
); + } + })} +
+
+ ); + }); + }, [studyConfig, data]); + + return ( +
+ + {authorization !== undefined && authorization?.enabled ? ( + + ) : false} + {elements} +
+ ); +}; + +export default SinglePageStudyDetailsPanel; diff --git a/packages/frontend/src/features/Discovery/StudyDetails/StudyDetails.tsx b/packages/frontend/src/features/Discovery/StudyDetails/StudyDetails.tsx index efee60eb..e74fa7cd 100644 --- a/packages/frontend/src/features/Discovery/StudyDetails/StudyDetails.tsx +++ b/packages/frontend/src/features/Discovery/StudyDetails/StudyDetails.tsx @@ -4,6 +4,7 @@ import React, { useEffect } from 'react'; import { useDiscoveryContext } from '../DiscoveryProvider'; import { useDisclosure } from '@mantine/hooks'; import { MdKeyboardDoubleArrowLeft as BackIcon } from 'react-icons/md'; +import SinglePageStudyDetailsPanel from './SinglePageStudyDetailsPanel'; const StudyDetails = () => { const { discoveryConfig: config, studyDetails } = useDiscoveryContext(); @@ -47,10 +48,13 @@ const StudyDetails = () => { + {config.detailView ? + /> : config.simpleDetailsView ? + : +
Study Details Panel not configured
}
diff --git a/packages/frontend/src/features/Discovery/StudyDetails/StudyGroup.tsx b/packages/frontend/src/features/Discovery/StudyDetails/StudyGroup.tsx index 85e27512..caa91ee7 100644 --- a/packages/frontend/src/features/Discovery/StudyDetails/StudyGroup.tsx +++ b/packages/frontend/src/features/Discovery/StudyDetails/StudyGroup.tsx @@ -14,12 +14,12 @@ const StudyGroup = ({ data, header, fields }: StudyTabGroupProps) => { () => fields.some((field) => { // TDDO: handle ifFieldIsNotAvailable - if (!field.sourceField) { + if (!field.field) { return false; } const resourceFieldValue = JSONPath({ json: data, - path: field.sourceField, + path: field.field, }); return ( resourceFieldValue && diff --git a/packages/frontend/src/features/Discovery/StudyDetails/StudyItems.tsx b/packages/frontend/src/features/Discovery/StudyDetails/StudyItems.tsx index 544cde6c..866b5273 100644 --- a/packages/frontend/src/features/Discovery/StudyDetails/StudyItems.tsx +++ b/packages/frontend/src/features/Discovery/StudyDetails/StudyItems.tsx @@ -1,12 +1,13 @@ import React, { ReactElement } from 'react'; import Link from 'next/link'; +import { isArray, toString } from 'lodash'; import { JSONPath } from 'jsonpath-plus'; -import { Alert } from '@mantine/core'; +import { Alert, Text } from '@mantine/core'; import { accessibleFieldName, DiscoveryResource, AccessLevel, - StudyTabField, + StudyDetailsField, StudyTabTagField, } from '../types'; import { RenderTagsCell } from '../TableRenderers/CellRenderers'; @@ -16,27 +17,118 @@ import { FieldRendererFunctionMap, DiscoveryDetailsRenderer, } from './RendererFactory'; -import { JSONArray } from '@gen3/core'; +import { JSONValue } from '@gen3/core'; -const discoveryFieldStyle = 'flex px-0.5 justify-between whitespace-pre-wrap'; +/** + * Converts a JSON value into a React element. + * + * @param {JSONValue} value - The JSON value to convert. + * @returns {ReactElement} The React element representing the JSON value. + */ +const jsonValueToElement = (value: JSONValue): ReactElement => { + if (typeof value === 'string') { + // if JSONValue is a string, display it inside a span + return {value}; + } else if (typeof value === 'boolean') { + // if JSONValue is a boolean, display it inside a span + return {value.toString()}; + } else if (typeof value === 'number') { + // if JSONValue is a number, display it inside a span + return {value}; + } else if (Array.isArray(value)) { + // if JSONValue is an array, map each item into a list element and wrap them in a ul + return ( +
    + {value.map((v, i) => ( +
  • {jsonValueToElement(v)}
  • + ))} +
+ ); + } else if (typeof value === 'object' && value !== null) { + // if JSONValue is an object (excluding null), display each property in its own line, + // using a recursive call to display the value of each property + return ( +
    + {Object.entries(value).map(([k, v], i) => ( +
  • + {k}: {jsonValueToElement(v)} +
  • + ))} +
+ ); + } else { + // if JSONValue is null, or otherwise unhandled, return an empty fragment + return ; + } +}; + +/** + * Default style for discovery field. + */ +const discoveryFieldStyle = 'flex w-full justify-between px-0.5 no-wrap'; -const blockTextField = (_: string, text = undefined) => ( -
{text}
+/** + * Renders a block of text. + * + * @param {JSONValue} fieldValue - The value to render. + * @returns {ReactElement} The rendered block of text. + */ +const blockTextField = (fieldValue: JSONValue): ReactElement => ( +
{jsonValueToElement(fieldValue)}
); -const label = (text: string) => {text}; +/** + * Renders a label for a field. If the label text is undefined, returns an empty fragment. + * @param labelText - the label text to render. + */ +const label = (labelText?: string): ReactElement => + labelText ? ( + {labelText} + ) : ( + + ); + +/** + * Renders a text field component. + * + * @param {string} fieldValue - The value to be displayed in the text field. + * @returns {React.Element} - The rendered text field component. + */ +const textField = (fieldValue: JSONValue): ReactElement => ( + {toString(fieldValue)} +); -const textField = (text: string) => {text}; +/** + * Represents a link field component that generates a hyperlink with an optional text label. + * + * @param linkValue - The target URL for the hyperlink. + * @param linkText - The optional text label for the hyperlink. If not provided, the linkValue will be used as the label. + * @returns A JSX element containing the generated hyperlink. + */ +const linkFieldWithOptionalLabel = ( + linkValue: string, + linkText?: string, +): ReactElement => ( + + {linkText ?? linkValue} + +); -const linkField = (text: string, _ = undefined) => ( - - {text} +const linkFieldOnly = (linkValue: string, _?: string) => ( + + {linkValue} ); -const linkFieldWithTitle = (title: string, link: string) => ( - - {title} +/** + * Represents a link field component that generates a hyperlink with an optional text label. + * + * @param linkValue - The target URL for the hyperlink. + * @returns A JSX element containing the generated hyperlink. + */ +const linkField = (linkValue: string) => ( + + {linkValue} ); @@ -44,32 +136,46 @@ const subHeading = (text: string) => (

{text}

); -const labeledSingleLinkField = (labelText: string, linkText: string) => ( -
- {label(labelText)} {linkField(linkText)} -
-); +const labeledSingleLinkField = (linkValue: JSONValue, labelText?: string) => + typeof linkValue !== 'string' ? ( + + ) : ( +
+ {label(labelText)} {linkField(linkValue)} +
+ ); -const labeledMultipleLinkField = (labelText: string, linksText: string[]) => { +const labeledNumberField = (fieldValue: JSONValue, labelText?: string) => { + if (typeof fieldValue !== 'number' && typeof fieldValue !== 'string') + return ; return ( - linksText.length ? ( +
+ {label(labelText)} {fieldValue?.toLocaleString()} +
+ ); +}; + +const labeledMultipleLinkField = (value: JSONValue, labelText?: string) => { + const linksText = isArray(value) ? value : [toString(value)]; + return linksText.length ? (
{[ // labeled first field
- {label(labelText)} {linkField(linksText[0])} + {label(labelText)} {linkField(linksText[0] as string)}
, // unlabeled subsequent fields ...linksText.slice(1).map((linkText, i) => (
- {label(labelText)}{linkField(linkText)} + {label(labelText)} + {linkField(linkText as string)}
)), ]}
) : ( - )); + ); }; interface LinkWithTitle { @@ -77,44 +183,77 @@ interface LinkWithTitle { link: string; } -const unlabeledMultipleLinkField = (fieldName:string, fieldData: JSONArray ) => { +const unlabeledMultipleLinkField = ( + fieldData: JSONValue, + fieldName?: string, +) => { + if (!isArray(fieldData) || fieldData.length === 0) return ; const links = fieldData[0] as unknown as LinkWithTitle[]; return (
- { - links.map((link) => linkFieldWithTitle(link.title, link.link)) - } + {links.map((link) => labeledSingleLinkField(link.link, link.title))}
); }; -const labeledSingleTextField = (labelText: string, fieldText: string) => { +const labeledSingleTextField: FieldRendererFunction = ( + fieldValue: JSONValue, + fieldLabel?: string, +) => { + if (typeof fieldValue !== 'string') return ; + + const stringFieldValue = fieldValue as string; return (
- {label(labelText)} {textField(fieldText)} + {label(fieldLabel)} {textField(stringFieldValue)}
); }; -const labeledMultipleTextField = ( +const labeledParagraph: FieldRendererFunction = ( + fieldValue: JSONValue, + fieldLabel?: string, +) => { + if (typeof fieldValue !== 'string') return ; + + const stringFieldValue = fieldValue as string; + return ( +
+ {fieldLabel ? ( + {fieldLabel} + ) : ( + + )} + + {toString(fieldValue)} +
+ ); +}; + +const labeledMultipleTextField: FieldRendererFunction = ( + fieldsText: JSONValue, labelText?: string, - fieldsText?: string[], -): ReactElement => - fieldsText?.length ? ( +): ReactElement => { + return isArray(fieldsText) && fieldsText?.length ? (
{[ // labeled first field
- {label(labelText ?? '')} {textField(fieldsText[0])} + {label(labelText ?? '')} {textField(fieldsText[0] as string)}
, // unlabeled subsequent fields ...fieldsText.slice(1).map((text, i) => (
-
{textField(text)} +
+ {textField(text)}
)), ]} @@ -122,11 +261,16 @@ const labeledMultipleTextField = ( ) : ( ); +}; -const accessDescriptor = ( +const accessDescriptor: FieldRendererFunction = ( + resource: JSONValue, _: string | undefined, - resource: DiscoveryResource, ) => { + if (typeof resource !== 'object' || !(accessibleFieldName in resource)) { + return ; + } + if (resource[accessibleFieldName] === AccessLevel.ACCESSIBLE) { return You have access to this study.; } @@ -152,11 +296,22 @@ const formatResourceValuesWhenNestedArray = ( return resourceFieldValue; }; -const renderDetailTags = ( - fieldConfig: StudyTabTagField, - resource: DiscoveryResource, +const renderDetailTags: FieldRendererFunction = ( + fieldValue: JSONValue, + _label: string | undefined, + fieldConfig?: Record, ): ReactElement => { - if (fieldConfig.type === 'tags') { + //TODO - fix this type + const resource = fieldValue as DiscoveryResource; + + if (fieldConfig === undefined) { + return ; + } + if (fieldConfig?.categories === undefined) { + return ; + } + + if (fieldConfig.contentType === 'tags') { const tags = fieldConfig.categories ? (resource.tags || []).filter((tag) => fieldConfig.categories?.includes(tag.category), @@ -164,7 +319,7 @@ const renderDetailTags = ( : resource.tags; return ( -
+
{RenderTagsCell({ value: tags })}
); @@ -172,95 +327,74 @@ const renderDetailTags = ( return ; }; -export const createFieldRendererElementOrig = ( - field: StudyTabField | StudyTabTagField, - resource: DiscoveryResource, -) => { +export const createFieldRendererElement = ( + field: StudyDetailsField | StudyTabTagField, + resource: JSONValue, +) : ReactElement | null => { + + // determine the value of the field let resourceFieldValue = - field.sourceField && JSONPath({ json: resource, path: field.sourceField }); - if ( - resourceFieldValue && - resourceFieldValue.length > 0 && - resourceFieldValue[0].length !== 0 - ) { - resourceFieldValue = - formatResourceValuesWhenNestedArray(resourceFieldValue); - - switch (field.type) { - case 'text': { - return labeledSingleTextField(field.label, resourceFieldValue); - } - case 'link': { - return labeledSingleLinkField(field.label, resourceFieldValue); - } - case 'textList': { - return labeledMultipleTextField(field.label, resourceFieldValue); - } - case 'linkList': { - return labeledMultipleLinkField(field.label, resourceFieldValue); - } - case 'block': { - return blockTextField(field.label, resourceFieldValue); - } - } - } else { - switch (field.type) { - case 'accessDescriptor': { - return accessDescriptor(field.label, resource); - } - case 'tags': { - return renderDetailTags(field, resource); - } - } - } -}; + field.field && JSONPath({ json: resource, path: field.field }); -export const createFieldRendererElement = ( - field: StudyTabField | StudyTabTagField, - resource: DiscoveryResource, -) => { + if (!resourceFieldValue) { + if (!field.includeIfNotAvailable) return null; + if (field.valueIfNotAvailable) + resourceFieldValue = field.valueIfNotAvailable as JSONValue; + } else + resourceFieldValue = resourceFieldValue.length > 0 ? resourceFieldValue[0] : ''; + const label = + field.includeLabel === undefined || field?.includeLabel + ? field.name + : undefined; - let resourceFieldValue = - field.sourceField && JSONPath({ json: resource, path: field.sourceField }); - const studyFieldRenderer = DiscoveryDetailsRenderer(field.type, field?.renderFunction ?? 'default'); - switch (field.type) { + const studyFieldRenderer = DiscoveryDetailsRenderer( + field.contentType, + field?.renderer ?? 'default', + ); + switch (field.contentType) { case 'accessDescriptor': { - return studyFieldRenderer(field.label, resource); + return studyFieldRenderer(resource, label, field.params); } case 'tags': { - return studyFieldRenderer(field, resource); + return studyFieldRenderer(resource, label, { ...field.params, ...field }); } default: if ( resourceFieldValue && + isArray(resourceFieldValue) && resourceFieldValue.length > 0 && resourceFieldValue[0].length !== 0 && - resourceFieldValue.every( (val : any ) => typeof val === 'string') + resourceFieldValue.every((val: unknown) => typeof val === 'string') ) { resourceFieldValue = formatResourceValuesWhenNestedArray(resourceFieldValue); - return studyFieldRenderer(field.label, resourceFieldValue); + return studyFieldRenderer(resourceFieldValue, label, field.params); } else if (resourceFieldValue) - return studyFieldRenderer(field.label, resourceFieldValue); - + return studyFieldRenderer(resourceFieldValue, label, field.params); } - return ; + return null; }; const DefaultGen3StudyDetailsFieldsRenderers: Record< string, FieldRendererFunctionMap > = { - text: { default: labeledSingleTextField as FieldRendererFunction }, - link: { default: labeledSingleLinkField as FieldRendererFunction }, - textList: { default: labeledMultipleTextField as FieldRendererFunction }, - linkList: { default: labeledMultipleLinkField as FieldRendererFunction, linkWithTitle: unlabeledMultipleLinkField as FieldRendererFunction }, - block: { default: blockTextField as FieldRendererFunction }, - accessDescriptor: { default: accessDescriptor as FieldRendererFunction }, - tags: { default: renderDetailTags as FieldRendererFunction }, + text: { default: labeledSingleTextField }, + string: { default: labeledSingleTextField }, + link: { default: labeledSingleLinkField }, + textList: { default: labeledMultipleTextField }, + linkList: { + default: labeledMultipleLinkField, + linkWithTitle: unlabeledMultipleLinkField, + }, + block: { default: blockTextField }, + accessDescriptor: { default: accessDescriptor }, + tags: { default: renderDetailTags }, + number: { default: labeledNumberField }, + paragraphs: { default: labeledParagraph }, }; StudyFieldRendererFactory.registerFieldRendererCatalog( diff --git a/packages/frontend/src/features/Discovery/TableRenderers/CellRendererFactory.tsx b/packages/frontend/src/features/Discovery/TableRenderers/CellRendererFactory.tsx index b47580b4..faeb4b0e 100644 --- a/packages/frontend/src/features/Discovery/TableRenderers/CellRendererFactory.tsx +++ b/packages/frontend/src/features/Discovery/TableRenderers/CellRendererFactory.tsx @@ -1,12 +1,24 @@ import React, { ReactElement } from 'react'; import { Text } from '@mantine/core'; import { JSONObject } from '@gen3/core'; -import { CellRendererFunction, CellRenderFunctionProps } from './types'; +import { CellRendererFunction } from './types'; +import { toString } from 'lodash'; -const defaultCellRenderer = ({ - value, -}: CellRenderFunctionProps): ReactElement => { - return {value}; +const defaultCellRenderer : CellRendererFunction = ( + value, params?: JSONObject, +): ReactElement => { + if (value === undefined || value === null || toString(value) === '') { + return ( + + {`${ + params && params?.valueIfNotAvailable + ? params?.valueIfNotAvailable + : '' + }`}{' '} + + ); + } + return {toString(value)}dfdsfdsf; }; export interface CellRendererFunctionCatalogEntry { @@ -85,5 +97,8 @@ export const DiscoveryTableCellRenderer = ( return defaultCellRenderer; } const func = DiscoveryCellRendererFactory.getCellRenderer(type, functionName); + if (!func) { + return defaultCellRenderer; + } return (cellProps): ReactElement => func(cellProps, params); }; diff --git a/packages/frontend/src/features/Discovery/TableRenderers/CellRenderers.tsx b/packages/frontend/src/features/Discovery/TableRenderers/CellRenderers.tsx index f9cf68de..21ef3170 100644 --- a/packages/frontend/src/features/Discovery/TableRenderers/CellRenderers.tsx +++ b/packages/frontend/src/features/Discovery/TableRenderers/CellRenderers.tsx @@ -1,4 +1,4 @@ -import { isArray } from 'lodash'; +import { isArray, toString } from 'lodash'; import React from 'react'; import { Badge, Text } from '@mantine/core'; import Link from 'next/link'; @@ -9,6 +9,8 @@ import { CellRendererFunction, CellRenderFunctionProps } from './types'; import { DataAccessCellRenderer } from './DataAccessCellRenderers'; import { JSONObject } from '@gen3/core'; +import { getParamsValueAsString } from '../../../utils/values'; + // TODO need to type this export const RenderArrayCell: CellRendererFunction = ({ value, @@ -72,6 +74,46 @@ export const RenderLinkCell: CellRendererFunction = ({ ); }; +// given a field name, extract the value from the row using the type guards above + +export const RenderLinkWithURL: CellRendererFunction = ( + { value, cell }: CellRenderFunctionProps, + + params?: JSONObject, +) => { + const content = toString(value); + if (!content) { + return ( + {`${ + getParamsValueAsString(params, 'valueIfNotAvailable') ?? '' + }`} + ); + } + const rowData = getParamsValueAsString(cell?.row?.original, toString(params?.['hrefValueFromField'])); + if (rowData) { + return ( + ev.stopPropagation()} + target="_blank" + rel="noreferrer" + > + {content} + + ); + } + return ( + ev.stopPropagation()} + target="_blank" + rel="noreferrer" + > + {content} + + ); +}; + const RenderStringCell: CellRendererFunction = ( { value }: CellRenderFunctionProps, params?: JSONObject, @@ -204,6 +246,7 @@ export const Gen3DiscoveryStandardCellRenderers = { }, link: { default: RenderLinkCell, + withURL: RenderLinkWithURL, }, dataAccess: { default: DataAccessCellRenderer, diff --git a/packages/frontend/src/features/Discovery/TableRenderers/RowRendererFactory.tsx b/packages/frontend/src/features/Discovery/TableRenderers/RowRendererFactory.tsx index 27323c74..7866d542 100644 --- a/packages/frontend/src/features/Discovery/TableRenderers/RowRendererFactory.tsx +++ b/packages/frontend/src/features/Discovery/TableRenderers/RowRendererFactory.tsx @@ -4,12 +4,12 @@ import { defaultRowRenderer, Gen3DiscoveryStandardRowPreviewRenderers, } from './RowRenderers'; -import { StudyPreviewField } from '../types'; +import { StudyDetailsField } from '../types'; // TODO Tighten up the typing here export type RowRendererFunction = ( props: RowRenderFunctionParams, - studyPreviewConfig?: StudyPreviewField, + studyPreviewConfig?: StudyDetailsField, ) => ReactElement; export interface RowRendererFunctionCatalogEntry { @@ -36,6 +36,11 @@ export class DiscoveryRowRendererFactory { type: string, functionName: string, ): RowRendererFunction { + if (!(type in DiscoveryRowRendererFactory.getInstance().RowRendererCatalog)) { + console.log('No row renderer found for type: ', type); + return defaultRowRenderer; + } + return ( DiscoveryRowRendererFactory.getInstance().RowRendererCatalog[type][ functionName @@ -75,17 +80,17 @@ export class DiscoveryRowRendererFactory { * @param studyPreviewConfig */ export const DiscoveryTableRowRenderer = ( - studyPreviewConfig?: StudyPreviewField, + studyPreviewConfig?: StudyDetailsField, ): RowRendererFunction => { if (!studyPreviewConfig?.contentType) { return (row): ReactElement => defaultRowRenderer(row, studyPreviewConfig); } const func = DiscoveryRowRendererFactory.getRowRenderer( studyPreviewConfig?.contentType, - studyPreviewConfig?.detailRenderer ?? 'default', + studyPreviewConfig?.renderer ?? 'default', ); if (!func) { - throw new Error(`No row renderer found for given config (contentType: ${ studyPreviewConfig?.contentType }, detailRenderer: ${ studyPreviewConfig?.detailRenderer ?? 'default'}`); + throw new Error(`No row renderer found for given config (contentType: ${ studyPreviewConfig?.contentType }, detailRenderer: ${ studyPreviewConfig?.renderer ?? 'default'}`); } return (row): ReactElement => func(row, studyPreviewConfig); }; diff --git a/packages/frontend/src/features/Discovery/TableRenderers/RowRenderers.tsx b/packages/frontend/src/features/Discovery/TableRenderers/RowRenderers.tsx index 1f902286..fdb99c26 100644 --- a/packages/frontend/src/features/Discovery/TableRenderers/RowRenderers.tsx +++ b/packages/frontend/src/features/Discovery/TableRenderers/RowRenderers.tsx @@ -1,5 +1,5 @@ import { MRT_Row } from 'mantine-react-table'; -import { StudyPreviewField } from '../types'; +import { StudyDetailsField } from '../types'; import React, { ReactElement } from 'react'; import { Box, Text } from '@mantine/core'; import { JSONPath } from 'jsonpath-plus'; @@ -9,12 +9,12 @@ export interface RowRenderFunctionParams = Rec } export interface RowRendererRegisteredFunctionParams extends RowRenderFunctionParams { - studyPreviewConfig?: StudyPreviewField; + studyPreviewConfig?: StudyDetailsField; } const StringRowRenderer = ( { row }: RowRenderFunctionParams, - studyPreviewConfig?: StudyPreviewField, + studyPreviewConfig?: StudyDetailsField, ): ReactElement => { if (!studyPreviewConfig) { return ; diff --git a/packages/frontend/src/features/Discovery/TableRenderers/types.tsx b/packages/frontend/src/features/Discovery/TableRenderers/types.tsx index 60a0d016..f1defebc 100644 --- a/packages/frontend/src/features/Discovery/TableRenderers/types.tsx +++ b/packages/frontend/src/features/Discovery/TableRenderers/types.tsx @@ -14,7 +14,7 @@ export interface CellRenderFunctionProps { * is available from the DiscoveryContext. * @param props: value and optional cell object */ -export type CellRendererFunction = ( +export type CellRendererFunction = ( props: CellRenderFunctionProps, - params?: JSONObject, + params?: T, ) => ReactElement; diff --git a/packages/frontend/src/features/Discovery/index.ts b/packages/frontend/src/features/Discovery/index.ts index 81c7163c..c48a4aaf 100644 --- a/packages/frontend/src/features/Discovery/index.ts +++ b/packages/frontend/src/features/Discovery/index.ts @@ -11,16 +11,15 @@ import { import DiscoveryConfigProvider, { useDiscoveryContext, } from './DiscoveryProvider'; -import { type DiscoveryConfig, type StudyPreviewField } from './types'; +import { type DiscoveryConfig, type StudyDetailsField } from './types'; import StudyGroup from './StudyDetails/StudyGroup'; import { getTagColor } from './utils'; import { registerDefaultDiscoveryDataLoaders } from './DataLoaders/registeredDataLoaders'; -export * from './Statistics'; export { type CellRenderFunctionProps, type DiscoveryConfig, - type StudyPreviewField, + type StudyDetailsField, type RowRenderFunctionParams, Discovery, TagCloud, diff --git a/packages/frontend/src/features/Discovery/types.ts b/packages/frontend/src/features/Discovery/types.ts index 737ae6b6..8ae844af 100644 --- a/packages/frontend/src/features/Discovery/types.ts +++ b/packages/frontend/src/features/Discovery/types.ts @@ -1,5 +1,5 @@ import { - JSONArray, + JSONValue, JSONObject, type MetadataPaginationParams, } from '@gen3/core'; @@ -118,30 +118,22 @@ export interface MinimalFieldMapping { uid: string; } -export interface StudyPreviewField { +export interface StudyDetailsField { name: string; field: string; - contentType?: DiscoveryContentTypes; - includeName: boolean; - includeIfNotAvailable: boolean; + contentType?: string; + includeLabel?: boolean; + includeIfNotAvailable?: boolean; valueIfNotAvailable?: string | number; - detailRenderer?: string; + renderer?: string; params?: Record; } -export interface StudyTabField { - type: string; - sourceField: string; - label: string; - default?: string; - renderFunction?: string; -} - export interface StudyPageGroup { groupName?: string; groupWidth?: 'half' | 'full'; includeName?: boolean; - fields: StudyPreviewField[]; + fields: StudyDetailsField[]; } export interface DataDownloadLinks { @@ -155,11 +147,8 @@ export interface DownloadLinkFields { descriptionField: string; } -/** Legacy config interface fro StudyPage view - * Plan to deprecate this interface in the future - * Please use StudyDetailView instead - */ export interface StudyPageConfig { + showAllAvailableFields?: boolean, header?: { field: string; }; @@ -168,13 +157,13 @@ export interface StudyPageConfig { fieldsToShow: Array; // render multiple groups of fields } -export interface StudyTabTagField extends StudyTabField { +export interface StudyTabTagField extends StudyDetailsField { categories?: string[]; } export interface StudyTabGroup { header: string; - fields: Array; + fields: Array; } export interface StudyDetailTab { @@ -247,9 +236,9 @@ export interface ExportToDataLibrary { } export interface DataAuthorization { - columnTooltip: string; - supportedValues: Record; - enabled: boolean; + columnTooltip?: string; + supportedValues?: Record; + enabled?: boolean; } export interface AccessFilters { @@ -263,15 +252,15 @@ export interface DiscoveryConfig { pageTitle: DiscoveryPageTitle; exportToDataLibrary?: ExportToDataLibrary; search?: SearchConfig; - authorization: Partial; + authorization: DataAuthorization; dataFetchFunction?: string; }; aggregations: SummaryStatisticsConfig[]; tagCategories: TagCategory[]; tableConfig: DiscoveryTableConfig; studyColumns: StudyColumn[]; - studyPreviewField?: StudyPreviewField; - studyPageConfig?: StudyPageConfig; + studyPreviewField?: StudyDetailsField; + simpleDetailsView?: StudyPageConfig; detailView: StudyDetailView; minimalFieldMapping: MinimalFieldMapping; } @@ -295,8 +284,8 @@ export enum AccessLevel { export interface DiscoveryResource extends Record< string, - JSONObject | JSONArray | AccessLevel | TagInfo[] | undefined + JSONValue | AccessLevel | TagInfo[] | undefined > { - [accessibleFieldName]: AccessLevel; + [accessibleFieldName]?: AccessLevel; tags?: Array; } diff --git a/packages/frontend/src/features/Discovery/utils.ts b/packages/frontend/src/features/Discovery/utils.ts index e38ebe3b..dbcba5e7 100644 --- a/packages/frontend/src/features/Discovery/utils.ts +++ b/packages/frontend/src/features/Discovery/utils.ts @@ -1,6 +1,6 @@ import { JSONPath } from 'jsonpath-plus'; -import { JSONObject } from '@gen3/core'; -import { TagCategory } from './types'; +import { JSONObject, JSONValue } from '@gen3/core'; +import { DiscoveryResource, TagCategory } from './types'; export const jsonPathAccessor = (path: string) => (row: JSONObject) => { // TODO: add logging if path is not found diff --git a/packages/frontend/src/features/Navigation/Footer.tsx b/packages/frontend/src/features/Navigation/Footer.tsx index 07b68f9c..4b767a40 100644 --- a/packages/frontend/src/features/Navigation/Footer.tsx +++ b/packages/frontend/src/features/Navigation/Footer.tsx @@ -19,7 +19,7 @@ const Footer = ({ const mergedClassNames = { ...defaultClassNames, ...classNames }; return ( - +
{(footerLogos || [[]]).map((col, index) => { @@ -113,7 +113,7 @@ const Footer = ({ ))}
) : null} - +
); }; diff --git a/packages/frontend/src/features/Query/data.ts b/packages/frontend/src/features/Query/data.ts deleted file mode 100644 index 8bb2974f..00000000 --- a/packages/frontend/src/features/Query/data.ts +++ /dev/null @@ -1,3 +0,0 @@ -// import { Fetcher } from "@graphiql/toolkit"; -// import { GEN3_API } from "@gen3/core"; -// import { GqlQueryEndpointProps } from "./types"; diff --git a/packages/frontend/src/features/Query/index.ts b/packages/frontend/src/features/Query/index.ts index 4223a318..1c975df7 100644 --- a/packages/frontend/src/features/Query/index.ts +++ b/packages/frontend/src/features/Query/index.ts @@ -1,3 +1,3 @@ import GqlQueryEditor from './GqlQueryEditor'; -export default GqlQueryEditor; +export { GqlQueryEditor }; diff --git a/packages/frontend/src/index.ts b/packages/frontend/src/index.ts index eeaf68c2..4809728d 100644 --- a/packages/frontend/src/index.ts +++ b/packages/frontend/src/index.ts @@ -4,10 +4,11 @@ export * from './features/Discovery'; export * from './components/Profile'; export * from './components/Login'; export * from './features/CohortBuilder'; +export * from './features/Query'; export * from './utils/'; import { getNavPageLayoutPropsFromConfig } from './lib/common/staticProps'; -import "@gen3/core"; +import '@gen3/core'; // export Gen3 data UI standard pages import Gen3Provider from './components/Providers/Gen3Provider'; diff --git a/packages/frontend/src/utils/focusStyle.ts b/packages/frontend/src/utils/focusStyle.ts index edc2c846..59fa0bd1 100644 --- a/packages/frontend/src/utils/focusStyle.ts +++ b/packages/frontend/src/utils/focusStyle.ts @@ -1,2 +1,2 @@ export const focusStyles = - "focus-visible:outline-none focus-visible:ring-offset-2 focus:ring-offset-white rounded-md focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-focusColor"; + 'focus-visible:outline-none focus-visible:ring-offset-2 focus:ring-offset-white rounded-md focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-focusColor'; diff --git a/packages/frontend/src/utils/values.ts b/packages/frontend/src/utils/values.ts new file mode 100644 index 00000000..fa157b64 --- /dev/null +++ b/packages/frontend/src/utils/values.ts @@ -0,0 +1,23 @@ +import { JSONObject } from '@gen3/core'; +import { toString } from 'lodash'; + +export const isPropertyKey = (value: unknown): value is PropertyKey => { + return ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'symbol' + ); +}; +export const hasOwnProperty = ( + obj?: X, + prop?: Y, +): obj is X & Record => { + if (obj === undefined) return false; + if (prop === undefined) return false; + return Object.hasOwnProperty.call(obj, prop); +}; +export const getParamsValueAsString = (params?: JSONObject, key?: string) => { + return params && key && key !== '' && isPropertyKey(key) && hasOwnProperty(params, key) + ? toString(params[key]) + : undefined; +}; diff --git a/packages/sampleCommons/config/brh/discovery.json b/packages/sampleCommons/config/brh/discovery.json index 4eee36b2..76f98458 100644 --- a/packages/sampleCommons/config/brh/discovery.json +++ b/packages/sampleCommons/config/brh/discovery.json @@ -39,7 +39,13 @@ }, "search": { "searchBar": { - "enabled": true + "enabled": true, + "searchableTextFields": [ + "short_name", + "full_name", + "study_description", + "study_id" + ] }, "tagSearchDropdown": { "enabled": true, @@ -132,8 +138,11 @@ "field": "commons", "errorIfNotAvailable": false, "valueIfNotAvailable": "n/a", - "hrefValueFromField": "commons_url", - "contentType": "string" + "contentType": "link", + "cellRenderFunction": "withURL", + "params": { + "hrefValueFromField": "commons_url" + } }, { "name": "DATA ACCESS METHOD", @@ -151,7 +160,7 @@ "includeIfNotAvailable": true, "valueIfNotAvailable": "No description has been provided for this study." }, - "studyPageFields": { + "simpleDetailsView": { "showAllAvailableFields": false, "header": { "field": "name" @@ -175,7 +184,7 @@ { "name": "Short Name", "field": "short_name", - "contentType": "string", + "contentType": "text", "includeName": true, "includeIfNotAvailable": true, "valueIfNotAvailable": "N/A" @@ -183,26 +192,26 @@ { "name": "dbGaP Accession Number", "field": "dbgap_accession", - "contentType": "string", + "contentType": "text", "includeName": true, "includeIfNotAvailable": false }, { "name": "Project ID", "field": "project_id", - "contentType": "string", + "contentType": "text", "includeIfNotAvailable": false }, { "name": "Data Commons", "field": "commons", - "contentType": "string", + "contentType": "text", "includeIfNotAvailable": false }, { "name": "Tutorial Notebook", "field": "tutorial_notebook", - "contentType": "string", + "contentType": "text", "includeIfNotAvailable": true, "valueIfNotAvailable": "NO" } diff --git a/packages/sampleCommons/config/gen3/discovery.json b/packages/sampleCommons/config/gen3/discovery.json index e49aaf38..3f30a019 100644 --- a/packages/sampleCommons/config/gen3/discovery.json +++ b/packages/sampleCommons/config/gen3/discovery.json @@ -26,6 +26,7 @@ } ] }, + "aiSearch" : true, "tagsColumn": { "enabled": false }, diff --git a/packages/sampleCommons/next.config.js b/packages/sampleCommons/next.config.js index 59c105a0..61c3000c 100644 --- a/packages/sampleCommons/next.config.js +++ b/packages/sampleCommons/next.config.js @@ -37,11 +37,11 @@ const nextConfig = { async headers() { return [ { - source: "/(.*)?", // Matches all pages + source: '/(.*)?', // Matches all pages headers: [ { - key: "X-Frame-Options", - value: "SAMEORIGIN", + key: 'X-Frame-Options', + value: 'SAMEORIGIN', }, ], }, diff --git a/packages/sampleCommons/package.json b/packages/sampleCommons/package.json index 5b1a389e..a225159e 100644 --- a/packages/sampleCommons/package.json +++ b/packages/sampleCommons/package.json @@ -31,7 +31,7 @@ "next": "^14.1.0", "react": "^18.2.0", "react-dom": "18.2.0", - "tailwindcss": "^3.2.4", + "tailwindcss": "^3.4.1", "ts-jest": "^29.0.3", "ts-node": "^10.9.1", "typescript": "5.0.2" diff --git a/packages/sampleCommons/src/lib/Discovery/CustomCellRenderers.tsx b/packages/sampleCommons/src/lib/Discovery/CustomCellRenderers.tsx index 8b4dad80..f0e0356d 100644 --- a/packages/sampleCommons/src/lib/Discovery/CustomCellRenderers.tsx +++ b/packages/sampleCommons/src/lib/Discovery/CustomCellRenderers.tsx @@ -2,13 +2,15 @@ import { DiscoveryCellRendererFactory, CellRenderFunctionProps, } from '@gen3/frontend'; -import { Badge } from '@mantine/core'; +import { Badge, Text } from '@mantine/core'; import React from 'react'; import { MdOutlineCheckCircle as CheckCircleOutlined, MdOutlineRemoveCircleOutline as MinusCircleOutlined, } from 'react-icons/md'; import { isArray } from 'lodash'; +import { JSONObject } from '@gen3/core'; +import { toString } from 'lodash'; /** * Custom cell renderer for the linked study column for HEAL @@ -36,8 +38,21 @@ export const LinkedStudyCell = ({ const WrappedStringCell = ( { value }: CellRenderFunctionProps, // eslint-disable-next-line @typescript-eslint/no-unused-vars - _params: any[0], + params?: JSONObject, ) => { + + if (value === undefined || value === null || toString(value) === '') { + return ( + + {`${ + params && params?.valueIfNotAvailable + ? params?.valueIfNotAvailable + : '' + }`}{' '} + + ); + } + const content = value as string | string[]; return (
diff --git a/packages/sampleCommons/src/lib/Discovery/CustomRowRenderers.tsx b/packages/sampleCommons/src/lib/Discovery/CustomRowRenderers.tsx index be8b2622..722fcb32 100644 --- a/packages/sampleCommons/src/lib/Discovery/CustomRowRenderers.tsx +++ b/packages/sampleCommons/src/lib/Discovery/CustomRowRenderers.tsx @@ -3,7 +3,7 @@ import React, { ReactElement } from 'react'; import { JSONPath } from 'jsonpath-plus'; import { Badge, Box, Text } from '@mantine/core'; import { - StudyPreviewField, + StudyDetailsField, RowRenderFunctionParams, DiscoveryRowRendererFactory, useDiscoveryContext, @@ -19,7 +19,7 @@ interface TagData { const DetailsWithTagsRowRenderer = ( { row } : RowRenderFunctionParams, - studyPreviewConfig?: StudyPreviewField, + studyPreviewConfig?: StudyDetailsField, ): ReactElement => { const { discoveryConfig: config, setStudyDetails } = useDiscoveryContext(); @@ -55,24 +55,22 @@ const DetailsWithTagsRowRenderer = {row.original?.tags.map(({ name, category }: TagData) => { const color = getTagColor(category, config.tagCategories); return ( - {name} - ); })}
diff --git a/packages/sampleCommons/src/pages/SamplePage.tsx b/packages/sampleCommons/src/pages/SamplePage.tsx index 11243645..7fe48bb8 100644 --- a/packages/sampleCommons/src/pages/SamplePage.tsx +++ b/packages/sampleCommons/src/pages/SamplePage.tsx @@ -26,7 +26,7 @@ const SamplePage = ({ headerProps, footerProps }: NavPageLayoutProps) => { export const getServerSideProps: GetServerSideProps< NavPageLayoutProps -> = async () => { +> = async (_context) => { return { props: { ...(await getNavPageLayoutPropsFromConfig()), From 123beb81e65cca1865d49561aa622d3e6cec1ca0 Mon Sep 17 00:00:00 2001 From: craigrbarnes Date: Tue, 12 Mar 2024 11:38:52 -0500 Subject: [PATCH 2/3] resolve NextJS warnings. (#150) Co-authored-by: craigrbarnes --- .../frontend/src/components/Content/LandingPageContent.tsx | 3 +-- packages/frontend/src/features/Navigation/Footer.tsx | 3 +-- packages/frontend/src/features/Navigation/NavigationLogo.tsx | 1 - packages/frontend/src/pages/Landing/about-us.tsx | 3 +-- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/frontend/src/components/Content/LandingPageContent.tsx b/packages/frontend/src/components/Content/LandingPageContent.tsx index 3407b345..9f3d5728 100644 --- a/packages/frontend/src/components/Content/LandingPageContent.tsx +++ b/packages/frontend/src/components/Content/LandingPageContent.tsx @@ -111,8 +111,7 @@ const LandingPageContent = ({ content }: LandingPageContentProp) => { {obj.image.alt}
); diff --git a/packages/frontend/src/features/Navigation/Footer.tsx b/packages/frontend/src/features/Navigation/Footer.tsx index 4b767a40..36ee27fb 100644 --- a/packages/frontend/src/features/Navigation/Footer.tsx +++ b/packages/frontend/src/features/Navigation/Footer.tsx @@ -32,9 +32,9 @@ const Footer = ({ > {col.map((logo) => ( {logo.description} {title && ( diff --git a/packages/frontend/src/pages/Landing/about-us.tsx b/packages/frontend/src/pages/Landing/about-us.tsx index e4baf16a..abb70e60 100644 --- a/packages/frontend/src/pages/Landing/about-us.tsx +++ b/packages/frontend/src/pages/Landing/about-us.tsx @@ -84,8 +84,7 @@ const AboutUsPage = ({ footerProps, headerProps }: NavPageLayoutProps) => { Gen3 Logo
From 6734d399cee10818bdc0b6c2a1fae08a38f3aca2 Mon Sep 17 00:00:00 2001 From: craigrbarnes Date: Tue, 12 Mar 2024 12:05:39 -0500 Subject: [PATCH 3/3] v0.10.14 (#151) Co-authored-by: craigrbarnes --- lerna.json | 2 +- package-lock.json | 10 +++++----- packages/core/package.json | 2 +- packages/frontend/package.json | 2 +- packages/sampleCommons/package.json | 2 +- packages/tools/package.json | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lerna.json b/lerna.json index fb258886..7226c009 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "packages": ["packages/*"], "useWorkspaces": true, - "version": "0.10.13" + "version": "0.10.14" } diff --git a/package-lock.json b/package-lock.json index 0a3d6c1b..66a943ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,7 +40,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "jest-canvas-mock": "^2.4.0", "jest-environment-jsdom": "^29.7.0", - "lerna": "^6.6.2", + "lerna": ">=6.6.2 <7.0.0", "prettier": "^2.7.1", "rollup": "^3.29.4", "rollup-plugin-swc3": "^0.10.2", @@ -36895,7 +36895,7 @@ }, "packages/core": { "name": "@gen3/core", - "version": "0.10.13", + "version": "0.10.14", "license": "Apache-2.0", "dependencies": { "@reduxjs/toolkit": "1.9.5", @@ -36983,7 +36983,7 @@ }, "packages/frontend": { "name": "@gen3/frontend", - "version": "0.10.13", + "version": "0.10.14", "license": "Apache-2.0", "dependencies": { "@fontsource/montserrat": "^4.5.12", @@ -37103,7 +37103,7 @@ }, "packages/sampleCommons": { "name": "@gen3/samplecommons", - "version": "0.10.13", + "version": "0.10.14", "dependencies": { "@gen3/core": "file:../core", "@gen3/frontend": "file:../frontend", @@ -38266,7 +38266,7 @@ }, "packages/tools": { "name": "@gen3/toolsff", - "version": "0.10.13", + "version": "0.10.14", "license": "Apache-2.0", "dependencies": { "@iconify/tools": "^2.1.2", diff --git a/packages/core/package.json b/packages/core/package.json index 2cd5a55c..3d28b041 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@gen3/core", - "version": "0.10.13", + "version": "0.10.14", "author": "CTDS", "description": "Core module for gen3 frontend. Provides an interface for interacting with the gen3 API and a redux store for managing state.", "license": "Apache-2.0", diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 25a6a5df..eda2819a 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -1,6 +1,6 @@ { "name": "@gen3/frontend", - "version": "0.10.13", + "version": "0.10.14", "description": "Gen3 frontend components, content management, and pages", "keywords": [], "author": "Center for Translational Data Science", diff --git a/packages/sampleCommons/package.json b/packages/sampleCommons/package.json index a225159e..3c82be7d 100644 --- a/packages/sampleCommons/package.json +++ b/packages/sampleCommons/package.json @@ -1,6 +1,6 @@ { "name": "@gen3/samplecommons", - "version": "0.10.13", + "version": "0.10.14", "private": true, "scripts": { "lint": "next lint", diff --git a/packages/tools/package.json b/packages/tools/package.json index ad0ab84d..61082b9e 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -1,6 +1,6 @@ { "name": "@gen3/toolsff", - "version": "0.10.13", + "version": "0.10.14", "description": "tools for processing portal content", "main": "index.js", "type": "module",