From b79cc3e28e61fe7b060224323e939507286321e3 Mon Sep 17 00:00:00 2001 From: Beking0912 <37431792+Beking0912@users.noreply.github.com> Date: Tue, 22 Aug 2023 19:06:41 -0400 Subject: [PATCH] Add Interactive Web Tour for Virtual Studies and Group Comparison (#4687) Co-authored-by: Ino de Bruijn --- .../local/runtime-config/portal.properties | 1 + src/config/IAppConfig.ts | 1 + src/config/serverConfigDefaults.ts | 1 + .../groupComparison/GroupComparisonPage.tsx | 5 +- .../groupSelector/GroupSelector.tsx | 1 + src/pages/home/HomePage.tsx | 5 + src/pages/studyView/StudyViewPage.tsx | 28 +- src/pages/studyView/charts/ChartContainer.tsx | 6 +- .../studyPageHeader/ActionButtons.tsx | 1 + .../studySummary/StudySummary.tsx | 1 + src/pages/studyView/tabs/SummaryTab.tsx | 3 +- .../studyView/virtualStudy/VirtualStudy.tsx | 8 +- .../components/PageLayout/PageLayout.tsx | 2 +- .../components/query/CancerStudySelector.tsx | 2 + .../components/query/CancerStudyTreeData.ts | 7 +- .../components/query/QueryContainer.tsx | 1 + .../components/query/studyList/StudyList.tsx | 6 +- src/shared/components/rightbar/RightBar.tsx | 23 ++ .../survival/LeftTruncationCheckbox.tsx | 2 +- src/tours/Steps/GroupComparison.tsx | 239 +++++++++++++ src/tours/Steps/VirtualStudy.tsx | 331 ++++++++++++++++++ src/tours/Steps/index.tsx | 9 + src/tours/Tour/index.tsx | 143 ++++++++ src/tours/Tour/styles.css | 55 +++ src/tours/Tour/types.tsx | 80 +++++ src/tours/index.tsx | 3 + 26 files changed, 951 insertions(+), 13 deletions(-) create mode 100644 src/tours/Steps/GroupComparison.tsx create mode 100644 src/tours/Steps/VirtualStudy.tsx create mode 100644 src/tours/Steps/index.tsx create mode 100644 src/tours/Tour/index.tsx create mode 100644 src/tours/Tour/styles.css create mode 100644 src/tours/Tour/types.tsx create mode 100644 src/tours/index.tsx diff --git a/end-to-end-test/local/runtime-config/portal.properties b/end-to-end-test/local/runtime-config/portal.properties index 7a2835fe36d..26d20e3b2ed 100644 --- a/end-to-end-test/local/runtime-config/portal.properties +++ b/end-to-end-test/local/runtime-config/portal.properties @@ -74,6 +74,7 @@ skin.right_nav.show_data_sets=true skin.right_nav.show_examples=false skin.right_nav.show_testimonials=false skin.right_nav.show_whats_new=false +skin.right_nav.show_web_tours=true # settings controlling what to show in the right navigation bar skin.study_view.link_text=To build your own case set, try out our enhanced Study View. diff --git a/src/config/IAppConfig.ts b/src/config/IAppConfig.ts index fdd227a5079..188bc441fcb 100644 --- a/src/config/IAppConfig.ts +++ b/src/config/IAppConfig.ts @@ -104,6 +104,7 @@ export interface IServerConfig { skin_right_nav_show_testimonials: boolean; skin_right_nav_show_whats_new: boolean; skin_right_nav_show_twitter: boolean; + skin_right_nav_show_web_tours: boolean; skin_right_nav_whats_new_blurb: string | null; skin_show_about_tab: boolean; skin_show_data_tab: boolean; diff --git a/src/config/serverConfigDefaults.ts b/src/config/serverConfigDefaults.ts index 323324caf38..885abd0b36c 100644 --- a/src/config/serverConfigDefaults.ts +++ b/src/config/serverConfigDefaults.ts @@ -75,6 +75,7 @@ export const ServerConfigDefaults: Partial = { skin_right_nav_show_examples: true, skin_right_nav_show_testimonials: true, skin_right_nav_show_whats_new: true, + skin_right_nav_show_web_tours: false, skin_right_nav_show_twitter: false, skin_citation_rule_text: 'Please cite: Cerami et al., 2012 & Gao et al., 2013', diff --git a/src/pages/groupComparison/GroupComparisonPage.tsx b/src/pages/groupComparison/GroupComparisonPage.tsx index e1039f1ef25..47255ad88d1 100644 --- a/src/pages/groupComparison/GroupComparisonPage.tsx +++ b/src/pages/groupComparison/GroupComparisonPage.tsx @@ -51,6 +51,7 @@ import { HelpWidget } from 'shared/components/HelpWidget/HelpWidget'; import GroupComparisonPathwayMapper from './pathwayMapper/GroupComparisonPathwayMapper'; import GroupComparisonMutationsTab from './GroupComparisonMutationsTab'; import GroupComparisonPathwayMapperUserSelectionStore from './pathwayMapper/GroupComparisonPathwayMapperUserSelectionStore'; +import { Tour } from 'tours'; export interface IGroupComparisonPageProps { routing: any; @@ -363,7 +364,7 @@ export default class GroupComparisonPage extends React.Component< break; case 1: studyHeader = ( -

+

{studies[0].name} @@ -392,6 +393,7 @@ export default class GroupComparisonPage extends React.Component< {studyHeader}Groups from{' '} {this.store.sessionClinicalAttributeName} @@ -506,6 +508,7 @@ export default class GroupComparisonPage extends React.Component<
{this.tabs.component}
+ {this.tabs.isComplete && } ); diff --git a/src/pages/groupComparison/groupSelector/GroupSelector.tsx b/src/pages/groupComparison/groupSelector/GroupSelector.tsx index 7047fea8f68..f8cdc825fc6 100644 --- a/src/pages/groupComparison/groupSelector/GroupSelector.tsx +++ b/src/pages/groupComparison/groupSelector/GroupSelector.tsx @@ -208,6 +208,7 @@ export default class GroupSelector extends React.Component< ); return (
{this.body.component} + {!this.isLoading && ( + + )} ); } diff --git a/src/pages/studyView/charts/ChartContainer.tsx b/src/pages/studyView/charts/ChartContainer.tsx index b8556a31044..9da99a2456d 100644 --- a/src/pages/studyView/charts/ChartContainer.tsx +++ b/src/pages/studyView/charts/ChartContainer.tsx @@ -108,6 +108,7 @@ const COMPARISON_CHART_TYPES: ChartType[] = [ ]; export interface IChartContainerProps { + id?: string; chartMeta: ChartMeta; chartType: ChartType; store: StudyViewPageStore; @@ -466,7 +467,10 @@ export class ChartContainer extends React.Component { if (this.selectedRowsKeys!.length >= 2) { return { content: ( -
+
{this.virtualStudyButtonTooltip}} > + +
+
+ ), + action: () => { + setLockTour(true); + } + }, + // Step 8: Intro to the Survival tab + { + selector: '[data-tour="mainColumn"]', + content: () => ( +
+

The Survival tab

+ The Survival tab shows a Kaplan-Meier plot of Overall + Survival or Disease/Progression-free Survival based on the selected groups. + This tab will only be visible when the original study contains survival data. +
+ + +
+
+ ), + action: () => { + setLockTour(true); + }, + }, + // Step 9: Intro to the Clinical tab + { + selector: '[data-tour="mainColumn"]', + content: () => ( +
+

