From 0bd958f310c7819c41d0b9d3ba41bf535e108bd0 Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 1 Nov 2024 12:30:18 +0000 Subject: [PATCH 01/10] change from key to props.id - didn't fix it --- .../libs/components/src/map/animation_functions/geohash.tsx | 4 ++-- .../components/src/map/animation_functions/updateMarkers.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/libs/components/src/map/animation_functions/geohash.tsx b/packages/libs/components/src/map/animation_functions/geohash.tsx index d0b0ed10d0..12ea0afaa3 100644 --- a/packages/libs/components/src/map/animation_functions/geohash.tsx +++ b/packages/libs/components/src/map/animation_functions/geohash.tsx @@ -11,8 +11,8 @@ export default function geohashAnimation({ prevMarkers, markers, }: geoHashAnimation) { - const prevGeoHash = prevMarkers[0].key as string; - const currentGeohash = markers[0].key as string; + const prevGeoHash = prevMarkers[0].props.id as string; + const currentGeohash = markers[0].props.id as string; let zoomType, consolidatedMarkers; /** Zoom Out - Move existing markers to new position diff --git a/packages/libs/components/src/map/animation_functions/updateMarkers.tsx b/packages/libs/components/src/map/animation_functions/updateMarkers.tsx index 1673f1881f..e4e1f30e15 100644 --- a/packages/libs/components/src/map/animation_functions/updateMarkers.tsx +++ b/packages/libs/components/src/map/animation_functions/updateMarkers.tsx @@ -8,12 +8,12 @@ export default function updateMarkers( ) { return toChangeMarkers.map((markerObj) => { // Calculate the matching geohash - const sourceKey = markerObj.key as string; + const sourceKey = markerObj.props.id as string; const sourceHash = sourceKey.slice(0, -hashDif); // Find the object with the matching geohash const matchingMarkers = sourceMarkers.filter((obj) => { - return obj.key === sourceHash; + return obj.props.id === sourceHash; }); // Clone marker element with new position From 66f116030e47a4e9a1f1a34288fe1fa5f9dfe345 Mon Sep 17 00:00:00 2001 From: Dave Falke Date: Fri, 1 Nov 2024 10:48:04 -0400 Subject: [PATCH 02/10] Upload forms for rnaseq and bigwig user datasets (#1255) * adding rnaseq and bw * Set type="button" * Add padding and fixed dimensions * Remove padding * Add hook to use WdkDependencies context value * Move upload-config to web-common and add new property for dependencies * 10GB limit for rnaseq and bigwigfiles * Move dependencies before spreading form data to prevent overwriting. * Use correct format for reference genome dependency * Update styling and add display and description properties to upload-config * Refresh styling * Validate dependencies are selected, if required. --------- Co-authored-by: aurreco-uga --- .../buttons/PopoverButton/PopoverButton.tsx | 1 + .../inputs/SelectTree/SelectTree.tsx | 6 +- .../variableSelectors/VariableList.tsx | 3 - .../src/lib/Components/UploadForm.scss | 43 ++- .../src/lib/Components/UploadForm.tsx | 49 ++- .../src/lib/Components/UploadFormMenu.scss | 37 ++ .../src/lib/Components/UploadFormMenu.tsx | 40 +++ .../lib/Components/UserDatasetsWorkspace.tsx | 20 +- .../UserDatasetNewUploadController.tsx | 42 ++- .../src/lib/Controllers/UserDatasetRouter.tsx | 2 +- .../libs/user-datasets/src/lib/Service/api.ts | 2 +- .../libs/user-datasets/src/lib/Utils/types.ts | 15 +- .../src/lib/Utils/upload-config.tsx | 106 ------ .../src/Hooks/WdkDependenciesEffect.ts | 12 + packages/libs/web-common/package.json | 8 + .../src/user-dataset-upload-config.tsx | 332 ++++++++++++++++++ .../js/client/routes/userDatasetRoutes.tsx | 2 +- .../js/client/userDatasetRoutes.tsx | 4 +- .../js/client/routes/userDatasetRoutes.tsx | 2 +- 19 files changed, 567 insertions(+), 159 deletions(-) create mode 100644 packages/libs/user-datasets/src/lib/Components/UploadFormMenu.scss create mode 100644 packages/libs/user-datasets/src/lib/Components/UploadFormMenu.tsx create mode 100644 packages/libs/web-common/src/user-dataset-upload-config.tsx diff --git a/packages/libs/coreui/src/components/buttons/PopoverButton/PopoverButton.tsx b/packages/libs/coreui/src/components/buttons/PopoverButton/PopoverButton.tsx index 3dfe53f36c..59a1fd2a3f 100644 --- a/packages/libs/coreui/src/components/buttons/PopoverButton/PopoverButton.tsx +++ b/packages/libs/coreui/src/components/buttons/PopoverButton/PopoverButton.tsx @@ -150,6 +150,7 @@ export default function PopoverButton(props: PopoverButtonProps) { additionalAriaProperties={{ 'aria-controls': 'dropdown', 'aria-haspopup': 'true', + type: 'button', }} /> ); diff --git a/packages/libs/coreui/src/components/inputs/SelectTree/SelectTree.tsx b/packages/libs/coreui/src/components/inputs/SelectTree/SelectTree.tsx index 19cc9c7d6e..5e94400a2b 100644 --- a/packages/libs/coreui/src/components/inputs/SelectTree/SelectTree.tsx +++ b/packages/libs/coreui/src/components/inputs/SelectTree/SelectTree.tsx @@ -84,7 +84,11 @@ function SelectTree(props: SelectTreeProps) { onClose={onClose} isDisabled={props.isDisabled} > - {wrapPopover ? wrapPopover(checkboxTree) : checkboxTree} +
+ {wrapPopover ? wrapPopover(checkboxTree) : checkboxTree} +
); } diff --git a/packages/libs/eda/src/lib/core/components/variableSelectors/VariableList.tsx b/packages/libs/eda/src/lib/core/components/variableSelectors/VariableList.tsx index df4a6d2910..85c54c262f 100644 --- a/packages/libs/eda/src/lib/core/components/variableSelectors/VariableList.tsx +++ b/packages/libs/eda/src/lib/core/components/variableSelectors/VariableList.tsx @@ -870,9 +870,6 @@ export default function VariableList({ style={{ position: 'relative', borderRadius: '0.25em', - padding: '0.5em 0.5em 0.5em 0', - height: '60vh', - width: '30em', overflow: 'hidden', display: 'flex', flexDirection: 'column', diff --git a/packages/libs/user-datasets/src/lib/Components/UploadForm.scss b/packages/libs/user-datasets/src/lib/Components/UploadForm.scss index cfdd8f1b2c..93c6d2a9a5 100644 --- a/packages/libs/user-datasets/src/lib/Components/UploadForm.scss +++ b/packages/libs/user-datasets/src/lib/Components/UploadForm.scss @@ -1,4 +1,11 @@ .UploadForm { + max-width: 1000px; + padding: 1em 3em; + + > * + * { + margin-block-start: 1em; + } + h2 { color: #222; font-size: 1.5em; @@ -6,30 +13,27 @@ margin: 0; padding: 22px 0 8px 0; } - .formSection > label { - font-size: medium; - } - #data-set-name { - min-width: 300px; - } - #data-set-summary { - width: 100%; - } - #data-set-description { - width: 100%; - height: 8em; - } - #data-set-url { - width: 100%; - max-width: 51em; + .formSection { + > & { + margin-block-start: 1em; + } + + display: flex; + flex-direction: column; + gap: 0.5em; + + > label { + font-size: medium; + } + + #data-set-name { + width: 300px; + } } .formInfo { width: 80%; text-align: justify; } - .formSection { - margin: 1em 0; - } select { max-width: 450px; white-space: nowrap; @@ -43,6 +47,7 @@ li > label { display: grid; grid-template-columns: auto 10em 1fr; + align-items: baseline; label { font-size: medium; diff --git a/packages/libs/user-datasets/src/lib/Components/UploadForm.tsx b/packages/libs/user-datasets/src/lib/Components/UploadForm.tsx index 5be92d60cc..396702394f 100644 --- a/packages/libs/user-datasets/src/lib/Components/UploadForm.tsx +++ b/packages/libs/user-datasets/src/lib/Components/UploadForm.tsx @@ -29,6 +29,7 @@ import { DatasetUploadTypeConfigEntry, NewUserDataset, ResultUploadConfig, + UserDataset, } from '../Utils/types'; import { Modal } from '@veupathdb/coreui'; @@ -72,6 +73,7 @@ interface FormContent { summary: string; description: string; dataUploadSelection: DataUploadSelection; + dependencies?: UserDataset['dependencies']; } export type FormValidation = InvalidForm | ValidForm; @@ -132,6 +134,9 @@ function UploadForm({ urlParams.datasetDescription ?? '' ); + const [dependencies, setDependencies] = + useState(); + const [dataUploadMode, setDataUploadMode] = useState( urlParams.datasetStepId ? 'step' @@ -208,6 +213,7 @@ function UploadForm({ summary, description, dataUploadSelection, + dependencies, } ); @@ -226,6 +232,7 @@ function UploadForm({ name, summary, description, + dependencies, dataUploadSelection, submitForm, ] @@ -396,11 +403,10 @@ function UploadForm({ ), }} /> -
+
Name -
-
+
Summary -
-
+
+ {datasetUploadType.formConfig.dependencies && ( +
+ + {datasetUploadType.formConfig.dependencies.label} + + {datasetUploadType.formConfig.dependencies.render({ + value: dependencies, + onChange: setDependencies, + })} +
+ )} { -
+
{uploadMethodItems.length === 1 ? (
@@ -577,7 +600,18 @@ function validateForm( enableResultUploadMethod: boolean, formContent: FormContent ): FormValidation { - const { name, summary, description, dataUploadSelection } = formContent; + const { name, summary, description, dataUploadSelection, dependencies } = + formContent; + + if ( + datasetUploadType.formConfig.dependencies?.required && + dependencies == null + ) { + return { + valid: false, + errors: [`Required: ${datasetUploadType.formConfig.dependencies.label}`], + }; + } if (!isCompleteDataUploadSelection(dataUploadSelection)) { return { @@ -607,6 +641,7 @@ function validateForm( datasetType: datasetUploadType.type, projects: [projectId], dataUploadSelection, + dependencies, visibility: 'private', }, }; diff --git a/packages/libs/user-datasets/src/lib/Components/UploadFormMenu.scss b/packages/libs/user-datasets/src/lib/Components/UploadFormMenu.scss new file mode 100644 index 0000000000..a4e65530eb --- /dev/null +++ b/packages/libs/user-datasets/src/lib/Components/UploadFormMenu.scss @@ -0,0 +1,37 @@ +.UserDatasetUploadFormMenu { + max-width: 1000px; + padding: 1em 3em; + h2 { + color: #222; + font-size: 1.5em; + font-weight: 500; + margin: 0; + padding: 22px 0 8px 0; + } + menu { + display: flex; + flex-direction: column; + list-style: none; + gap: 1em; + padding: 0; + width: max-content; + + a.btn { + display: inline-block; + width: 100%; + padding: 1.25em; + font-size: 1.1em; + + .title { + font-size: 1.5em; + font-weight: 600; + } + .description { + font-size: 1.1em; + font-weight: 500; + margin: 0; + margin-top: 1em; + } + } + } +} diff --git a/packages/libs/user-datasets/src/lib/Components/UploadFormMenu.tsx b/packages/libs/user-datasets/src/lib/Components/UploadFormMenu.tsx new file mode 100644 index 0000000000..6687a8ea28 --- /dev/null +++ b/packages/libs/user-datasets/src/lib/Components/UploadFormMenu.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { DatasetUploadTypeConfigEntry } from '../Utils/types'; +import { Link, useRouteMatch } from 'react-router-dom'; + +import './UploadFormMenu.scss'; + +interface Props { + availableTypes: string[]; + datasetUploadTypes: Record>; +} + +export function UploadFormMenu(props: Props) { + const { availableTypes, datasetUploadTypes } = props; + const { url } = useRouteMatch(); + return ( +
+

Choose an upload type

+ + {availableTypes.map((type) => { + const datasetUploadType = datasetUploadTypes[type]; + return ( + datasetUploadType && ( +
  • + +
    + {' '} + {datasetUploadType.displayName} +
    +
    + {datasetUploadType.description} +
    + +
  • + ) + ); + })} +
    +
    + ); +} diff --git a/packages/libs/user-datasets/src/lib/Components/UserDatasetsWorkspace.tsx b/packages/libs/user-datasets/src/lib/Components/UserDatasetsWorkspace.tsx index ecc8849e5c..ec6f8a1ab8 100644 --- a/packages/libs/user-datasets/src/lib/Components/UserDatasetsWorkspace.tsx +++ b/packages/libs/user-datasets/src/lib/Components/UserDatasetsWorkspace.tsx @@ -1,6 +1,6 @@ import { ReactNode } from 'react'; -import { Switch, Redirect } from 'react-router-dom'; +import { Switch, Redirect, RouteComponentProps } from 'react-router-dom'; import WorkspaceNavigation from '@veupathdb/wdk-client/lib/Components/Workspace/WorkspaceNavigation'; import WdkRoute from '@veupathdb/wdk-client/lib/Core/WdkRoute'; @@ -49,11 +49,8 @@ function UserDatasetsWorkspace(props: Props) { { display: 'New upload', route: '/new', + exact: false, }, - // { - // display: 'Recent uploads', - // route: '/recent', - // }, ] : [], helpTabContents != null @@ -87,16 +84,13 @@ function UserDatasetsWorkspace(props: Props) { ( + path={`${baseUrl}/new/:type?`} + component={(childProps: RouteComponentProps<{ type?: string }>) => ( )} diff --git a/packages/libs/user-datasets/src/lib/Controllers/UserDatasetNewUploadController.tsx b/packages/libs/user-datasets/src/lib/Controllers/UserDatasetNewUploadController.tsx index 1f928121d0..a3d9963d37 100644 --- a/packages/libs/user-datasets/src/lib/Controllers/UserDatasetNewUploadController.tsx +++ b/packages/libs/user-datasets/src/lib/Controllers/UserDatasetNewUploadController.tsx @@ -20,20 +20,56 @@ import { StateSlice } from '../StoreModules/types'; import { datasetIdType, DatasetUploadTypeConfigEntry } from '../Utils/types'; import { assertIsVdiCompatibleWdkService } from '../Service'; +import { NotFoundController } from '@veupathdb/wdk-client/lib/Controllers'; +import { UploadFormMenu } from '../Components/UploadFormMenu'; const SUPPORTED_FILE_UPLOAD_TYPES: string[] = []; -interface Props { +interface Props { + baseUrl: string; + type?: string; + availableTypes: string[]; + datasetUploadTypes: Record>; + urlParams: Record; +} + +export default function UserDatasetUploadSelector(props: Props) { + const { baseUrl, type, availableTypes, datasetUploadTypes, urlParams } = + props; + + if (type == null && availableTypes.length !== 1) { + return ( + + ); + } + + const datasetUploadType = datasetUploadTypes[type ?? availableTypes[0]]; + if (datasetUploadType == null) { + return ; + } + return ( + + ); +} + +interface InnerProps { baseUrl: string; datasetUploadType: DatasetUploadTypeConfigEntry; urlParams: Record; } -export default function UserDatasetUploadController({ +function InnerUserDatasetUploadController({ baseUrl, datasetUploadType, urlParams, -}: Props) { +}: InnerProps) { useSetDocumentTitle(datasetUploadType.uploadTitle); const projectId = useWdkService( diff --git a/packages/libs/user-datasets/src/lib/Controllers/UserDatasetRouter.tsx b/packages/libs/user-datasets/src/lib/Controllers/UserDatasetRouter.tsx index 6c835766fc..a99aed7e09 100644 --- a/packages/libs/user-datasets/src/lib/Controllers/UserDatasetRouter.tsx +++ b/packages/libs/user-datasets/src/lib/Controllers/UserDatasetRouter.tsx @@ -78,7 +78,7 @@ export function UserDatasetRouter({ }} /> = { export interface DatasetUploadTypeConfigEntry { type: T; + displayName: string; + description: React.ReactNode; uploadTitle: string; formConfig: { name?: { inputProps: Partial>; }; summary?: { - inputProps: Partial>; + inputProps: Partial>; }; description?: { inputProps: Partial>; }; + dependencies?: { + label: ReactNode; + render: (props: DependencyProps) => ReactNode; + required?: boolean; + }; uploadMethodConfig: { file?: FileUploadConfig; url?: UrlUploadConfig; @@ -97,6 +104,11 @@ export interface DatasetUploadTypeConfigEntry { }; } +export interface DependencyProps { + value: UserDataset['dependencies']; + onChange: (value: UserDataset['dependencies']) => void; +} + export interface FileUploadConfig { render?: (props: { fieldNode: ReactNode }) => ReactNode; maxSizeBytes?: number; @@ -130,6 +142,7 @@ export type DatasetUploadPageConfig< export interface NewUserDataset extends UserDatasetMeta { datasetType: string; // In prototype, the only value is "biom" - will eventually be an enum projects: string[]; + dependencies?: UserDataset['dependencies']; uploadMethod: | { type: 'file'; diff --git a/packages/libs/user-datasets/src/lib/Utils/upload-config.tsx b/packages/libs/user-datasets/src/lib/Utils/upload-config.tsx index a903349177..e01bc58664 100644 --- a/packages/libs/user-datasets/src/lib/Utils/upload-config.tsx +++ b/packages/libs/user-datasets/src/lib/Utils/upload-config.tsx @@ -1,112 +1,6 @@ import { intersection } from 'lodash'; - import { DatasetUploadPageConfig, DatasetUploadTypeConfig } from './types'; -type ImplementedUploadTypes = 'biom' | 'genelist' | 'isasimple'; - -export const uploadTypeConfig: DatasetUploadTypeConfig = - { - biom: { - type: 'biom', - uploadTitle: 'Upload My Data Set', - formConfig: { - summary: { - inputProps: { - placeholder: 'brief summary of the study in a few sentences', - }, - }, - description: { - inputProps: { - required: false, - placeholder: - 'optional longer description of the summary including background, study objectives, methodology, etc.', - }, - }, - renderInfo: () => ( -

    - We accept any file in the{' '} - BIOM format, either JSON-based - (BIOM 1.0) or HDF5 (BIOM 2.0+). -
    -
    - If possible, try including taxonomic information and rich sample - details in your file. This will allow you to select groups of - samples and create meaningful comparisons at a desired aggregation - level, using our filtering and visualisation tools. -

    - ), - uploadMethodConfig: { - file: { - maxSizeBytes: 100 * 1000 * 1000, // 100MB - render: ({ fieldNode }) => ( - <> - {fieldNode} -
    - File must be less than 100MB -
    - - ), - }, - }, - }, - }, - genelist: { - type: 'genelist', - uploadTitle: 'Upload My Gene List', - formConfig: { - uploadMethodConfig: { - result: { - offerStrategyUpload: false, - compatibleRecordTypes: { - transcript: { - reportName: 'attributesTabular', - reportConfig: { - attributes: ['primary_key'], - includeHeader: false, - attachmentType: 'plain', - applyFilter: true, - }, - }, - }, - }, - }, - }, - }, - isasimple: { - type: 'isasimple', - uploadTitle: 'Upload My Study', - formConfig: { - summary: { - inputProps: { - placeholder: 'brief summary of the study in a few sentences', - }, - }, - description: { - inputProps: { - required: false, - placeholder: - 'optional longer description of the study including background, study objectives, methodology, etc.', - }, - }, - uploadMethodConfig: { - file: { - render: ({ fieldNode }) => ( - <> - {fieldNode} -
    - File must be a .csv, .tsv, or tab-delimited .txt file -
    - - ), - }, - url: { - offer: false, - }, - }, - }, - }, - }; - export function makeDatasetUploadPageConfig< T1 extends string, T2 extends string diff --git a/packages/libs/wdk-client/src/Hooks/WdkDependenciesEffect.ts b/packages/libs/wdk-client/src/Hooks/WdkDependenciesEffect.ts index 4e7e65b249..5d9f9560f2 100644 --- a/packages/libs/wdk-client/src/Hooks/WdkDependenciesEffect.ts +++ b/packages/libs/wdk-client/src/Hooks/WdkDependenciesEffect.ts @@ -16,6 +16,18 @@ export const WdkDependenciesContext = export type WdkDependenciesEffectCallback = DepEffectCallback; +export function useWdkDependenciesContext() { + const wdkDependencies = useContext(WdkDependenciesContext); + + if (wdkDependencies == null) { + throw new Error( + 'useWdkDependenciesEffect requires WdkDependencies to be provided via React context' + ); + } + + return wdkDependencies; +} + export const useWdkDependenciesEffect = ( effect: WdkDependenciesEffectCallback, deps?: DependencyList diff --git a/packages/libs/web-common/package.json b/packages/libs/web-common/package.json index 5a2fd7aced..cd5573a51d 100644 --- a/packages/libs/web-common/package.json +++ b/packages/libs/web-common/package.json @@ -14,6 +14,14 @@ "build-npm-modules": "npm-run-all build generate-icons", "test": "echo \"Error: no test specified\" && exit 1" }, + "eslintConfig": { + "extends": [ + "@veupathdb" + ] + }, + "browserslist": [ + "extends @veupathdb/browserslist-config" + ], "files": [ "dist", "src", diff --git a/packages/libs/web-common/src/user-dataset-upload-config.tsx b/packages/libs/web-common/src/user-dataset-upload-config.tsx new file mode 100644 index 0000000000..535e99ec67 --- /dev/null +++ b/packages/libs/web-common/src/user-dataset-upload-config.tsx @@ -0,0 +1,332 @@ +import { + DatasetUploadTypeConfig, + DependencyProps, + UserDataset, +} from '@veupathdb/user-datasets/lib/Utils/types'; +import { useOrganismTree } from './hooks/organisms'; +import { SelectTree } from '@veupathdb/coreui'; +import { useCallback, useState } from 'react'; +import { projectId } from './config'; +import { useWdkService } from '@veupathdb/wdk-client/lib/Hooks/WdkServiceHook'; +import { TreeBoxVocabNode } from '@veupathdb/wdk-client/lib/Utils/WdkModel'; +import { Node } from '@veupathdb/wdk-client/lib/Utils/TreeUtils'; +import { areTermsInString } from '@veupathdb/wdk-client/lib/Utils/SearchUtils'; + +type ImplementedUploadTypes = + | 'biom' + | 'genelist' + | 'isasimple' + | 'bigwigfiles' + | 'rnaseq'; + +export const uploadTypeConfig: DatasetUploadTypeConfig = + { + rnaseq: { + type: 'rnaseq', + displayName: 'RNA-Seq', + description: `Integrate your RNA-Seq data in ${projectId}.`, + uploadTitle: 'Upload My RNA-Seq Data Set', + formConfig: { + summary: { + inputProps: { + placeholder: 'brief summary of the study in a few sentences', + }, + }, + description: { + inputProps: { + required: false, + placeholder: 'optional longer description of the summary', + }, + }, + dependencies: { + label: 'Reference Genome', + required: true, + render: (props) => , + }, + renderInfo: () => ( +

    + Please upload a zip file with your RNASeq results: your bigWig and + fpkm fastq files containing your processed reads. +
    + Each file in the collection of FPKM or TPM files should be a two + column tab-delimited file where the first column contains gene ids, + and the second column contains normalized counts for each gene, + either FPKM or TPM. The first line must have column headings + 'gene_id' and either 'FPKM' or 'TMP'. +
    +
    + The files must be mapped to the reference genome that you select + below. +
    + Only letters, numbers, spaces and dashes are allowed in the file + name. +
    + Please restrict the name to 100 characters or less. +

    + ), + uploadMethodConfig: { + file: { + maxSizeBytes: 10 * 1000 * 1000 * 1000, // 10GB + render: ({ fieldNode }) => ( + <> + {fieldNode} +
    + File must be less than 10GB +
    + + ), + }, + }, + }, + }, + bigwigfiles: { + type: 'bigwigfiles', + displayName: 'bigWig', + description: `Integrate your BigWig data in ${projectId}.`, + uploadTitle: 'Upload My bigWig Data Set', + formConfig: { + summary: { + inputProps: { + placeholder: 'brief summary in a few sentences', + }, + }, + description: { + inputProps: { + required: false, + placeholder: 'optional longer description of the summary.', + }, + }, + dependencies: { + label: 'Reference Genome', + required: true, + render: (props) => , + }, + renderInfo: () => ( +

    + We accept any file in the{' '} + + bigWig format + + . +
    + The bigwig files you select here must be mapped to the reference + genome that you select below. +
    + Only letters, numbers, spaces and dashes are allowed in the file + name. +
    + Please restrict the name to 100 characters or less. +

    + ), + uploadMethodConfig: { + file: { + maxSizeBytes: 10 * 1000 * 1000 * 1000, // 10GB + render: ({ fieldNode }) => ( + <> + {fieldNode} +
    + File must be less than 10GB +
    + + ), + }, + }, + }, + }, + biom: { + type: 'biom', + displayName: 'BIOM', + description: `Integrate your BIOM study data in ${projectId}.`, + uploadTitle: 'Upload My Data Set', + formConfig: { + summary: { + inputProps: { + placeholder: 'brief summary of the study in a few sentences', + }, + }, + description: { + inputProps: { + required: false, + placeholder: + 'optional longer description of the summary including background, study objectives, methodology, etc.', + }, + }, + renderInfo: () => ( +

    + We accept any file in the{' '} + BIOM format, either JSON-based + (BIOM 1.0) or HDF5 (BIOM 2.0+). +
    +
    + If possible, try including taxonomic information and rich sample + details in your file. This will allow you to select groups of + samples and create meaningful comparisons at a desired aggregation + level, using our filtering and visualisation tools. +

    + ), + uploadMethodConfig: { + file: { + maxSizeBytes: 100 * 1000 * 1000, // 100MB + render: ({ fieldNode }) => ( + <> + {fieldNode} +
    + File must be less than 100MB +
    + + ), + }, + }, + }, + }, + genelist: { + type: 'genelist', + displayName: 'Gene List', + description: `Integrate your gene list in ${projectId}.`, + uploadTitle: 'Upload My Gene List', + formConfig: { + uploadMethodConfig: { + result: { + offerStrategyUpload: false, + compatibleRecordTypes: { + transcript: { + reportName: 'attributesTabular', + reportConfig: { + attributes: ['primary_key'], + includeHeader: false, + attachmentType: 'plain', + applyFilter: true, + }, + }, + }, + }, + }, + }, + }, + isasimple: { + type: 'isasimple', + displayName: 'ISA Study', + description: `Integrate your study data in ${projectId}.`, + uploadTitle: 'Upload My Study', + formConfig: { + summary: { + inputProps: { + placeholder: 'brief summary of the study in a few sentences', + }, + }, + description: { + inputProps: { + required: false, + placeholder: + 'optional longer description of the study including background, study objectives, methodology, etc.', + }, + }, + uploadMethodConfig: { + file: { + render: ({ fieldNode }) => ( + <> + {fieldNode} +
    + File must be a .csv, .tsv, or tab-delimited .txt file +
    + + ), + }, + url: { + offer: false, + }, + }, + }, + }, + }; + +const styleOverrides = { + treeNode: { + labelTextWrapper: { + fontSize: '1.1em', + }, + }, +}; + +function ReferenceGenomeDepdency(props: DependencyProps) { + const { value, onChange } = props; + const selectedList = value?.map((entry) => entry.resourceDisplayName); + const organismTree = useOrganismTree(true); + const fileNameByTerm = useWdkService(async (wdkService) => { + const genomeDataTypesResult = await wdkService.getAnswerJson( + { + searchName: 'GenomeDataTypes', + searchConfig: { parameters: {} }, + }, + { + attributes: ['organism_full', 'name_for_filenames'], + pagination: { + numRecords: -1, + offset: 0, + }, + } + ); + return new Map( + genomeDataTypesResult.records.map((rec) => [ + rec.attributes.organism_full as string, + rec.attributes.name_for_filenames as string, + ]) + ); + }, []); + const buildNumber = useWdkService(async (wdkService) => { + const config = await wdkService.getConfig(); + return config.buildNumber; + }, []); + const onSelectionChange = useCallback( + function handleChange(selection: string[]) { + if (fileNameByTerm == null || buildNumber == null) return; + const dependencies = selection + .map((term) => { + const fileName = fileNameByTerm.get(term); + return fileName == null + ? undefined + : { + resourceDisplayName: term, + resourceIdentifier: `${projectId}-${buildNumber}_${fileName}_Genome`, + resourceVersion: buildNumber, + }; + }) + .filter((dep) => dep != null) as UserDataset['dependencies']; + onChange(dependencies); + }, + [buildNumber, fileNameByTerm, onChange] + ); + const [expandedNodes, setExpandedNodes] = useState([]); + const [searchTerm, setSearchTerm] = useState(''); + if (organismTree == null) return null; + return ( + + ); +} + +function getNodeId(node: Node) { + return node.data.term; +} +function getNodeChildren(node: Node) { + return node.children; +} +function searchPredicate(node: Node, terms: string[]) { + return areTermsInString(terms, node.data.display); +} diff --git a/packages/sites/clinepi-site/webapp/js/client/routes/userDatasetRoutes.tsx b/packages/sites/clinepi-site/webapp/js/client/routes/userDatasetRoutes.tsx index ce867336c3..ccb10ce801 100644 --- a/packages/sites/clinepi-site/webapp/js/client/routes/userDatasetRoutes.tsx +++ b/packages/sites/clinepi-site/webapp/js/client/routes/userDatasetRoutes.tsx @@ -10,7 +10,7 @@ import { diyUserDatasetIdToWdkRecordId } from '@veupathdb/user-datasets/lib/Util import { UserDatasetDetailProps } from '@veupathdb/user-datasets/lib/Controllers/UserDatasetDetailController'; -import { uploadTypeConfig } from '@veupathdb/user-datasets/lib/Utils/upload-config'; +import { uploadTypeConfig } from '@veupathdb/web-common/lib/user-dataset-upload-config'; import { communitySite, diff --git a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/userDatasetRoutes.tsx b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/userDatasetRoutes.tsx index 62012246df..7bdbef30c1 100644 --- a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/userDatasetRoutes.tsx +++ b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/userDatasetRoutes.tsx @@ -8,13 +8,13 @@ import { RouteEntry } from '@veupathdb/wdk-client/lib/Core/RouteEntry'; import { communitySite } from '@veupathdb/web-common/lib/config'; import ExternalContentController from '@veupathdb/web-common/lib/controllers/ExternalContentController'; -import { uploadTypeConfig } from '@veupathdb/user-datasets/lib/Utils/upload-config'; +import { uploadTypeConfig } from '@veupathdb/web-common/lib/user-dataset-upload-config'; const UserDatasetRouter = React.lazy( () => import('./controllers/UserDatasetRouter') ); -const availableUploadTypes = ['genelist']; +const availableUploadTypes = ['genelist', 'bigwigfiles', 'rnaseq']; const USER_DATASETS_HELP_PAGE = 'user_datasets_help.html'; diff --git a/packages/sites/mbio-site/webapp/wdkCustomization/js/client/routes/userDatasetRoutes.tsx b/packages/sites/mbio-site/webapp/wdkCustomization/js/client/routes/userDatasetRoutes.tsx index c822434a5e..b36217f010 100644 --- a/packages/sites/mbio-site/webapp/wdkCustomization/js/client/routes/userDatasetRoutes.tsx +++ b/packages/sites/mbio-site/webapp/wdkCustomization/js/client/routes/userDatasetRoutes.tsx @@ -10,7 +10,7 @@ import { diyUserDatasetIdToWdkRecordId } from '@veupathdb/user-datasets/lib/Util import { UserDatasetDetailProps } from '@veupathdb/user-datasets/lib/Controllers/UserDatasetDetailController'; -import { uploadTypeConfig } from '@veupathdb/user-datasets/lib/Utils/upload-config'; +import { uploadTypeConfig } from '@veupathdb/web-common/lib/user-dataset-upload-config'; import { communitySite, projectId } from '@veupathdb/web-common/lib/config'; From 31edb9adeefa1f4a899117ec5c92bc03ec99731c Mon Sep 17 00:00:00 2001 From: Dave Falke Date: Fri, 1 Nov 2024 10:54:32 -0400 Subject: [PATCH 03/10] use tag-based workflow to publish npm packages --- .github/workflows/npm-publish-sites.yml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/npm-publish-sites.yml b/.github/workflows/npm-publish-sites.yml index 67092afc65..7702527530 100644 --- a/.github/workflows/npm-publish-sites.yml +++ b/.github/workflows/npm-publish-sites.yml @@ -3,9 +3,9 @@ name: NPM Publish Sites on: - release: - types: - - published + push: + tags: + - v* jobs: # Gather the names of the sites directories and store it as an output variable @@ -51,15 +51,11 @@ jobs: elif [[ "$git_tag" =~ ^[0-9]+\.[0-9]+\.[0-9]+-([a-zA-Z0-9]+)(\.[0-9]+)*$ ]]; then npm_tag="${BASH_REMATCH[1]}" echo "npm_tag=$npm_tag" >> $GITHUB_ENV - # if prerelease is checked, use "prerelease" for the npm tag - elif ${{ github.event.release.prerelease }}; then - echo "npm_tag=prerelease" >> $GITHUB_ENV # or fall back to 'latest' just in case else - echo "npm_tag=latest" >> $GITHUB_ENV + echo "npm_tag=alpha" >> $GITHUB_ENV fi - uses: JS-DevTools/npm-publish@v2 - if: ${{ startsWith(github.event.release.tag_name, 'v') }} with: token: ${{ secrets.NPM_TOKEN }} access: public From 48d15cfba41bb84bc414a11e29beb8e1a1c3ccef Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 1 Nov 2024 15:27:22 +0000 Subject: [PATCH 04/10] 90 percent fixed --- .../components/src/map/SemanticMarkers.tsx | 47 +++++-------------- .../src/map/animation_functions/geohash.tsx | 11 ++--- .../map/animation_functions/updateMarkers.tsx | 3 ++ 3 files changed, 19 insertions(+), 42 deletions(-) diff --git a/packages/libs/components/src/map/SemanticMarkers.tsx b/packages/libs/components/src/map/SemanticMarkers.tsx index dfc0715261..5e59be5a11 100644 --- a/packages/libs/components/src/map/SemanticMarkers.tsx +++ b/packages/libs/components/src/map/SemanticMarkers.tsx @@ -64,7 +64,7 @@ export default function SemanticMarkers({ // 2023-11: it does seem to be needed for zoom-in animation to work. const debouncedUpdateMarkerPositions = debounce( updateMarkerPositions, - 1000 + animation ? animation.duration : 0 ); // call it at least once at the beginning of the life cycle debouncedUpdateMarkerPositions(); @@ -126,27 +126,23 @@ export default function SemanticMarkers({ ) { // get the position-modified markers from `animationFunction` // see geohash.tsx for example - const animationValues = animation.animationFunction({ - prevMarkers: prevRecenteredMarkers, - markers: recenteredMarkers, - }); + const { markers: oldAndNewRepositionedMarkers } = + animation.animationFunction({ + prevMarkers: prevRecenteredMarkers, + markers: recenteredMarkers, + }); // set them as current // any marker that already existed will move to the modified position if ( !isEqual( - animationValues.markers.map(({ props }) => props), + oldAndNewRepositionedMarkers.map(({ props }) => props), consolidatedMarkers.map(({ props }) => props) ) ) - setConsolidatedMarkers(animationValues.markers); - // then set a timer to remove the old markers when zooming out - // or if zooming in, switch to just the new markers straight away - // (their starting position was set by `animationFunction`) - // It's complicated but it works! - timeoutVariable = enqueueZoom( - animationValues.zoomType, - recenteredMarkers - ); + setConsolidatedMarkers(oldAndNewRepositionedMarkers); + + // we used to set a timer to remove the old markers when zooming out + // but now we just let the next render cycle do it. } else { /** First render of markers **/ if ( @@ -171,27 +167,6 @@ export default function SemanticMarkers({ ) setPrevRecenteredMarkers(recenteredMarkers); } - - function enqueueZoom( - zoomType: string | null, - nextMarkers: ReactElement[] - ) { - /** If we are zooming in then reset the marker elements. When initially rendered - * the new markers will start at the matching existing marker's location and here we will - * reset marker elements so they will animated to their final position - **/ - if (zoomType === 'in') { - setConsolidatedMarkers(nextMarkers); - } else if (zoomType === 'out') { - /** If we are zooming out then remove the old markers after they finish animating. **/ - return window.setTimeout( - () => { - setConsolidatedMarkers(nextMarkers); - }, - animation ? animation.duration : 0 - ); - } - } }, [ animation, map, diff --git a/packages/libs/components/src/map/animation_functions/geohash.tsx b/packages/libs/components/src/map/animation_functions/geohash.tsx index 12ea0afaa3..4e27553172 100644 --- a/packages/libs/components/src/map/animation_functions/geohash.tsx +++ b/packages/libs/components/src/map/animation_functions/geohash.tsx @@ -22,9 +22,9 @@ export default function geohashAnimation({ if (prevGeoHash.length > currentGeohash.length) { zoomType = 'out'; const hashDif = prevGeoHash.length - currentGeohash.length; - // Get a new array of existing markers with new position property + // Get array of old markers with new positions const cloneArray = updateMarkers(prevMarkers, markers, hashDif); - // Combine the new and existing markers + // Combine the new and old markers consolidatedMarkers = [...markers, ...cloneArray]; } else if (prevGeoHash.length < currentGeohash.length) { /** Zoom In - New markers start at old position @@ -33,10 +33,9 @@ export default function geohashAnimation({ **/ zoomType = 'in'; const hashDif = currentGeohash.length - prevGeoHash.length; - // Get a new array of new markers with existing position property - // Set final render markers to the cloneArray which holds the new markers with - // their new starting location - consolidatedMarkers = updateMarkers(markers, prevMarkers, hashDif); + // Get array of new markers with old positions + const cloneArray = updateMarkers(markers, prevMarkers, hashDif); + consolidatedMarkers = [...prevMarkers, ...cloneArray]; } else { /** No difference in geohashes - Render markers as they are **/ zoomType = null; diff --git a/packages/libs/components/src/map/animation_functions/updateMarkers.tsx b/packages/libs/components/src/map/animation_functions/updateMarkers.tsx index e4e1f30e15..a727f77f37 100644 --- a/packages/libs/components/src/map/animation_functions/updateMarkers.tsx +++ b/packages/libs/components/src/map/animation_functions/updateMarkers.tsx @@ -22,6 +22,9 @@ export default function updateMarkers( // Clone marker element with new position markerCloneProps = { position: matchingMarkers[0].props.position, + // ideally we would put the modified markers on top + // but this doesn't seem to work: + // zIndexOffset: -1000, // or +1000 }; } From 5dce817f84458302075df578d396f7d0deb2faac Mon Sep 17 00:00:00 2001 From: Dave Falke Date: Fri, 1 Nov 2024 14:30:20 -0400 Subject: [PATCH 05/10] Override wdkService methods to omit the blast input query from WDK service requests (#1258) * Override wdkService methods to omit the blast input sequence from WDK service requests * Add comments --- .../src/lib/utils/wdkServiceIntegration.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/packages/libs/multi-blast/src/lib/utils/wdkServiceIntegration.ts b/packages/libs/multi-blast/src/lib/utils/wdkServiceIntegration.ts index 8686c4be9e..3f86bcfccc 100644 --- a/packages/libs/multi-blast/src/lib/utils/wdkServiceIntegration.ts +++ b/packages/libs/multi-blast/src/lib/utils/wdkServiceIntegration.ts @@ -5,6 +5,8 @@ import { record, string, } from '@veupathdb/wdk-client/lib/Utils/Json'; +import { omit } from 'lodash'; +import { BLAST_QUERY_SEQUENCE_PARAM_NAME } from './params'; const blastParamInternalValues = objectOf( record({ @@ -22,6 +24,41 @@ export function wrapWdkService( ...wdkService, getBlastParamInternalValues: blastCompatibleWdkServiceWrappers.getBlastParamInternalValues(wdkService), + // Send an empty input query value, since they can potentially be very large + // when multiple sequences are included. + getRefreshedDependentParams( + questionUrlSegment, + paramName, + paramValue, + paramValues + ) { + if (questionUrlSegment.endsWith('MultiBlast')) { + paramValues = { + ...paramValues, + [BLAST_QUERY_SEQUENCE_PARAM_NAME]: '', + }; + } + return wdkService.getRefreshedDependentParams( + questionUrlSegment, + paramName, + paramValue, + paramValues + ); + }, + // Send an empty input query value, since they can potentially be very large + // when multiple sequences are included. + getQuestionGivenParameters(questionUrlSegment, paramValues) { + if (questionUrlSegment.endsWith('MultiBlast')) { + paramValues = { + ...paramValues, + [BLAST_QUERY_SEQUENCE_PARAM_NAME]: '', + }; + } + return wdkService.getQuestionGivenParameters( + questionUrlSegment, + paramValues + ); + }, }; } From c702c1cf7cd3d26218238746a1bca32ff2e01149 Mon Sep 17 00:00:00 2001 From: Bob MacCallum Date: Sun, 3 Nov 2024 13:24:47 +0000 Subject: [PATCH 06/10] failed attempt to get z-index ordering of animated markers correct --- .../components/src/map/animation_functions/geohash.tsx | 5 ++++- .../src/map/animation_functions/updateMarkers.tsx | 7 ++++--- packages/libs/components/src/map/styles/map-styles.css | 4 ++++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/libs/components/src/map/animation_functions/geohash.tsx b/packages/libs/components/src/map/animation_functions/geohash.tsx index 4e27553172..e962e92c86 100644 --- a/packages/libs/components/src/map/animation_functions/geohash.tsx +++ b/packages/libs/components/src/map/animation_functions/geohash.tsx @@ -35,7 +35,10 @@ export default function geohashAnimation({ const hashDif = currentGeohash.length - prevGeoHash.length; // Get array of new markers with old positions const cloneArray = updateMarkers(markers, prevMarkers, hashDif); - consolidatedMarkers = [...prevMarkers, ...cloneArray]; + // put them first - ideally underneath the old markers + // (though it seems impossible to get this to work) + // ((see also updateMarkers zIndexOffset failed attempt)) + consolidatedMarkers = [...cloneArray, ...prevMarkers]; } else { /** No difference in geohashes - Render markers as they are **/ zoomType = null; diff --git a/packages/libs/components/src/map/animation_functions/updateMarkers.tsx b/packages/libs/components/src/map/animation_functions/updateMarkers.tsx index a727f77f37..064eeab941 100644 --- a/packages/libs/components/src/map/animation_functions/updateMarkers.tsx +++ b/packages/libs/components/src/map/animation_functions/updateMarkers.tsx @@ -22,9 +22,10 @@ export default function updateMarkers( // Clone marker element with new position markerCloneProps = { position: matchingMarkers[0].props.position, - // ideally we would put the modified markers on top - // but this doesn't seem to work: - // zIndexOffset: -1000, // or +1000 + icon: { + ...markerObj.props.icon, + className: 'bottom-marker', // doesn't seem to work :-( + }, }; } diff --git a/packages/libs/components/src/map/styles/map-styles.css b/packages/libs/components/src/map/styles/map-styles.css index 1f3810d3d3..5d709bf9f9 100644 --- a/packages/libs/components/src/map/styles/map-styles.css +++ b/packages/libs/components/src/map/styles/map-styles.css @@ -71,6 +71,10 @@ z-index: 1; } +.bottom-marker { + z-index: -1000; +} + /* Replace leaflet styles*/ /* general typography */ From e8dc14d788cf50ef18e299c36b58afcc9ef2dfb3 Mon Sep 17 00:00:00 2001 From: Dave Falke Date: Tue, 5 Nov 2024 13:40:33 -0500 Subject: [PATCH 07/10] User dataset tweaks (#1261) * max height for SelectTree popup 40em * handle case where fileListing is undefined * Update verbiage --- .../coreui/src/components/inputs/SelectTree/SelectTree.tsx | 6 +++++- .../src/lib/Components/Detail/BigwigDatasetDetail.jsx | 2 +- packages/libs/web-common/src/user-dataset-upload-config.tsx | 5 +++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/libs/coreui/src/components/inputs/SelectTree/SelectTree.tsx b/packages/libs/coreui/src/components/inputs/SelectTree/SelectTree.tsx index 5e94400a2b..7d7655073e 100644 --- a/packages/libs/coreui/src/components/inputs/SelectTree/SelectTree.tsx +++ b/packages/libs/coreui/src/components/inputs/SelectTree/SelectTree.tsx @@ -85,7 +85,11 @@ function SelectTree(props: SelectTreeProps) { isDisabled={props.isDisabled} >
    {wrapPopover ? wrapPopover(checkboxTree) : checkboxTree}
    diff --git a/packages/libs/user-datasets/src/lib/Components/Detail/BigwigDatasetDetail.jsx b/packages/libs/user-datasets/src/lib/Components/Detail/BigwigDatasetDetail.jsx index dbccc3ec6c..dcabb91346 100644 --- a/packages/libs/user-datasets/src/lib/Components/Detail/BigwigDatasetDetail.jsx +++ b/packages/libs/user-datasets/src/lib/Components/Detail/BigwigDatasetDetail.jsx @@ -108,7 +108,7 @@ class BigwigDatasetDetail extends UserDatasetDetail { renderTracksSection() { const { userDataset, appUrl, projectName, config, fileListing } = this.props; - const installFiles = fileListing.install?.contents + const installFiles = fileListing?.install?.contents ?.filter((file) => file.fileName.endsWith('.bw')) .map((file) => ({ dataFileName: file.fileName, diff --git a/packages/libs/web-common/src/user-dataset-upload-config.tsx b/packages/libs/web-common/src/user-dataset-upload-config.tsx index 535e99ec67..2c656fe97d 100644 --- a/packages/libs/web-common/src/user-dataset-upload-config.tsx +++ b/packages/libs/web-common/src/user-dataset-upload-config.tsx @@ -56,7 +56,7 @@ export const uploadTypeConfig: DatasetUploadTypeConfig =

    The files must be mapped to the reference genome that you select - below. + above.
    Only letters, numbers, spaces and dashes are allowed in the file name. @@ -110,7 +110,7 @@ export const uploadTypeConfig: DatasetUploadTypeConfig = .
    The bigwig files you select here must be mapped to the reference - genome that you select below. + genome that you select above.
    Only letters, numbers, spaces and dashes are allowed in the file name. @@ -301,6 +301,7 @@ function ReferenceGenomeDepdency(props: DependencyProps) { if (organismTree == null) return null; return ( Date: Wed, 6 Nov 2024 12:23:51 -0500 Subject: [PATCH 08/10] Improve display for "queueing-error" (#1222) --- .../lib/components/BlastWorkspaceResult.tsx | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/libs/multi-blast/src/lib/components/BlastWorkspaceResult.tsx b/packages/libs/multi-blast/src/lib/components/BlastWorkspaceResult.tsx index 07f680d693..05f28e52ac 100644 --- a/packages/libs/multi-blast/src/lib/components/BlastWorkspaceResult.tsx +++ b/packages/libs/multi-blast/src/lib/components/BlastWorkspaceResult.tsx @@ -82,7 +82,23 @@ export function BlastWorkspaceResult(props: Props) { ) : jobResult.value?.status === 'error' ? ( ) : jobResult.value?.status === 'queueing-error' ? ( - + +
    + Your job did not run successfully. Please{' '} + + contact us + {' '} + for support. +
    +
    ) : queryResult.value?.status === 'error' ? ( ) : jobResult.value.job.config.tool.startsWith('diamond-') ? ( @@ -169,7 +185,23 @@ function StandardBlastResult( ) : reportResult.value != null && reportResult.value.status === 'queueing-error' ? ( - + +
    + We were unable to create your combined results report. Please{' '} + + contact us + {' '} + for support. +
    +
    ) : individualQueriesResult.value != null && individualQueriesResult.value.status === 'error' ? ( From aa42bc8a0f5a7fb01b344f089f44886a5382b8e3 Mon Sep 17 00:00:00 2001 From: Bob Date: Thu, 7 Nov 2024 17:10:17 +0000 Subject: [PATCH 09/10] fix fly-by problem from #628 --- packages/libs/components/src/map/SemanticMarkers.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/libs/components/src/map/SemanticMarkers.tsx b/packages/libs/components/src/map/SemanticMarkers.tsx index 5e59be5a11..73d2659433 100644 --- a/packages/libs/components/src/map/SemanticMarkers.tsx +++ b/packages/libs/components/src/map/SemanticMarkers.tsx @@ -106,6 +106,15 @@ export default function SemanticMarkers({ lnMin += 360; recentered = true; } + + // Is the new position inside the "viewport"? + // (strictly this is the un-greyed-out region in the middle when zoomed well out) + const inBounds = + lng <= bounds.northEast.lng && + lng >= bounds.southWest.lng && + lat <= bounds.northEast.lat && + lat >= bounds.southWest.lat; + return recentered ? cloneElement(marker, { position: { lat, lng }, @@ -113,6 +122,8 @@ export default function SemanticMarkers({ southWest: { lat: ltMin, lng: lnMin }, northEast: { lat: ltMax, lng: lnMax }, }, + // to prevent "fly-bys" (see #628) disable animation for out-of-bounds markers + ...(inBounds ? {} : { duration: -1 }), }) : marker; }) From de53de9d2996b74de4daa84cf25eee061b7e2f80 Mon Sep 17 00:00:00 2001 From: Dave Falke Date: Thu, 7 Nov 2024 16:55:55 -0500 Subject: [PATCH 10/10] EDA viz - handle invalid variable selections (#1262) --- .../visualizations/InputVariables.tsx | 61 +++++++++++++++++-- .../core/utils/data-element-constraints.ts | 16 ++--- 2 files changed, 65 insertions(+), 12 deletions(-) diff --git a/packages/libs/eda/src/lib/core/components/visualizations/InputVariables.tsx b/packages/libs/eda/src/lib/core/components/visualizations/InputVariables.tsx index e2a8b91627..e14f06e415 100644 --- a/packages/libs/eda/src/lib/core/components/visualizations/InputVariables.tsx +++ b/packages/libs/eda/src/lib/core/components/visualizations/InputVariables.tsx @@ -10,13 +10,17 @@ import { import VariableTreeDropdown from '../variableSelectors/VariableTreeDropdown'; import { Toggle } from '@veupathdb/coreui'; -import { makeEntityDisplayName } from '../../utils/study-metadata'; +import { + findEntityAndVariable, + makeEntityDisplayName, +} from '../../utils/study-metadata'; import { useInputStyles } from './inputStyles'; import { Tooltip } from '@veupathdb/coreui'; import RadioButtonGroup from '@veupathdb/components/lib/components/widgets/RadioButtonGroup'; import { isEqual } from 'lodash'; import { red } from '@veupathdb/coreui/lib/definitions/colors'; import { CSSProperties } from '@material-ui/core/styles/withStyles'; +import Banner from '@veupathdb/coreui/lib/components/banners/Banner'; export interface InputSpec { name: string; @@ -182,12 +186,27 @@ export function InputVariables(props: Props) { onChange({ ...selectedVariables, [inputName]: selectedVariable }); }; + const invalidInputs = inputs.filter((inputSpec) => { + const variableDescriptor = selectedVariables[inputSpec.name]; + if (variableDescriptor == null) return false; + const entityAndVariable = findEntityAndVariable( + entities, + variableDescriptor + ); + return ( + entityAndVariable == null || + entityAndVariable.variable.type === 'category' + ); + }); + // Find entities that are excluded for each variable, and union their variables // with the disabled variables. const disabledVariablesByInputName: Record = useMemo( () => inputs.reduce((map, input) => { + // ignore invalid inputs + if (invalidInputs.includes(input)) return map; // For each input (ex. xAxisVariable), determine its constraints based on which patterns any other selected variables match. const filteredConstraints = constraints && @@ -210,6 +229,7 @@ export function InputVariables(props: Props) { }, {} as Record), [ inputs, + invalidInputs, constraints, selectedVariables, entities, @@ -270,7 +290,8 @@ export function InputVariables(props: Props) { constraints && constraints.length && constraints[0][input.name]?.isRequired && - !selectedVariables[input.name] + (!selectedVariables[input.name] || + invalidInputs.includes(input)) ? requiredInputLabelStyle : input.role === 'stratification' && hasMultipleStratificationValues @@ -351,8 +372,16 @@ export function InputVariables(props: Props) { } starredVariables={starredVariables} toggleStarredVariable={toggleStarredVariable} - entityId={selectedVariables[input.name]?.entityId} - variableId={selectedVariables[input.name]?.variableId} + entityId={ + invalidInputs.includes(input) + ? undefined + : selectedVariables[input.name]?.entityId + } + variableId={ + invalidInputs.includes(input) + ? undefined + : selectedVariables[input.name]?.variableId + } variableLinkConfig={{ type: 'button', onClick: (variable) => @@ -396,6 +425,30 @@ export function InputVariables(props: Props) { {content}
    ))} + {invalidInputs.length > 0 && ( + + The following inputs reference a variable that no longer exists. + Use the dropdown to choose a new variable. + { +
      + {invalidInputs.map((inputSpec) => { + return ( +
    • + {inputSpec.label} +
    • + ); + })} +
    + } +
    + ), + }} + /> + )}
    ); } diff --git a/packages/libs/eda/src/lib/core/utils/data-element-constraints.ts b/packages/libs/eda/src/lib/core/utils/data-element-constraints.ts index dcf0216811..af41510236 100644 --- a/packages/libs/eda/src/lib/core/utils/data-element-constraints.ts +++ b/packages/libs/eda/src/lib/core/utils/data-element-constraints.ts @@ -286,6 +286,13 @@ export function filterConstraints( if (value == null) return true; // Ignore constraints that are on this selectedVarReference if (selectedVarReference === variableName) return true; + // {{ treat invalid selections as "empty" and return true + const entityAndVariable = findEntityAndVariable(entities, value); + if (entityAndVariable == null) return true; + const { variable } = entityAndVariable; + if (variable.type === 'category') return true; + // }} + // If a constraint does not declare shapes or types and it allows multivalued variables, then any value is allowed, thus the constraint is "in-play" if ( isEmpty(constraint.allowedShapes) && @@ -296,15 +303,8 @@ export function filterConstraints( constraint.allowMultiValued ) return true; + // Check that the value's associated variable has compatible characteristics - const entityAndVariable = findEntityAndVariable(entities, value); - if (entityAndVariable == null) - throw new Error( - `Could not find selected entity and variable: entityId = ${value.entityId}; variableId = ${value.variableId}.` - ); - const { variable } = entityAndVariable; - if (variable.type === 'category') - throw new Error('Categories are not allowed for variable constraints.'); const typeIsValid = isEmpty(constraint.allowedTypes) || constraint.allowedTypes?.includes(variable.type);