diff --git a/frontend/app/AdditionalRoutes.tsx b/frontend/app/AdditionalRoutes.tsx index 667ab957f..db93ff9c5 100644 --- a/frontend/app/AdditionalRoutes.tsx +++ b/frontend/app/AdditionalRoutes.tsx @@ -1,11 +1,17 @@ import React from 'react'; +import { Redirect } from 'react-router-dom'; - - -function AdditionalRoutes() { - return (<>) +interface Props { + redirect: string; } +const AdditionalRoutes = (props: Props) => { + const { redirect } = props; + return ( + <> + + + ); +}; - -export default AdditionalRoutes; \ No newline at end of file +export default AdditionalRoutes; diff --git a/frontend/app/IFrameRoutes.tsx b/frontend/app/IFrameRoutes.tsx new file mode 100644 index 000000000..bdaf1a774 --- /dev/null +++ b/frontend/app/IFrameRoutes.tsx @@ -0,0 +1,86 @@ +import React, { lazy, Suspense } from 'react'; +import { Switch, Route } from 'react-router-dom'; +import { connect } from 'react-redux'; +import { Loader } from 'UI'; +import withSiteIdUpdater from 'HOCs/withSiteIdUpdater'; + +import * as routes from './routes'; +import { GLOBAL_HAS_NO_RECORDINGS } from 'App/constants/storageKeys'; +import { Map } from 'immutable'; +import NotFoundPage from 'Shared/NotFoundPage'; +import { ModalProvider } from 'Components/Modal'; +import Layout from 'App/layout/Layout'; +import PublicRoutes from 'App/PublicRoutes'; + +const components: any = { + SessionPure: lazy(() => import('Components/Session/Session')), + LiveSessionPure: lazy(() => import('Components/Session/LiveSession')) +}; + + +const enhancedComponents: any = { + Session: withSiteIdUpdater(components.SessionPure), + LiveSession: withSiteIdUpdater(components.LiveSessionPure) +}; + +const withSiteId = routes.withSiteId; + +const SESSION_PATH = routes.session(); +const LIVE_SESSION_PATH = routes.liveSession(); + + +interface Props { + isEnterprise: boolean; + tenantId: string; + siteId: string; + jwt: string; + sites: Map; + onboarding: boolean; + isJwt?: boolean; + isLoggedIn?: boolean; + loading: boolean; +} + +function IFrameRoutes(props: Props) { + const { isJwt = false, isLoggedIn = false, loading, onboarding, sites, siteId, jwt } = props; + const siteIdList: any = sites.map(({ id }) => id).toJS(); + + if (isLoggedIn) { + return ( + + + + }> + + + + + + + + + + ); + } + + if (isJwt) { + return ; + } + + return ; +} + + +export default connect((state: any) => ({ + changePassword: state.getIn(['user', 'account', 'changePassword']), + onboarding: state.getIn(['user', 'onboarding']), + sites: state.getIn(['site', 'list']), + siteId: state.getIn(['site', 'siteId']), + jwt: state.getIn(['user', 'jwt']), + tenantId: state.getIn(['user', 'account', 'tenantId']), + isEnterprise: + state.getIn(['user', 'account', 'edition']) === 'ee' || + state.getIn(['user', 'authDetails', 'edition']) === 'ee' +}))(IFrameRoutes); \ No newline at end of file diff --git a/frontend/app/PrivateRoutes.tsx b/frontend/app/PrivateRoutes.tsx new file mode 100644 index 000000000..e39a90982 --- /dev/null +++ b/frontend/app/PrivateRoutes.tsx @@ -0,0 +1,194 @@ +import React, { lazy, Suspense } from 'react'; +import { Switch, Route, Redirect } from 'react-router-dom'; +import { connect } from 'react-redux'; +import { Loader } from 'UI'; +import withSiteIdUpdater from 'HOCs/withSiteIdUpdater'; + +import APIClient from './api_client'; +import * as routes from './routes'; +import { OB_DEFAULT_TAB } from 'App/routes'; +import { GLOBAL_HAS_NO_RECORDINGS } from 'App/constants/storageKeys'; +import { Map } from 'immutable'; +import AdditionalRoutes from 'App/AdditionalRoutes'; + +const components: any = { + SessionPure: lazy(() => import('Components/Session/Session')), + LiveSessionPure: lazy(() => import('Components/Session/LiveSession')), + OnboardingPure: lazy(() => import('Components/Onboarding/Onboarding')), + ClientPure: lazy(() => import('Components/Client/Client')), + AssistPure: lazy(() => import('Components/Assist/AssistRouter')), + SessionsOverviewPure: lazy(() => import('Components/Overview')), + DashboardPure: lazy(() => import('Components/Dashboard/NewDashboard')), + FunnelDetailsPure: lazy(() => import('Components/Funnels/FunnelDetails')), + FunnelIssueDetails: lazy(() => import('Components/Funnels/FunnelIssueDetails')), + FunnelPagePure: lazy(() => import('Components/Funnels/FunnelPage')), + MultiviewPure: lazy(() => import('Components/Session_/Multiview/Multiview')), + AssistStatsPure: lazy(() => import('Components/AssistStats')) +}; + + +const enhancedComponents: any = { + SessionsOverview: withSiteIdUpdater(components.SessionsOverviewPure), + Dashboard: withSiteIdUpdater(components.DashboardPure), + Session: withSiteIdUpdater(components.SessionPure), + LiveSession: withSiteIdUpdater(components.LiveSessionPure), + Assist: withSiteIdUpdater(components.AssistPure), + Client: withSiteIdUpdater(components.ClientPure), + Onboarding: withSiteIdUpdater(components.OnboardingPure), + FunnelPage: withSiteIdUpdater(components.FunnelPagePure), + FunnelsDetails: withSiteIdUpdater(components.FunnelDetailsPure), + FunnelIssue: withSiteIdUpdater(components.FunnelIssueDetails), + Multiview: withSiteIdUpdater(components.MultiviewPure), + AssistStats: withSiteIdUpdater(components.AssistStatsPure) +}; + +const withSiteId = routes.withSiteId; + +const METRICS_PATH = routes.metrics(); +const METRICS_DETAILS = routes.metricDetails(); +const METRICS_DETAILS_SUB = routes.metricDetailsSub(); + +const ALERTS_PATH = routes.alerts(); +const ALERT_CREATE_PATH = routes.alertCreate(); +const ALERT_EDIT_PATH = routes.alertEdit(); + +const DASHBOARD_PATH = routes.dashboard(); +const DASHBOARD_SELECT_PATH = routes.dashboardSelected(); +const DASHBOARD_METRIC_CREATE_PATH = routes.dashboardMetricCreate(); +const DASHBOARD_METRIC_DETAILS_PATH = routes.dashboardMetricDetails(); + +const SESSIONS_PATH = routes.sessions(); +const FFLAGS_PATH = routes.fflags(); +const FFLAG_PATH = routes.fflag(); +const FFLAG_CREATE_PATH = routes.newFFlag(); +const FFLAG_READ_PATH = routes.fflagRead(); +const NOTES_PATH = routes.notes(); +const BOOKMARKS_PATH = routes.bookmarks(); +const RECORDINGS_PATH = routes.recordings(); +const FUNNEL_PATH = routes.funnels(); +const FUNNEL_CREATE_PATH = routes.funnelsCreate(); +const FUNNEL_ISSUE_PATH = routes.funnelIssue(); +const SESSION_PATH = routes.session(); +const CLIENT_PATH = routes.client(); +const ONBOARDING_PATH = routes.onboarding(); +const ONBOARDING_REDIRECT_PATH = routes.onboarding(OB_DEFAULT_TAB); + +const ASSIST_PATH = routes.assist(); +const LIVE_SESSION_PATH = routes.liveSession(); +const MULTIVIEW_PATH = routes.multiview(); +const MULTIVIEW_INDEX_PATH = routes.multiviewIndex(); +const ASSIST_STATS_PATH = routes.assistStats(); + + +interface Props { + isEnterprise: boolean; + tenantId: string; + siteId: string; + jwt: string; + sites: Map; + onboarding: boolean; +} + +function PrivateRoutes(props: Props) { + const { onboarding, sites, siteId, jwt } = props; + const redirectToOnboarding = !onboarding && localStorage.getItem(GLOBAL_HAS_NO_RECORDINGS) === 'true'; + const siteIdList: any = sites.map(({ id }) => id).toJS(); + return ( + }> + + + + { + const client = new APIClient(); + switch (location.pathname) { + case '/integrations/slack': + client.post('integrations/slack/add', { + code: location.search.split('=')[1], + state: props.tenantId + }); + break; + case '/integrations/msteams': + client.post('integrations/msteams/add', { + code: location.search.split('=')[1], + state: props.tenantId + }); + break; + } + return ; + }} + /> + {redirectToOnboarding && } + + {/* DASHBOARD and Metrics */} + + + + + + + + + + + + + + + {Object.entries(routes.redirects).map(([fr, to]) => ( + + ))} + + + + ); +} + + +export default connect((state: any) => ({ + changePassword: state.getIn(['user', 'account', 'changePassword']), + onboarding: state.getIn(['user', 'onboarding']), + sites: state.getIn(['site', 'list']), + siteId: state.getIn(['site', 'siteId']), + jwt: state.getIn(['user', 'jwt']), + tenantId: state.getIn(['user', 'account', 'tenantId']), + isEnterprise: + state.getIn(['user', 'account', 'edition']) === 'ee' || + state.getIn(['user', 'authDetails', 'edition']) === 'ee' +}))(PrivateRoutes); \ No newline at end of file diff --git a/frontend/app/Router.tsx b/frontend/app/Router.tsx index 6398f4d76..d6fb8945c 100644 --- a/frontend/app/Router.tsx +++ b/frontend/app/Router.tsx @@ -1,366 +1,185 @@ -import React, {lazy, Suspense, useEffect, useRef} from 'react'; -import {Switch, Route, Redirect, withRouter, RouteComponentProps, BrowserRouter} from 'react-router-dom'; -import {connect, ConnectedProps} from 'react-redux'; -import {Notification} from 'UI'; -import {Loader} from 'UI'; -import {fetchUserInfo, setJwt} from 'Duck/user'; -import withSiteIdUpdater from 'HOCs/withSiteIdUpdater'; -import {fetchList as fetchSiteList} from 'Duck/site'; -import {withStore} from 'App/mstore'; -import {Map} from 'immutable'; +import React, { useEffect, useRef } from 'react'; +import { withRouter, RouteComponentProps } from 'react-router-dom'; +import { connect, ConnectedProps } from 'react-redux'; +import { Notification } from 'UI'; +import { Loader } from 'UI'; +import { fetchUserInfo, setJwt } from 'Duck/user'; +import { fetchList as fetchSiteList } from 'Duck/site'; +import { withStore } from 'App/mstore'; +import { Map } from 'immutable'; -import APIClient from './api_client'; import * as routes from './routes'; -import {OB_DEFAULT_TAB, isRoute} from 'App/routes'; -import {fetchTenants} from 'Duck/user'; -import {setSessionPath} from 'Duck/sessions'; -import {ModalProvider} from 'Components/Modal'; -import {GLOBAL_DESTINATION_PATH, GLOBAL_HAS_NO_RECORDINGS} from 'App/constants/storageKeys'; +import { fetchTenants } from 'Duck/user'; +import { setSessionPath } from 'Duck/sessions'; +import { ModalProvider } from 'Components/Modal'; +import { GLOBAL_DESTINATION_PATH, IFRAME, JWT_PARAM } from 'App/constants/storageKeys'; import PublicRoutes from 'App/PublicRoutes'; import Layout from 'App/layout/Layout'; - -const components = { - SessionPure: lazy(() => import('Components/Session/Session')), - LiveSessionPure: lazy(() => import('Components/Session/LiveSession')), - OnboardingPure: lazy(() => import('Components/Onboarding/Onboarding')), - ClientPure: lazy(() => import('Components/Client/Client')), - AssistPure: lazy(() => import('Components/Assist/AssistRouter')), - SessionsOverviewPure: lazy(() => import('Components/Overview')), - DashboardPure: lazy(() => import('Components/Dashboard/NewDashboard')), - FunnelDetailsPure: lazy(() => import('Components/Funnels/FunnelDetails')), - FunnelIssueDetails: lazy(() => import('Components/Funnels/FunnelIssueDetails')), - FunnelPagePure: lazy(() => import('Components/Funnels/FunnelPage')), - MultiviewPure: lazy(() => import('Components/Session_/Multiview/Multiview')), - AssistStatsPure: lazy(() => import('Components/AssistStats')), -}; - - -const enhancedComponents = { - SessionsOverview: withSiteIdUpdater(components.SessionsOverviewPure), - Dashboard: withSiteIdUpdater(components.DashboardPure), - Session: withSiteIdUpdater(components.SessionPure), - LiveSession: withSiteIdUpdater(components.LiveSessionPure), - Assist: withSiteIdUpdater(components.AssistPure), - Client: withSiteIdUpdater(components.ClientPure), - Onboarding: withSiteIdUpdater(components.OnboardingPure), - FunnelPage: withSiteIdUpdater(components.FunnelPagePure), - FunnelsDetails: withSiteIdUpdater(components.FunnelDetailsPure), - FunnelIssue: withSiteIdUpdater(components.FunnelIssueDetails), - Multiview: withSiteIdUpdater(components.MultiviewPure), - AssistStats: withSiteIdUpdater(components.AssistStatsPure) -}; - -const withSiteId = routes.withSiteId; - -const METRICS_PATH = routes.metrics(); -const METRICS_DETAILS = routes.metricDetails(); -const METRICS_DETAILS_SUB = routes.metricDetailsSub(); - -const ALERTS_PATH = routes.alerts(); -const ALERT_CREATE_PATH = routes.alertCreate(); -const ALERT_EDIT_PATH = routes.alertEdit(); - -const DASHBOARD_PATH = routes.dashboard(); -const DASHBOARD_SELECT_PATH = routes.dashboardSelected(); -const DASHBOARD_METRIC_CREATE_PATH = routes.dashboardMetricCreate(); -const DASHBOARD_METRIC_DETAILS_PATH = routes.dashboardMetricDetails(); - -const SESSIONS_PATH = routes.sessions(); -const FFLAGS_PATH = routes.fflags(); -const FFLAG_PATH = routes.fflag(); -const FFLAG_CREATE_PATH = routes.newFFlag(); -const FFLAG_READ_PATH = routes.fflagRead(); -const NOTES_PATH = routes.notes(); -const BOOKMARKS_PATH = routes.bookmarks(); -const RECORDINGS_PATH = routes.recordings(); -const FUNNEL_PATH = routes.funnels(); -const FUNNEL_CREATE_PATH = routes.funnelsCreate(); -const FUNNEL_ISSUE_PATH = routes.funnelIssue(); -const SESSION_PATH = routes.session(); -const CLIENT_PATH = routes.client(); -const ONBOARDING_PATH = routes.onboarding(); -const ONBOARDING_REDIRECT_PATH = routes.onboarding(OB_DEFAULT_TAB); - -const ASSIST_PATH = routes.assist(); -const LIVE_SESSION_PATH = routes.liveSession(); -const MULTIVIEW_PATH = routes.multiview(); -const MULTIVIEW_INDEX_PATH = routes.multiviewIndex(); -const ASSIST_STATS_PATH = routes.assistStats(); +import { fetchListActive as fetchMetadata } from 'Duck/customField'; +import { init as initSite } from 'Duck/site'; +import PrivateRoutes from 'App/PrivateRoutes'; +import { checkParam } from 'App/utils'; +import IFrameRoutes from 'App/IFrameRoutes'; interface RouterProps extends RouteComponentProps, ConnectedProps { - isLoggedIn: boolean; - jwt: string; - // siteId: number; - sites: Map; - loading: boolean; - changePassword: boolean; - onboarding: boolean; - isEnterprise: boolean; - fetchUserInfo: () => any; - fetchTenants: () => any; - setSessionPath: (path: any) => any; - fetchSiteList: (siteId?: number) => any; - match: { - params: { - siteId: string; - } - }; - mstore: any; - setJwt: (jwt: string) => any; - additionalRoutes?: React.ReactElement | null; + isLoggedIn: boolean; + sites: Map; + loading: boolean; + changePassword: boolean; + isEnterprise: boolean; + fetchUserInfo: () => any; + fetchTenants: () => any; + setSessionPath: (path: any) => any; + fetchSiteList: (siteId?: number) => any; + match: { + params: { + siteId: string; + } + }; + mstore: any; + setJwt: (jwt: string) => any; + additionalRoutes?: React.ReactElement | null; + fetchMetadata: (siteId: string) => void; + initSite: (site: any) => void; } const Router: React.FC = (props) => { + const { + isLoggedIn, + siteId, + sites, + loading, + location, + fetchUserInfo, + fetchSiteList, + history, + match: { params: { siteId: siteIdFromPath } }, + additionalRoutes = null + } = props; + const [isIframe, setIsIframe] = React.useState(false); + const [isJwt, setIsJwt] = React.useState(false); - const { - isLoggedIn, - jwt, - siteId, - sites, - loading, - location, - onboarding, - isEnterprise, - fetchUserInfo, - fetchTenants, - setSessionPath, - fetchSiteList, - history, - match: {params: {siteId: siteIdFromPath}}, - additionalRoutes = null - } = props; + const handleJwtFromUrl = () => { + const urlJWT = new URLSearchParams(location.search).get('jwt'); + if (urlJWT && !isLoggedIn) { + props.setJwt(urlJWT); + } + }; - const checkJWT = () => { - const urlJWT = new URLSearchParams(window.location.search).get('jwt'); - if (urlJWT && !props.isLoggedIn) { - props.setJwt(urlJWT); - } - }; - - useEffect(() => { - checkJWT(); - - const fetchInitialData = async () => { - const siteIdFromPath = parseInt(window.location.pathname.split('/')[1]); - await fetchUserInfo(); - await fetchSiteList(siteIdFromPath); - }; - - if (isLoggedIn) { - fetchInitialData().then(r => { - const {mstore} = props; - mstore.initClient(); - }); - } - }, []); - - useEffect(() => { - if (!location.pathname.includes('login')) { - localStorage.setItem(GLOBAL_DESTINATION_PATH, location.pathname); - } - }, [location]); - - function usePrevious(value: any) { - const ref = useRef(); - useEffect(() => { - ref.current = value; - }, [value]); - return ref.current; + const handleDestinationPath = () => { + if (!isLoggedIn && location.pathname !== routes.login()) { + localStorage.setItem(GLOBAL_DESTINATION_PATH, location.pathname); } - const prevEmail = usePrevious(props.email); - const prevIsLoggedIn = usePrevious(props.isLoggedIn); + setSessionPath(location); + }; + const handleUserLogin = async () => { + const destinationPath = localStorage.getItem(GLOBAL_DESTINATION_PATH); + if ( + destinationPath && + destinationPath !== routes.login() && + destinationPath !== '/' + ) { + history.push(destinationPath + location.search); + localStorage.removeItem(GLOBAL_DESTINATION_PATH); + } + await fetchUserInfo(); + const siteIdFromPath = parseInt(location.pathname.split('/')[1]); + await fetchSiteList(siteIdFromPath); + props.mstore.initClient(); + }; + + useEffect(() => { + setIsIframe(checkParam('iframe', IFRAME)); + setIsJwt(checkParam('jwt', JWT_PARAM)); + }, []); + + useEffect(() => { + handleJwtFromUrl(); + handleDestinationPath(); + }, [location]); + + useEffect(() => { + if (prevIsLoggedIn !== isLoggedIn && isLoggedIn) { + handleUserLogin(); + } + }, [isLoggedIn]); + + useEffect(() => { + if (siteId && siteId !== lastFetchedSiteIdRef.current) { + const activeSite = sites.find((s) => s.id == siteId); + props.initSite(activeSite); + props.fetchMetadata(siteId); + lastFetchedSiteIdRef.current = siteId; + } + }, [siteId]); + + const lastFetchedSiteIdRef = useRef(null); + + function usePrevious(value: any) { + const ref = useRef(); useEffect(() => { - setSessionPath(props.location); - const destinationPath = localStorage.getItem(GLOBAL_DESTINATION_PATH); + ref.current = value; + }, [value]); + return ref.current; + } - if (prevEmail !== props.email && !props.email) { - fetchTenants(); - } + const prevIsLoggedIn = usePrevious(isLoggedIn); - if ( - destinationPath && - !prevIsLoggedIn && - props.isLoggedIn && - destinationPath !== routes.login() && - destinationPath !== '/' - ) { - history.push(destinationPath + window.location.search); - } + const hideHeader = (location.pathname && location.pathname.includes('/session/')) || + location.pathname.includes('/assist/') || location.pathname.includes('multiview'); - if (!prevIsLoggedIn && props.isLoggedIn) { - const fetchInitialData = async () => { - await fetchUserInfo(); - await fetchSiteList(); - const {mstore} = props; - mstore.initClient(); - }; + if (isIframe) { + return ; + } - fetchInitialData(); - localStorage.removeItem(GLOBAL_DESTINATION_PATH); - } - }, [props.email, props.isLoggedIn, props.jwt]); - - const siteIdList = sites.map(({id}) => id).toJS(); - const hideHeader = - (location.pathname && location.pathname.includes('/session/')) || - location.pathname.includes('/assist/') || - location.pathname.includes('multiview'); - const hideMenu = hideHeader || location.pathname.includes('/onboarding/'); - - // const isPlayer = - // isRoute(SESSION_PATH, location.pathname) || - // isRoute(LIVE_SESSION_PATH, location.pathname) || - // isRoute(MULTIVIEW_PATH, location.pathname) || - // isRoute(MULTIVIEW_INDEX_PATH, location.pathname); - - const redirectToOnboarding = !onboarding && localStorage.getItem(GLOBAL_HAS_NO_RECORDINGS) === 'true'; - - return isLoggedIn ? ( - - - - - - }> - - - - { - const client = new APIClient(jwt); - switch (location.pathname) { - case '/integrations/slack': - client.post('integrations/slack/add', { - code: location.search.split('=')[1], - state: props.tenantId - }); - break; - case '/integrations/msteams': - client.post('integrations/msteams/add', { - code: location.search.split('=')[1], - state: props.tenantId - }); - break; - } - return ; - }} - /> - {redirectToOnboarding && } - - {/* DASHBOARD and Metrics */} - - - - - - - - - - - - - - {additionalRoutes && additionalRoutes} - - {Object.entries(routes.redirects).map(([fr, to]) => ( - - ))} - - - - - - - - ) : - ; + return isLoggedIn ? ( + + + + + + + + + ) : ; }; const mapStateToProps = (state: Map) => { - const siteId = state.getIn(['site', 'siteId']); - const jwt = state.getIn(['user', 'jwt']); - const changePassword = state.getIn(['user', 'account', 'changePassword']); - const userInfoLoading = state.getIn(['user', 'fetchUserInfoRequest', 'loading']); + const siteId = state.getIn(['site', 'siteId']); + const jwt = state.getIn(['user', 'jwt']); + const changePassword = state.getIn(['user', 'account', 'changePassword']); + const userInfoLoading = state.getIn(['user', 'fetchUserInfoRequest', 'loading']); - return { - jwt, - siteId, - changePassword, - sites: state.getIn(['site', 'list']), - isLoggedIn: jwt !== null && !changePassword, - loading: siteId === null || userInfoLoading, - email: state.getIn(['user', 'account', 'email']), - account: state.getIn(['user', 'account']), - organisation: state.getIn(['user', 'account', 'name']), - tenantId: state.getIn(['user', 'account', 'tenantId']), - tenants: state.getIn(['user', 'tenants']), - onboarding: state.getIn(['user', 'onboarding']), - isEnterprise: - state.getIn(['user', 'account', 'edition']) === 'ee' || - state.getIn(['user', 'authDetails', 'edition']) === 'ee' - }; + return { + siteId, + changePassword, + sites: state.getIn(['site', 'list']), + isLoggedIn: jwt !== null && !changePassword, + loading: siteId === null || userInfoLoading, + email: state.getIn(['user', 'account', 'email']), + account: state.getIn(['user', 'account']), + organisation: state.getIn(['user', 'account', 'name']), + tenantId: state.getIn(['user', 'account', 'tenantId']), + tenants: state.getIn(['user', 'tenants']), + isEnterprise: + state.getIn(['user', 'account', 'edition']) === 'ee' || + state.getIn(['user', 'authDetails', 'edition']) === 'ee' + }; }; - const mapDispatchToProps = { - fetchUserInfo, - fetchTenants, - setSessionPath, - fetchSiteList, - setJwt + fetchUserInfo, + fetchTenants, + setSessionPath, + fetchSiteList, + setJwt, + fetchMetadata, + initSite }; const connector = connect(mapStateToProps, mapDispatchToProps); -const RouterWithStore = withStore(withRouter(connector(Router))); - -interface AppProps { - additionalRoutes?: React.ReactElement | null; -} - -const App: React.FC = (props: AppProps) => ( - - - -); - -export default App; +export default withStore(withRouter(connector(Router))); diff --git a/frontend/app/components/Session/Player/ReplayPlayer/PlayerBlockHeader.tsx b/frontend/app/components/Session/Player/ReplayPlayer/PlayerBlockHeader.tsx index b4b11b2f1..9f4115e32 100644 --- a/frontend/app/components/Session/Player/ReplayPlayer/PlayerBlockHeader.tsx +++ b/frontend/app/components/Session/Player/ReplayPlayer/PlayerBlockHeader.tsx @@ -51,6 +51,7 @@ function PlayerBlockHeader(props: any) { }, []); const backHandler = () => { + history.goBack(); if ( sessionPath.pathname === history.location.pathname || sessionPath.pathname.includes('/session/') diff --git a/frontend/app/constants/storageKeys.ts b/frontend/app/constants/storageKeys.ts index b94d4f9bf..817d167af 100644 --- a/frontend/app/constants/storageKeys.ts +++ b/frontend/app/constants/storageKeys.ts @@ -9,4 +9,5 @@ export const SITE_ID_STORAGE_KEY = "__$user-siteId$__" export const GETTING_STARTED = "__$user-gettingStarted$__" export const MOUSE_TRAIL = "__$session-mouseTrail$__" export const IFRAME = "__$session-iframe$__" +export const JWT_PARAM = "__$session-jwt-param$__" export const MENU_COLLAPSED = "__$global-menuCollapsed$__" \ No newline at end of file diff --git a/frontend/app/initialize.tsx b/frontend/app/initialize.tsx index 7f40f8000..ada078585 100644 --- a/frontend/app/initialize.tsx +++ b/frontend/app/initialize.tsx @@ -11,6 +11,7 @@ import {DndProvider} from 'react-dnd'; import {ConfigProvider, theme, ThemeConfig} from 'antd'; import colors from 'App/theme/colors'; import AdditionalRoutes from './AdditionalRoutes'; +import { BrowserRouter } from 'react-router-dom'; // @ts-ignore window.getCommitHash = () => console.log(window.env.COMMIT_HASH); @@ -64,7 +65,9 @@ document.addEventListener('DOMContentLoaded', () => { - }/> + + }/> + diff --git a/frontend/app/layout/Layout.tsx b/frontend/app/layout/Layout.tsx index 88e066c46..a436b3d05 100644 --- a/frontend/app/layout/Layout.tsx +++ b/frontend/app/layout/Layout.tsx @@ -1,9 +1,13 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { Layout as AntLayout } from 'antd'; import SideMenu from 'App/layout/SideMenu'; import TopHeader from 'App/layout/TopHeader'; import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; +import { fetchListActive as fetchMetadata } from 'Duck/customField'; +import { init as initSite } from 'Duck/site'; +import { connect } from 'react-redux'; + const { Header, Sider, Content } = AntLayout; @@ -11,6 +15,9 @@ interface Props { children: React.ReactNode; hideHeader?: boolean; siteId?: string; + fetchMetadata: (siteId: string) => void; + initSite: (site: any) => void; + sites: any[]; } function Layout(props: Props) { @@ -18,13 +25,25 @@ function Layout(props: Props) { const isPlayer = /\/(session|assist)\//.test(window.location.pathname); const { settingsStore } = useStore(); + // const lastFetchedSiteIdRef = React.useRef(null); + // + // useEffect(() => { + // if (!siteId || siteId === lastFetchedSiteIdRef.current) return; + // + // const activeSite = props.sites.find((s) => s.id == siteId); + // props.initSite(activeSite); + // props.fetchMetadata(siteId); + // + // lastFetchedSiteIdRef.current = siteId; + // }, [siteId]); + return ( {!hideHeader && ( )} - {!hideHeader && !window.location.pathname.includes('/onboarding/') && ( + {!hideHeader && !window.location.pathname.includes('/onboarding/') && ( ({ + siteId: state.getIn(['site', 'siteId']), + sites: state.getIn(['site', 'list']) +}), { fetchMetadata, initSite })(observer(Layout)); diff --git a/frontend/app/layout/TopHeader.tsx b/frontend/app/layout/TopHeader.tsx index 681061e44..c973302fa 100644 --- a/frontend/app/layout/TopHeader.tsx +++ b/frontend/app/layout/TopHeader.tsx @@ -14,26 +14,18 @@ import { fetchListActive as fetchMetadata } from 'Duck/customField'; const { Header } = Layout; interface Props { - sites: any[]; account: any; siteId: string; - boardingCompletion?: number; - showAlerts?: boolean; - fetchMetadata: () => void; + fetchMetadata: (siteId: string) => void; initSite: (site: any) => void; } function TopHeader(props: Props) { const { settingsStore } = useStore(); - const { sites, account, siteId, boardingCompletion = 100, showAlerts = false } = props; - - const name = account.get('name'); - const [hideDiscover, setHideDiscover] = useState(false); + const { account, siteId } = props; const { userStore, notificationStore } = useStore(); const initialDataFetched = useObserver(() => userStore.initialDataFetched); - let activeSite = null; - const isPreferences = window.location.pathname.includes('/client/'); useEffect(() => { if (!account.id || initialDataFetched) return; @@ -42,18 +34,13 @@ function TopHeader(props: Props) { Promise.all([ userStore.fetchLimits(), notificationStore.fetchNotificationsCount() - // props.fetchMetadata() // TODO check for this + ]).then(() => { userStore.updateKey('initialDataFetched', true); }); }, 0); }, [account]); - useEffect(() => { - activeSite = sites.find((s) => s.id == siteId); - props.initSite(activeSite); - }, [siteId]); - return (
({ account: state.getIn(['user', 'account']), - siteId: state.getIn(['site', 'siteId']), - sites: state.getIn(['site', 'list']), - boardingCompletion: state.getIn(['dashboard', 'boardingCompletion']) + siteId: state.getIn(['site', 'siteId']) }); const mapDispatchToProps = { diff --git a/frontend/app/utils/index.ts b/frontend/app/utils/index.ts index 50314c956..5c95789e7 100644 --- a/frontend/app/utils/index.ts +++ b/frontend/app/utils/index.ts @@ -426,3 +426,41 @@ export function deleteCookie(name: string, path: string, domain: string) { (domain ? ';domain=' + domain : '') + ';expires=Thu, 01 Jan 1970 00:00:01 GMT'; } + +/** + * Checks if a specified query parameter exists in the URL and if its value is set to 'true'. + * If a storageKey is provided, stores the result in localStorage under that key. + * + * @function + * @param {string} paramName - The name of the URL parameter to check. + * @param {string} [storageKey] - The optional key to use for storing the result in localStorage. + * @returns {boolean} - Returns true if the parameter exists and its value is 'true'. Otherwise, returns false. + * + * @example + * // Assuming URL is: http://example.com/?iframe=true + * const isIframeEnabled = checkParam('iframe'); // Returns true, doesn't store in localStorage + * const isIframeEnabledWithStorage = checkParam('iframe', 'storageKey'); // Returns true, stores in localStorage + * + * @description + * The function inspects the current URL's query parameters. If the specified parameter exists + * and its value is set to 'true', and a storageKey is provided, the function stores 'true' under + * the provided storage key in the localStorage. If the condition is not met, or if the parameter + * does not exist, and a storageKey is provided, any existing localStorage entry with the storageKey + * is removed. + */ +export const checkParam = (paramName: string, storageKey?: string): boolean => { + const urlParams = new URLSearchParams(window.location.search); + const paramValue = urlParams.get(paramName); + + const existsAndTrue = paramValue && paramValue === 'true' || paramValue?.length > 0; + + if (storageKey) { + if (existsAndTrue) { + localStorage.setItem(storageKey, 'true'); + } else { + localStorage.removeItem(storageKey); + } + } + + return existsAndTrue; +}; \ No newline at end of file