App Tour

Here you will learn how app tour work.

Backend

Tour is the part of Users Admin module. Go there and you will find all the necessary methods. Main on lays in services. It's called tour and it updates the tourCompleted filed of Users entity.

    async tour(JwtUser: JwtUserPayload) {
        const user = await this.userRepository.findOne({ where: { id: JwtUser.id } });

        if (!user) {
            throw new HttpException(
                {
                    message: 'Error',
                    description: 'User not found',
                    code: 'USER_NOT_FOUND',
                },
                HttpStatus.BAD_REQUEST
            );
        }

        user.tourCompleted = !user.tourCompleted;

        return await this.userRepository.save(user);
    }

Frontend

Frontend part is more complicated. Switcher on User settings page only changes tourCompleted field to opposite. If you turn it on again - app will reload and you will start your app tour.

Well, how is the tour of the application arranged? The main executable file lies in your_project/ui/src/context/TourContext.js.

As you can already see, this is a context file. That is, the tour uses context. Why the context? Because that is how, from one place, we can control the tour in general and, in fact, its steps on each page.

Also an important place for the tour is an auxiliary file located along the path your_project/ui/src/utils/tourHelper.js.

In addition, the tour uses a local storage, which will store our tour steps.


Context file

Let's start with the context file.

The state that is associated with the localStorage:

    const [stepsShown, setStepsShown] = useState(() => {
        const saved = localStorage.getItem('visitedPages');
        if (saved) {
            const parsed = JSON.parse(saved);
            return { ...parsed.stepsShown };
        } else if (
            currentUser &&
            (currentUser.teamRole === teamRole.VIEWER || currentUser.teamRole === teamRole.MANAGER)
        ) {
            return { ...defaultStepsShown, settings: true, subscriptions: true };
            // not showing tour for viewer and manager on this pages
        }
        return defaultStepsShown;
    });

N.B. Can notice the use of roles. Certain roles have their own access, respectively, this must be taken into account in the tour.


The function that closes the tour. And if you remember the user settings, it similarly updates the tourCompleted field.

    const closeTour = async () => {
        setIsTourOpen(false);
        try {
            await updateUserTourStatus();
            localStorage.removeItem('visitedPages');
            await fetchUser();
        } catch (error) {
            console.error(error); // just logging the error
        }
    };

Then a little bit more interesting. Our steps, they all come from our helper file.

Constants:

    const allStepsShown = Object.values(stepsShown).every(value => value === true);

    const globalSteps = getGlobalSteps(t, stepsShown, setStepsShown, setIsTourOpen, closeTour, isSmallDevice);

    const pageSpecificSteps = getPageSteps(
        location.pathname,
        currentUser.previewLink,
        t,
        stepsShown,
        setStepsShown,
        setIsTourOpen,
        closeTour
    );

Tour Helper

They are then used in useEffects to initiate a tour on the pages. The steps themselves are an array with objects that include various elements of different ordinary text and various kinds of functions. For example:

export const getGlobalSteps = (t, stepsShown, setStepsShown, setIsTourOpen, closeTour, isSmallDevice) => {
    const stepButtonProps = getButtonProps(t);

    const close = () => {
        setIsTourOpen(false);
        setStepsShown(prev => ({ ...prev, global: true }));

        const updatedPages = new Set(Object.keys(stepsShown));
        localStorage.setItem('visitedPages', JSON.stringify([...updatedPages]));
    };


    
    return isSmallDevice 
        ? [
            
              {
                  title: t('tour:global-steps.user-menu.title'),
                  description: t('tour:global-steps.user-menu.description'),
                  target: () => document.querySelector('.tour-user-menu'),
                  ...stepButtonProps,
                  onClose: () => closeTour(),
                  nextButtonProps: {
                      onClick: () => close(),
                      style: {
                          ...buttonStyle.small,
                      },
                      children: t('tour:buttons.finish'),
                  },
              },
          ]

}

N.B. This example does not cover the entire volume of the function, but only an important part, otherwise it would take up a lot of space here.

Something similar you will find in getPageSteps. That function will show page specific part of tour/


Let's pay attention to the object of the tour, especially the target prop. Target is the place where the tour step is attached. This can be done either using a reference (useRef) or various attributes such as a className or id. In this case, we used classes. They are more flexible compared to the useRef that must be passed to the parameters of the component.

onClose: () => closeTour(),

This function is the function of closing the tour, that is, the "X" button (close).


...stepButtonProps,

This sets the parameters of the tour step buttons. But...

nextButtonProps: {
        onClick: () => close(),
              style: {
                      ...buttonStyle.small,
              },
              children: t('tour:buttons.finish'),
},

Here we change it a little, so an example of the last step of a certain part of the tour is indicated. For what reason? Because this is how AntD of the used version works.


How tour helper really helps us?

Returning to our context file, we are left to consider the useEffects that remained there. There's nothing complicated. They are just checking which parts of the tour we went through and which are still left to go through, which part of the tour to launch.

As everyone knows, the context envelops something. It envelops our entire application in the main executive file. We will not discuss separately the work of the context of the React.

        <TourContext.Provider value={value}>
            {children}
            <Tour
                open={isTourOpen}
                steps={steps}
                current={currentStep}
                onChange={setCurrentStep}
                arrow={false}
                disabledInteraction
            />
        </TourContext.Provider>

About the Tour component please read the official documentation separately about it.

Last updated