From e91811ff236d6bb432231305210f9f4039c16b2a Mon Sep 17 00:00:00 2001 From: Ibrahim Date: Fri, 27 Sep 2024 12:58:39 -0400 Subject: [PATCH] updates --- extensions/default/src/ViewerLayout/index.tsx | 3 +- platform/app/public/config/default.js | 278 +++++++++++++++++- platform/app/src/App.tsx | 6 +- platform/app/src/routes/WorkList/WorkList.tsx | 4 +- .../src/components/Onboarding/Onboarding.css | 23 ++ .../src/components/Onboarding/Onboarding.tsx | 65 ++++ .../src/components/Onboarding/index.ts | 3 + platform/ui-next/src/components/index.ts | 2 + .../contextProviders/OnboardingProvider.tsx | 41 --- .../ui-next/src/contextProviders/index.ts | 2 - platform/ui-next/src/index.ts | 11 +- 11 files changed, 377 insertions(+), 61 deletions(-) create mode 100644 platform/ui-next/src/components/Onboarding/Onboarding.css create mode 100644 platform/ui-next/src/components/Onboarding/Onboarding.tsx create mode 100644 platform/ui-next/src/components/Onboarding/index.ts delete mode 100644 platform/ui-next/src/contextProviders/OnboardingProvider.tsx diff --git a/extensions/default/src/ViewerLayout/index.tsx b/extensions/default/src/ViewerLayout/index.tsx index d39d6e181..3125dd136 100644 --- a/extensions/default/src/ViewerLayout/index.tsx +++ b/extensions/default/src/ViewerLayout/index.tsx @@ -6,6 +6,7 @@ import { HangingProtocolService, CommandsManager } from '@ohif/core'; import { useAppConfig } from '@state'; import ViewerHeader from './ViewerHeader'; import SidePanelWithServices from '../Components/SidePanelWithServices'; +import { Onboarding } from '@ohif/ui-next'; function ViewerLayout({ // From Extension Module Params @@ -155,7 +156,7 @@ function ViewerLayout({ ) : null} - + ); diff --git a/platform/app/public/config/default.js b/platform/app/public/config/default.js index 42dc11282..d953e5b63 100644 --- a/platform/app/public/config/default.js +++ b/platform/app/public/config/default.js @@ -298,27 +298,97 @@ window.config = { steps: [ { id: 'scroll', - text: 'You can scroll through the images using the mouse wheel.', + text: 'You can scroll through the images using the mouse wheel or scrollbar.', attachTo: { - element: '.viewport-element', + element: '.scroll', on: 'left-start', }, + advanceOn: { + selector: '.cornerstone-viewport-element', + event: 'CORNERSTONE_STACK_VIEWPORT_SCROLL', + }, + /** + buttons: [ + { + text: 'Next', + action() { + this.next(); + }, + }, + { + text: 'Skip', + action() { + this.cancel(); + }, + }, + ], + */ }, { id: 'zoom', text: 'You can zoom the images using the right click.', attachTo: { element: '.viewport-element', - on: 'left-start', }, + advanceOn: { + selector: '.cornerstone-viewport-element', + event: 'CORNERSTONE_CAMERA_MODIFIED', + }, + /** + buttons: [ + { + text: 'Next', + action() { + this.next(); + }, + }, + { + text: 'Skip', + action() { + this.cancel(); + }, + }, + { + text: 'Back', + action() { + this.back(); + }, + }, + ], + */ }, { id: 'pan', text: 'You can pan the images using the middle click.', attachTo: { element: '.viewport-element', - on: 'left-start', }, + advanceOn: { + selector: '.cornerstone-viewport-element', + event: 'CORNERSTONE_TOOLS_MOUSE_DRAG', + }, + /** + buttons: [ + { + text: 'Next', + action() { + this.next(); + }, + }, + { + text: 'Skip', + action() { + this.cancel(); + }, + }, + { + text: 'Back', + action() { + this.back(); + }, + }, + ], + */ }, { id: 'windowing', @@ -327,14 +397,58 @@ window.config = { element: '.viewport-element', on: 'left-start', }, + /** + buttons: [ + { + text: 'Next', + action() { + this.next(); + }, + }, + { + text: 'Skip', + action() { + this.cancel(); + }, + }, + { + text: 'Back', + action() { + this.back(); + }, + }, + ], + */ }, { id: 'length', text: 'You can measure the length of a region using the Length tool.', attachTo: { - element: '.MeasurementTools-split-button-primary', + element: '[data-cy="MeasurementTools-split-button-primary"]', on: 'bottom', }, + /** + buttons: [ + { + text: 'Next', + action() { + this.next(); + }, + }, + { + text: 'Skip', + action() { + this.cancel(); + }, + }, + { + text: 'Back', + action() { + this.back(); + }, + }, + ], + */ }, { id: 'drawAnnotation', @@ -343,6 +457,28 @@ window.config = { element: '.viewport-element', on: 'left-start', }, + /** + buttons: [ + { + text: 'Next', + action() { + this.next(); + }, + }, + { + text: 'Skip', + action() { + this.cancel(); + }, + }, + { + text: 'Back', + action() { + this.back(); + }, + }, + ], + */ }, { id: 'trackMeasurement', @@ -351,6 +487,28 @@ window.config = { element: '[data-cy="prompt-begin-tracking-yes-btn"]', on: 'bottom', }, + /** + buttons: [ + { + text: 'Next', + action() { + this.next(); + }, + }, + { + text: 'Skip', + action() { + this.cancel(); + }, + }, + { + text: 'Back', + action() { + this.back(); + }, + }, + ], + */ }, { id: 'openMeasurementPanel', @@ -359,6 +517,28 @@ window.config = { element: '#trackedMeasurements-btn', on: 'left-start', }, + /** + buttons: [ + { + text: 'Next', + action() { + this.next(); + }, + }, + { + text: 'Skip', + action() { + this.cancel(); + }, + }, + { + text: 'Back', + action() { + this.back(); + }, + }, + ], + */ }, { id: 'scrollAwayFromMeasurement', @@ -367,6 +547,28 @@ window.config = { element: '.viewport-element', on: 'left-start', }, + /** + buttons: [ + { + text: 'Next', + action() { + this.next(); + }, + }, + { + text: 'Skip', + action() { + this.cancel(); + }, + }, + { + text: 'Back', + action() { + this.back(); + }, + }, + ], + */ }, { id: 'jumpToMeasurement', @@ -375,6 +577,28 @@ window.config = { element: '[data-cy="measurement-item"]', on: 'left-start', }, + /** + buttons: [ + { + text: 'Next', + action() { + this.next(); + }, + }, + { + text: 'Skip', + action() { + this.cancel(); + }, + }, + { + text: 'Back', + action() { + this.back(); + }, + }, + ], + */ }, { id: 'changeLayout', @@ -383,6 +607,28 @@ window.config = { element: '[data-cy="Layout"]', on: 'bottom', }, + /** + buttons: [ + { + text: 'Next', + action() { + this.next(); + }, + }, + { + text: 'Skip', + action() { + this.cancel(); + }, + }, + { + text: 'Back', + action() { + this.back(); + }, + }, + ], + */ }, { id: 'selectLayout', @@ -391,6 +637,28 @@ window.config = { element: '[data-cy="MPR"]', on: 'left-start', }, + /** + buttons: [ + { + text: 'Complete', + action() { + this.complete(); + }, + }, + { + text: 'Skip', + action() { + this.cancel(); + }, + }, + { + text: 'Back', + action() { + this.back(); + }, + }, + ], + */ }, ], tourOptions: {}, diff --git a/platform/app/src/App.tsx b/platform/app/src/App.tsx index cb5ff53dc..23690d612 100644 --- a/platform/app/src/App.tsx +++ b/platform/app/src/App.tsx @@ -24,13 +24,14 @@ import { UserAuthenticationProvider, ToolboxProvider, } from '@ohif/ui'; -import { ThemeWrapper as ThemeWrapperNext, OnboardingProvider } from '@ohif/ui-next'; +import { ThemeWrapper as ThemeWrapperNext } from '@ohif/ui-next'; // Viewer Project // TODO: Should this influence study list? import { AppConfigProvider } from '@state'; import createRoutes from './routes'; import appInit from './appInit.js'; import OpenIdConnectRoutes from './utils/OpenIdConnectRoutes'; +import { ShepherdJourneyProvider } from 'react-shepherd'; let commandsManager: CommandsManager, extensionManager: ExtensionManager, @@ -99,7 +100,6 @@ function App({ cineService, userAuthenticationService, customizationService, - onboardingService, } = servicesManager.services; const providers = [ @@ -116,7 +116,7 @@ function App({ [SnackbarProvider, { service: uiNotificationService }], [DialogProvider, { service: uiDialogService }], [ModalProvider, { service: uiModalService, modal: Modal }], - [OnboardingProvider], + [ShepherdJourneyProvider], ]; // Loop through and register each of the service providers registered with the ServiceProvidersManager. diff --git a/platform/app/src/routes/WorkList/WorkList.tsx b/platform/app/src/routes/WorkList/WorkList.tsx index 01c39509e..785810ca3 100644 --- a/platform/app/src/routes/WorkList/WorkList.tsx +++ b/platform/app/src/routes/WorkList/WorkList.tsx @@ -34,6 +34,7 @@ import { import { Types } from '@ohif/ui'; import i18n from '@ohif/i18n'; +import { Onboarding } from '@ohif/ui-next'; const PatientInfoVisibility = Types.PatientInfoVisibility; @@ -411,7 +412,7 @@ function WorkList({ disabled={!isValidMode} startIconTooltip={ !isValidMode ? ( -
+
{invalidModeDescription}
) : null @@ -534,6 +535,7 @@ function WorkList({ WhiteLabeling={appConfig.whiteLabeling} showPatientInfo={PatientInfoVisibility.DISABLED} /> +
.shepherd-arrow { + right: -5px !important; +} diff --git a/platform/ui-next/src/components/Onboarding/Onboarding.tsx b/platform/ui-next/src/components/Onboarding/Onboarding.tsx new file mode 100644 index 000000000..415ffff90 --- /dev/null +++ b/platform/ui-next/src/components/Onboarding/Onboarding.tsx @@ -0,0 +1,65 @@ +import { useEffect } from 'react'; +import { useShepherd } from 'react-shepherd'; +import { StepOptions, TourOptions } from 'shepherd.js'; +import { useLocation } from 'react-router'; +import 'shepherd.js/dist/css/shepherd.css'; +import './Onboarding.css'; + +const hasTourBeenShown = (tourId: string) => { + const shownTours = JSON.parse(localStorage.getItem('shownTours')) || []; + return shownTours.includes(tourId); +}; +const markTourAsShown = (tourId: string) => { + const shownTours = JSON.parse(localStorage.getItem('shownTours')) || []; + if (!shownTours.includes(tourId)) { + shownTours.push(tourId); + localStorage.setItem('shownTours', JSON.stringify(shownTours)); + } +}; + +const Onboarding = () => { + const Shepherd = useShepherd(); + const location = useLocation(); + const tours = window.config.tours as Array<{ + id: string; + route: string; + tourOptions: TourOptions; + steps: StepOptions[]; + }>; + useEffect(() => { + if (!tours) { + return null; + } + + const pathname = location.pathname; + const matchingTour = tours.find(tour => tour.route === pathname); + + if (matchingTour) { + const startTour = () => { + const tourInstance = new Shepherd.Tour(matchingTour.tourOptions); + matchingTour.steps.forEach(step => tourInstance.addStep(step)); + + const observer = new MutationObserver(() => { + const firstStepElement = document.querySelector( + matchingTour.steps[0].attachTo.element as string + ); + if (firstStepElement) { + observer.disconnect(); + tourInstance.start(); + markTourAsShown(matchingTour.id); + } + }); + + if (!hasTourBeenShown(matchingTour.id)) { + observer.observe(document.body, { childList: true, subtree: true }); + setTimeout(() => observer.disconnect(), 5000); + } + }; + + startTour(); + } + }, [Shepherd, tours, location.pathname]); + return null; +}; + +export { Onboarding }; diff --git a/platform/ui-next/src/components/Onboarding/index.ts b/platform/ui-next/src/components/Onboarding/index.ts new file mode 100644 index 000000000..459553f0b --- /dev/null +++ b/platform/ui-next/src/components/Onboarding/index.ts @@ -0,0 +1,3 @@ +import { Onboarding } from './Onboarding'; + +export { Onboarding }; diff --git a/platform/ui-next/src/components/index.ts b/platform/ui-next/src/components/index.ts index c8951c2d7..b7dedaafd 100644 --- a/platform/ui-next/src/components/index.ts +++ b/platform/ui-next/src/components/index.ts @@ -28,6 +28,7 @@ import ThumbnailList from './ThumbnailList'; import PanelSection from './PanelSection'; import DisplaySetMessageListTooltip from './DisplaySetMessageListTooltip'; import { Toolbox, ToolboxUI } from './Toolbox'; +import { Onboarding } from './Onboarding'; export { Button, @@ -71,4 +72,5 @@ export { DisplaySetMessageListTooltip, Toolbox, ToolboxUI, + Onboarding, }; diff --git a/platform/ui-next/src/contextProviders/OnboardingProvider.tsx b/platform/ui-next/src/contextProviders/OnboardingProvider.tsx deleted file mode 100644 index b756151a6..000000000 --- a/platform/ui-next/src/contextProviders/OnboardingProvider.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React, { createContext, useContext } from 'react'; -import { ShepherdJourneyProvider, useShepherd } from 'react-shepherd'; -import { StepOptions, TourOptions } from 'shepherd.js'; -import 'shepherd.js/dist/css/shepherd.css'; - -const OnboardingProviderContext = createContext(null); -const { Provider } = OnboardingProviderContext; - -export const useOnboardingProvider = () => useContext(OnboardingProviderContext); - -const ShepherdController = ({ - tours, -}: { - tours: Array<{ - tourOptions: TourOptions; - steps: StepOptions[]; - route: string; - id: string; - }>; -}) => { - const Shepherd = useShepherd(); - - return null; -}; - -const OnboardingProvider = ({ children }: { children: React.ReactNode }) => { - const tours = window?.config?.tours; - if (!tours) { - return <>{children}; - } - return ( - - - - {children} - - - ); -}; - -export default OnboardingProvider; diff --git a/platform/ui-next/src/contextProviders/index.ts b/platform/ui-next/src/contextProviders/index.ts index 09c42fe40..57d3d90ec 100644 --- a/platform/ui-next/src/contextProviders/index.ts +++ b/platform/ui-next/src/contextProviders/index.ts @@ -1,5 +1,3 @@ import NotificationProvider, { useNotification } from './NotificationProvider'; -import OnboardingProvider, { useOnboardingProvider } from './OnboardingProvider'; export { useNotification, NotificationProvider }; -export { useOnboardingProvider, OnboardingProvider }; diff --git a/platform/ui-next/src/index.ts b/platform/ui-next/src/index.ts index 85ac618d7..880b3c11f 100644 --- a/platform/ui-next/src/index.ts +++ b/platform/ui-next/src/index.ts @@ -33,14 +33,10 @@ import { DisplaySetMessageListTooltip, Toolbox, ToolboxUI, + Onboarding, } from './components'; -import { - useNotification, - NotificationProvider, - OnboardingProvider, - useOnboardingProvider, -} from './contextProviders'; +import { useNotification, NotificationProvider } from './contextProviders'; export { // components @@ -81,6 +77,5 @@ export { DisplaySetMessageListTooltip, Toolbox, ToolboxUI, - OnboardingProvider, - useOnboardingProvider, + Onboarding, };