The Clinical tab

+ The Clinical tab shows all the same clinical attributes that are present in Study View. + Select a clinical attribute in the table (Supervised DNA Methylation Cluster is selected here) + and a plot will appear to the right with the distribution of that clinical attribute across the selected groups. +
+ + +
+
+ ), + action: () => { + setLockTour(true); + }, + }, + // Step 10: Intro to the Genomic Alterations tab + { + selector: '[data-tour="mainColumn"]', + content: () => ( +
+

The Genomic Alterations tab

+ The Genomic Alterations tab compares the frequency of mutations in genes across the selected groups. + The visible plots change depending on how many groups are selected. +
+ ), + action: () => { + setLockTour(false); + }, + }, +]; + +export default getSteps; diff --git a/src/tours/Steps/VirtualStudy.tsx b/src/tours/Steps/VirtualStudy.tsx new file mode 100644 index 00000000000..644ed2eccbf --- /dev/null +++ b/src/tours/Steps/VirtualStudy.tsx @@ -0,0 +1,331 @@ +import React from 'react'; +import { GetSteps } from 'tours/Tour/types'; +import { setTourLocalStorage } from 'tours/Tour'; + +export const virtualStudyId = 'virtual-study-tour'; + +const getSteps: GetSteps = ({ + isLoggedIn, + studies, + setLockTour, + setGotoStep, + endTour, +}) => [ + // Step 0: Type “glioma” in the search box + { + selector: '[data-tour="cancer-study-search-box"] input', + content: () => ( +
+

Search for the studies

+ Use the search box to find the studies of interest. + For example, type in 'glioma', and then click the next step. +
+ ), + action: (node: Element) => { + // auto-fill “glioma” in the search box + setLockTour(false); + node.setAttribute('value', 'glioma'); + node.dispatchEvent(new Event('input', { bubbles: true })); + }, + }, + // Step 1: Select two studies + { + selector: '[data-tour="cancer-study-list-container"]', + content: () => ( +
+

Select two studies

+ Select two studies of interest. +
+ ), + action: () => { + // only when user selected more than one study, the tour will continue + setLockTour(true); + if (studies > 1) { + setLockTour(false); + } + }, + }, + // Step 2: Click the “Explore Selected Studies” button + { + selector: '[data-tour="explore-studies-button"]', + content: () => ( +
+

Click the Explore button

+ Click “Explore Selected Studies” button. +
+ ), + action: () => { + // before loading to the Study Summary page, record the current step + setTourLocalStorage(virtualStudyId, '3') + // hide the next step button, user should click the button to continue + setLockTour(true); + }, + }, + // Step 3: Click the “+” icon + { + selector: '[data-tour="show-more-description-icon"]', + content: () => ( +
+

See list of studies

+ Click on the “+” icon to see the list of studies. +
+ ), + }, + // Step 4: Select samples in the Mutated Genes table + { + selector: '[data-tour="mutated-genes-table"]', + content: () => ( +
+

Select samples

+ In the Mutated Genes table, Click the check box in the{' '} + “#” column to select samples with one more + mutation, and then click the “Select Samples”{' '} + button at the bottom of the table. +
+ ), + action: () => { + // only when user selected at least one study, the tour will continue + setLockTour(true); + if (studies > 0) { + setLockTour(false); + } + }, + }, + // tack onto last step where we describe what they are seeing. + { + selector: '', + content: () => ( +
+

Share/save your virtual study

+ We are now ready to create our virtual study. Let's create a + virtual study and share/save it. +
+ ), + }, + // Step 6: Click the bookmark icon + { + selector: '[data-tour="action-button-bookmark"]', + content: () => ( +
+

Click the bookmark icon

+ Click the bookmark icon to create and share + your virtual study. +
+ ), + action: (node: any) => { + // only after user clicked the bookmark icon, the tour will continue + if (node) { + setLockTour(true); + const handleClick = () => { + setTimeout(() => { + setGotoStep(7); + }, 400); + node.removeEventListener('click', handleClick); + }; + node.addEventListener('click', handleClick); + } + }, + }, + ...getRestSteps({ isLoggedIn, studies, setLockTour, setGotoStep, endTour }), +]; + +const getRestSteps: GetSteps = props => + props.isLoggedIn ? getLoggedInSteps(props) : getNotLoggedInSteps(props); + +const getNotLoggedInSteps: GetSteps = ({ + setLockTour, + setGotoStep, + endTour, +}) => [ + // Step 7: Click on the Share button + { + selector: '[data-tour="virtual-study-summary-panel"]', + content: () => ( +
+

Click on the Share button

+

1. Enter a name for your virtual study (optional).

+

+ 2. Text box pre-filled with a description of the studies + contributing samples and filters applied to the samples. You can edit this text +

+

+ 3. Check the list of studies contributing to samples with + links to the study summary for each. +

+

+ Click on the Share button for the next step. +

+
+ ), + action: () => { + // only after user clicked the share button, the tour will continue + setGotoStep(null); + const shareButton = document.querySelector( + '[data-tour="virtual-study-summary-share-btn"]' + ); + if (shareButton) { + const handleClick = () => { + setTimeout(() => { + setGotoStep(8); + }, 1000); + shareButton.removeEventListener('click', handleClick); + }; + shareButton.addEventListener('click', handleClick); + } + }, + }, + // Step 8: Show the share link + { + selector: '[data-tour="virtual-study-summary-panel"]', + content: () => ( +
+

Share your virtual study

+ Click on the link to open your virtual study, or click{' '} + “Copy” to copy the URL to your clipboard. +
+ ), + action: (node: any) => { + // clear the gotoStep + // after user clicked the panel, the tour ends + setGotoStep(null); + setLockTour(false); + if (node) { + const handleClick = () => { + endTour(); + node.removeEventListener('click', handleClick); + }; + node.addEventListener('click', handleClick); + } + }, + }, +]; + +const getLoggedInSteps: GetSteps = ({ setLockTour, setGotoStep, endTour }) => [ + // Step 7: Click on the Save button + { + selector: '[data-tour="virtual-study-summary-panel"]', + content: () => ( +
+

Click on the Save button

+

1. Enter a name for your virtual study (optional).

+ 2. Text box pre-filled with a description of the studies + contributing samples and filters applied to the samples. You can + edit this text +

+ 3. Check the list of studies contributing to samples with + links to the study summary for each. +

+

+ Click on the Save button for the next step. +

+
+ ), + action: () => { + // only after user clicked the save button, the tour will continue + setGotoStep(null); + const shareButton = document.querySelector( + '[data-tour="virtual-study-summary-save-btn"]' + ); + if (shareButton) { + const handleClick = () => { + setTimeout(() => { + setGotoStep(8); + }, 400); + shareButton.removeEventListener('click', handleClick); + }; + shareButton.addEventListener('click', handleClick); + } + }, + }, + // Step 8: Show the share link + { + selector: '[data-tour="virtual-study-summary-panel"]', + content: () => ( +
+

Already saved

+ Click on the link to open your virtual study, or click{' '} + “Copy” to copy the URL to your clipboard. +

+ When you save a study, it is added to the homepage, at the + top of the study list under “My Virtual Studies”. Clicking + “Query” brings you to the query selector with your new + virtual study pre-selected. +

+

+ Do you want to find it? +

+
+ + +
+
+ ), + action: (node: any) => { + // hide the original next step button + setLockTour(true); + + const queryButton = document.querySelector( + '[data-tour="virtual-study-summary-query-btn"]' + ); + if (queryButton) { + const handleClick = () => { + setTourLocalStorage(virtualStudyId, '9') + queryButton.removeEventListener('click', handleClick); + }; + queryButton.addEventListener('click', handleClick); + return; + } + + if (node) { + // after user clicked the panel, clear the gotoStep, the tour ends + const handleClick = () => { + setLockTour(false); + setGotoStep(null); + endTour(); + node.removeEventListener('click', handleClick); + }; + node.addEventListener('click', handleClick); + } + }, + }, + // Step 9: In homepage, Show the new virtual study pre-selected + { + selector: '[data-tour="my_virtual_studies_list"]', + content: () => ( +
+

My Virtual Studies

+ Your study is added to the homepage, at the top of the study + list under “My Virtual Studies”. +
+ ), + action: () => { + setLockTour(false); + // end the tour if user clicked the screen + const searchBox = document.querySelector('[data-tour="cancer-study-search-box"] input'); + + if (searchBox && (searchBox as any).value !== '') { + searchBox.setAttribute('value', ''); + searchBox.dispatchEvent(new Event('input', { bubbles: true })); + } + + setLockTour(false); + setGotoStep(null); + const handleClick = () => { + endTour(); + document.removeEventListener('click', handleClick); + }; + document.addEventListener('click', handleClick); + }, + }, +]; + +export default getSteps; diff --git a/src/tours/Steps/index.tsx b/src/tours/Steps/index.tsx new file mode 100644 index 00000000000..54578992cec --- /dev/null +++ b/src/tours/Steps/index.tsx @@ -0,0 +1,9 @@ +import getVirtualStudySteps, { virtualStudyId } from './VirtualStudy'; +import getGroupComparisonSteps, { groupComparisonId } from './GroupComparison'; + +export { + getVirtualStudySteps, + virtualStudyId, + getGroupComparisonSteps, + groupComparisonId +} diff --git a/src/tours/Tour/index.tsx b/src/tours/Tour/index.tsx new file mode 100644 index 00000000000..523bd679ba4 --- /dev/null +++ b/src/tours/Tour/index.tsx @@ -0,0 +1,143 @@ +import React, { useEffect, useState } from 'react'; +import Tour from 'reactour'; +import _ from 'lodash'; +import { + getGroupComparisonSteps, + getVirtualStudySteps, + groupComparisonId, + virtualStudyId, +} from '../Steps'; +import { TourProps, TourMapProps } from './types'; +import './styles.css'; + +const TOUR_LOCAL_STORAGE_ID = 'web-tour'; +export const setTourLocalStorage = (id: string, value: string) => { + localStorage.setItem(TOUR_LOCAL_STORAGE_ID, id); + localStorage.setItem(id, value); +}; + +export const setTourLocalStorageFromURL = () => { + /** + * If the url contains a query param 'web-tour', set the localStorage + * + * e.g. https://www.cbioportal.org/?web-tour=virtual-study-tour + * https://deploy-preview-4687--cbioportalfrontend.netlify.app/?web-tour=virtual-study-tour + */ + const urlStr = (window as any).routingStore.location.search; + if (urlStr) { + const urlSearchParams = new URLSearchParams(urlStr); + const webTour = urlSearchParams.get(TOUR_LOCAL_STORAGE_ID); + if (webTour && [groupComparisonId, virtualStudyId].includes(webTour)) { + setTourLocalStorage(webTour, '0'); + } + } +}; + +export default function WebTour({ + hideEntry = true, + isLoggedIn = false, + studies = 0, +}: TourProps) { + const [currentTour, setCurrentTour] = useState( + null + ); + const [isOpen, setIsOpen] = useState(true); + const [gotoStep, setGotoStep] = useState(null); + const [lockTour, setLockTour] = useState(false); + + const [startAt, setStartAt] = useState(0); + const endTour = () => setIsOpen(false); + const endTourWithBtn = (e: any) => { + e.preventDefault(); + e.stopPropagation(); + setIsOpen(false); + }; + + useEffect(() => { + /** + * Two sources to determine the current tour: + * 1. the tourType prop passed from the parent component + * 2. the localStorage (when load to another page) + */ + const tourContinued = localStorage.getItem(TOUR_LOCAL_STORAGE_ID); + if (tourContinued) { + localStorage.removeItem(TOUR_LOCAL_STORAGE_ID); + const currentStep = localStorage.getItem(tourContinued); + if (currentStep) { + localStorage.removeItem(tourContinued); + setCurrentTour(tourContinued); + setStartAt(+currentStep); + } + } + }, [currentTour]); + + const toursMap: TourMapProps = { + [virtualStudyId]: { + className: virtualStudyId + '-modal', + title: 'Create a Virtual Study', + getSteps: getVirtualStudySteps, + }, + [groupComparisonId]: { + className: groupComparisonId + '-modal', + title: 'Compare User-defined Groups of Samples', + getSteps: getGroupComparisonSteps, + }, + }; + + // click on the title to start the tour + const handleClick = (e: any) => { + const tourType = e.target.dataset.type; + setCurrentTour(tourType); + setStartAt(0); + setIsOpen(true); + }; + + return ( +
+ {!hideEntry && + _.map(toursMap, ({ title }, tourType) => ( +
+ {title} +
+ ))} + {currentTour && ( + + Finish guidance 🎉 +
+ } + prevButton={ +
+ Skip All +
+ } + nextButton={
Next Step
} + /> + )} +
+ ); +} diff --git a/src/tours/Tour/styles.css b/src/tours/Tour/styles.css new file mode 100644 index 00000000000..bd8292992fd --- /dev/null +++ b/src/tours/Tour/styles.css @@ -0,0 +1,55 @@ +.interactive-tour { + cursor: pointer; +} + +.reactour__helper { + box-shadow: 0 15px 20px rgba(0, 0, 0, 0.6); + padding: 20px 30px; + background: #f9fbf8; +} + +.reactour__helper .step .title { + font-size: 16px; + font-weight: bold; + margin-bottom: 10px; +} + +.reactour__helper .step p { + margin: 0; +} + +.reactour__helper .skip-all-btn, +.reactour__helper .next-step-btn, +.reactour__helper .finish-step-btn { + border: none; + border-radius: 10px; + font-size: 13px; + padding: 6px 10px; +} + +.reactour__helper .skip-all-btn:hover, +.reactour__helper .next-step-btn:hover, +.reactour__helper .finish-step-btn:hover { + opacity: 0.8; +} + +.reactour__helper .skip-all-btn { + background-color: #f0f0f4; + color: #666; +} + +.reactour__helper .next-step-btn, +.reactour__helper .finish-step-btn { + background-color: #e0f0e9; + color: #0c8918; +} + +.reactour__helper div:nth-child(2) { + justify-content: space-between; +} + +.reactour__helper .btn-class { + display: flex; + justify-content: space-between; + padding-top: 16px; +} diff --git a/src/tours/Tour/types.tsx b/src/tours/Tour/types.tsx new file mode 100644 index 00000000000..e807556525d --- /dev/null +++ b/src/tours/Tour/types.tsx @@ -0,0 +1,80 @@ +/** + * Represents a step of a tour. + * + * @param selector - CSS selector of the element to highlight. + * @param content - Content of the tour step to display. + * @param action - Function to execute when the step is reached. + */ +type Step = { + selector: string; + content: () => any; + action?: (node: Element) => void; +}; + +/** + * Defines the type of the steps of a tour. + */ +type Steps = Array; + +/** + * Represents a function to set the lock state of a tour. + * When the value is true, the tour is locked, and the user cannot go to the next step. + */ +type SetLockTour = (value: React.SetStateAction) => void; + +/** + * Represents a function to set the step to go to. + * For the goToStep function of the Tour component. + */ +type SetGotoStep = (value: React.SetStateAction) => void; + +/** + * The props of the getSteps function. + * + * @param isLoggedIn - Whether the user is logged in. + * @param studies - The number of selected studies or samples. + * @param setLockTour - Function to set the lock state of the tour. + * @param setGotoStep - Function to set the step to go to. + * @param endTour - Function to end the tour. + */ +type GetStepsProps = { + isLoggedIn: boolean; + studies: number; + setLockTour: SetLockTour; + setGotoStep: SetGotoStep; + endTour: () => void; +}; + +/** + * Represents a function to get the steps of a tour. + */ +type GetSteps = (props: GetStepsProps) => Steps; + +/** + * The props of the Tour component. + * + * @param hideEntry - Whether to hide the entry button that displays the name of the tour. + * @param isLoggedIn - Whether the user is logged in. + * @param studies - The number of selected studies or samples. + */ +type TourProps = { + hideEntry?: boolean; + isLoggedIn?: boolean; + studies?: number; +}; + +/** + * The props of the TourMap, which is a map of tours' titles and getSteps functions. + * + * @param title - The title of the tour. + * @param getSteps - The function to get the steps of the tour. + */ +type TourMapProps = { + [key: string]: { + title: string; + getSteps: any; + className: string; + }; +}; + +export { GetSteps, TourProps, TourMapProps }; diff --git a/src/tours/index.tsx b/src/tours/index.tsx new file mode 100644 index 00000000000..1fe561a6f46 --- /dev/null +++ b/src/tours/index.tsx @@ -0,0 +1,3 @@ +import Tour, { setTourLocalStorageFromURL } from './Tour'; + +export { Tour, setTourLocalStorageFromURL };