diff --git a/editor.planx.uk/src/components/EditorNavMenu.tsx b/editor.planx.uk/src/components/EditorNavMenu.tsx index bb821e1895..a3af14ef2e 100644 --- a/editor.planx.uk/src/components/EditorNavMenu.tsx +++ b/editor.planx.uk/src/components/EditorNavMenu.tsx @@ -13,8 +13,8 @@ import Tooltip, { tooltipClasses, TooltipProps } from "@mui/material/Tooltip"; import Typography from "@mui/material/Typography"; import { Role } from "@opensystemslab/planx-core/types"; import { useStore } from "pages/FlowEditor/lib/store"; -import React from "react"; -import { useCurrentRoute, useNavigation } from "react-navi"; +import React, { useRef } from "react"; +import { useCurrentRoute, useLoadingRoute, useNavigation } from "react-navi"; import { FONT_WEIGHT_SEMI_BOLD } from "theme"; import EditorIcon from "ui/icons/Editor"; @@ -26,6 +26,11 @@ interface Route { disabled?: boolean; } +interface RoutesForURL { + routes: Route[]; + compact: boolean; +} + const MENU_WIDTH_COMPACT = "51px"; const MENU_WIDTH_FULL = "164px"; @@ -104,6 +109,7 @@ const MenuButton = styled(IconButton, { function EditorNavMenu() { const { navigate } = useNavigation(); const { url } = useCurrentRoute(); + const isRouteLoading = useLoadingRoute(); const [teamSlug, flowSlug, user, canUserEditTeam, flowAnalyticsLink] = useStore((state) => [ state.teamSlug, @@ -225,14 +231,29 @@ function EditorNavMenu() { ...flowAnalyticsRoute, ]; - const getRoutesForUrl = ( - url: string, - ): { routes: Route[]; compact: boolean } => { - if (flowSlug && url.includes(flowSlug)) - return { routes: flowLayoutRoutes, compact: true }; - if (teamSlug && url.includes(teamSlug)) - return { routes: teamLayoutRoutes, compact: false }; - return { routes: globalLayoutRoutes, compact: false }; + const defaultRoutes: RoutesForURL = { + routes: globalLayoutRoutes, + compact: false, + }; + const previousRoutes = useRef(defaultRoutes); + + const getRoutesForUrl = (url: string): RoutesForURL => { + // Return the previous value when route is loading to avoid flash of incorrect version + if (isRouteLoading) return previousRoutes.current; + + let result: RoutesForURL; + + if (flowSlug && url.includes(flowSlug)) { + result = { routes: flowLayoutRoutes, compact: true }; + } else if (teamSlug && url.includes(teamSlug)) { + result = { routes: teamLayoutRoutes, compact: false }; + } else { + result = defaultRoutes; + } + + previousRoutes.current = result; + + return result; }; const { routes, compact } = getRoutesForUrl(url.href); diff --git a/editor.planx.uk/src/components/RouteLoadingIndicator.tsx b/editor.planx.uk/src/components/RouteLoadingIndicator.tsx new file mode 100644 index 0000000000..72c6852ce2 --- /dev/null +++ b/editor.planx.uk/src/components/RouteLoadingIndicator.tsx @@ -0,0 +1,36 @@ +import Box from "@mui/material/Box"; +import LinearProgress from "@mui/material/LinearProgress"; +import { styled } from "@mui/material/styles"; +import React, { useEffect, useState } from "react"; +import { useLoadingRoute } from "react-navi"; + +const Root = styled(Box)({ + width: "100%", + position: "fixed", + top: 0, + left: 0, +}); + +const RouteLoadingIndicator: React.FC<{ + msDelayBeforeVisible?: number; +}> = ({ msDelayBeforeVisible = 50 }) => { + const isLoading = useLoadingRoute(); + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + if (!isLoading) return setIsVisible(false); + + const timer = setTimeout(() => setIsVisible(true), msDelayBeforeVisible); + return () => clearTimeout(timer); + }, [isLoading, msDelayBeforeVisible]); + + if (!isVisible) return null; + + return ( + + + + ); +}; + +export default RouteLoadingIndicator; \ No newline at end of file diff --git a/editor.planx.uk/src/index.tsx b/editor.planx.uk/src/index.tsx index b2c4b6b3ce..7c8687f3ff 100644 --- a/editor.planx.uk/src/index.tsx +++ b/editor.planx.uk/src/index.tsx @@ -12,13 +12,12 @@ import ErrorPage from "pages/ErrorPage"; import { AnalyticsProvider } from "pages/FlowEditor/lib/analytics/provider"; import React, { Suspense, useEffect } from "react"; import { createRoot } from "react-dom/client"; -import { NotFoundBoundary, Router, useLoadingRoute, View } from "react-navi"; +import { NotFoundBoundary, Router, View } from "react-navi"; import HelmetProvider from "react-navi-helmet-async"; import { ToastContainer } from "react-toastify"; // init airbrake before everything else import * as airbrake from "./airbrake"; -import DelayedLoadingIndicator from "./components/DelayedLoadingIndicator"; import { client } from "./lib/graphql"; import navigation from "./lib/navigation"; import { defaultTheme } from "./theme"; @@ -55,8 +54,6 @@ const hasJWT = (): boolean | void => { const Layout: React.FC<{ children: React.ReactNode; }> = ({ children }) => { - const isLoading = useLoadingRoute(); - useEffect(() => { const observer = new MutationObserver(() => { // set the page title based on whatever heading is currently shown @@ -82,11 +79,7 @@ const Layout: React.FC<{ }> - {isLoading ? ( - - ) : ( - children - )} + {children} diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Settings/DataManagerSettings.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Settings/DataManagerSettings.tsx deleted file mode 100644 index c5899d05e2..0000000000 --- a/editor.planx.uk/src/pages/FlowEditor/components/Settings/DataManagerSettings.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import Container from "@mui/material/Container"; -import Typography from "@mui/material/Typography"; -import React from "react"; -import { FeaturePlaceholder } from "ui/editor/FeaturePlaceholder"; -import SettingsSection from "ui/editor/SettingsSection"; - -const DataManagerSettings: React.FC = () => { - return ( - - - - Data Manager - - - Manage the data that your service uses and makes available via its - API. - - - - - - - ); -}; -export default DataManagerSettings; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Settings/ServiceFlags.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Settings/ServiceFlags.tsx deleted file mode 100644 index d8165924c1..0000000000 --- a/editor.planx.uk/src/pages/FlowEditor/components/Settings/ServiceFlags.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import Container from "@mui/material/Container"; -import Typography from "@mui/material/Typography"; -import React from "react"; -import { FeaturePlaceholder } from "ui/editor/FeaturePlaceholder"; -import SettingsSection from "ui/editor/SettingsSection"; - -const ServiceFlags: React.FC = () => { - return ( - - - - Service flags - - - Manage the flag sets that this service uses. Flags at the top of a set - override flags below. - - - - - - - ); -}; -export default ServiceFlags; diff --git a/editor.planx.uk/src/pages/FlowEditor/index.tsx b/editor.planx.uk/src/pages/FlowEditor/index.tsx index cf55d4806b..4dd0e5a36c 100644 --- a/editor.planx.uk/src/pages/FlowEditor/index.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/index.tsx @@ -4,6 +4,7 @@ import Box from "@mui/material/Box"; import { styled } from "@mui/material/styles"; import { HEADER_HEIGHT_EDITOR } from "components/Header"; import React, { useRef } from "react"; +import { useCurrentRoute } from "react-navi"; import Flow from "./components/Flow"; import Sidebar from "./components/Sidebar"; @@ -18,7 +19,9 @@ const EditorContainer = styled(Box)(() => ({ maxHeight: `calc(100vh - ${HEADER_HEIGHT_EDITOR}px)`, })); -const FlowEditor: React.FC = ({ flow, breadcrumbs }) => { +const FlowEditor = () => { + const [ flow, ...breadcrumbs ] = useCurrentRoute().url.pathname.split("/").at(-1)?.split(",") || []; + const scrollContainerRef = useRef(null); useScrollControlsAndRememberPosition(scrollContainerRef); const showSidebar = useStore((state) => state.showSidebar); diff --git a/editor.planx.uk/src/pages/Login.tsx b/editor.planx.uk/src/pages/Login.tsx index d4fe9e22c7..dce5378a3c 100644 --- a/editor.planx.uk/src/pages/Login.tsx +++ b/editor.planx.uk/src/pages/Login.tsx @@ -5,7 +5,9 @@ import Button from "@mui/material/Button"; import Container from "@mui/material/Container"; import { styled } from "@mui/material/styles"; import Typography from "@mui/material/Typography"; +import DelayedLoadingIndicator from "components/DelayedLoadingIndicator"; import React from "react"; +import { useLoadingRoute } from "react-navi"; const Wrapper = styled(Box)(({ theme }) => ({ width: "100vw", @@ -46,6 +48,9 @@ const LoginButton = styled(Button)(({ theme }) => ({ })); const Login: React.FC = () => { + const isLoading = useLoadingRoute(); + if (isLoading) return ; + return ( diff --git a/editor.planx.uk/src/pages/layout/AuthenticatedLayout.tsx b/editor.planx.uk/src/pages/layout/AuthenticatedLayout.tsx index 437ddee99b..6f93190696 100644 --- a/editor.planx.uk/src/pages/layout/AuthenticatedLayout.tsx +++ b/editor.planx.uk/src/pages/layout/AuthenticatedLayout.tsx @@ -3,6 +3,7 @@ import { containerClasses } from "@mui/material/Container"; import { styled } from "@mui/material/styles"; import EditorNavMenu from "components/EditorNavMenu"; import { HEADER_HEIGHT_EDITOR } from "components/Header"; +import RouteLoadingIndicator from "components/RouteLoadingIndicator"; import React, { PropsWithChildren } from "react"; import { DndProvider } from "react-dnd"; import { HTML5Backend } from "react-dnd-html5-backend"; @@ -36,6 +37,7 @@ const DashboardContainer = styled(Box)(({ theme }) => ({ const Layout: React.FC = ({ children }) => ( <> +
diff --git a/editor.planx.uk/src/pages/layout/FlowEditorLayout.tsx b/editor.planx.uk/src/pages/layout/FlowEditorLayout.tsx index bb66ae8e20..327d8da975 100644 --- a/editor.planx.uk/src/pages/layout/FlowEditorLayout.tsx +++ b/editor.planx.uk/src/pages/layout/FlowEditorLayout.tsx @@ -1,9 +1,13 @@ import ErrorFallback from "components/ErrorFallback"; +import FlowEditor from "pages/FlowEditor"; import React, { PropsWithChildren } from "react"; import { ErrorBoundary } from "react-error-boundary"; const FlowEditorLayout: React.FC = ({ children }) => ( - {children} + + + {children} + ); export default FlowEditorLayout; diff --git a/editor.planx.uk/src/routes/flow.tsx b/editor.planx.uk/src/routes/flow.tsx index 1bb284969a..bc089262cb 100644 --- a/editor.planx.uk/src/routes/flow.tsx +++ b/editor.planx.uk/src/routes/flow.tsx @@ -1,36 +1,25 @@ import { gql } from "@apollo/client"; -import { - ComponentType as TYPES, - FlowStatus, -} from "@opensystemslab/planx-core/types"; +import { ComponentType as TYPES } from "@opensystemslab/planx-core/types"; import natsort from "natsort"; import { compose, - lazy, map, Matcher, mount, - NaviRequest, redirect, route, withData, withView, } from "navi"; -import DataManagerSettings from "pages/FlowEditor/components/Settings/DataManagerSettings"; -import ServiceFlags from "pages/FlowEditor/components/Settings/ServiceFlags"; -import ServiceSettings from "pages/FlowEditor/components/Settings/ServiceSettings"; -import Submissions from "pages/FlowEditor/components/Settings/Submissions"; import mapAccum from "ramda/src/mapAccum"; import React from "react"; -import { View } from "react-navi"; import { client } from "../lib/graphql"; -import FlowEditor from "../pages/FlowEditor"; import components from "../pages/FlowEditor/components/forms"; import FormModal from "../pages/FlowEditor/components/forms/FormModal"; import { SLUGS } from "../pages/FlowEditor/data/types"; import { useStore } from "../pages/FlowEditor/lib/store"; -import type { Flow, FlowSettings } from "../types"; +import type { Flow } from "../types"; import { makeTitle } from "./utils"; import { flowEditorView } from "./views/flowEditor"; @@ -179,44 +168,6 @@ const nodeRoutes = mount({ "/:parent/nodes/:id/edit": editNode, }); -const SettingsContainer = () => ; - -interface GetFlowSettings { - flows: { - id: string; - settings: FlowSettings; - status: FlowStatus; - }[]; -} - -export const getFlowSettings = async (req: NaviRequest) => { - const { - data: { - flows: [{ settings, status }], - }, - } = await client.query({ - query: gql` - query GetFlow($slug: String!, $team_slug: String!) { - flows( - limit: 1 - where: { slug: { _eq: $slug }, team: { slug: { _eq: $team_slug } } } - ) { - id - settings - status - } - } - `, - variables: { - slug: req.params.flow, - team_slug: req.params.team, - }, - }); - - useStore.getState().setFlowSettings(settings); - useStore.getState().setFlowStatus(status); -}; - const routes = compose( withData((req) => ({ flow: req.params.flow.split(",")[0], @@ -228,73 +179,12 @@ const routes = compose( "/": route(async (req) => { return { title: makeTitle([req.params.team, req.params.flow].join("/")), - view: () => { - const [flow, ...breadcrumbs] = req.params.flow.split(","); - return ( - - ); - }, + // Default view of FlowEditor (single instance held in layout) + view: () => null, }; }), - "/feedback": lazy(() => import("./feedback")), - - "/nodes": compose( - withView((req) => { - const [flow, ...breadcrumbs] = req.params.flow.split(","); - return ( - <> - - - - ); - }), - nodeRoutes, - ), - - "/service": compose( - withView(SettingsContainer), - - route(async (req) => ({ - getData: await getFlowSettings(req), - title: makeTitle( - [req.params.team, req.params.flow, "service"].join("/"), - ), - view: ServiceSettings, - })), - ), - - "/service-flags": compose( - withView(SettingsContainer), - - route(async (req) => ({ - getData: await getFlowSettings(req), - title: makeTitle( - [req.params.team, req.params.flow, "service-flags"].join("/"), - ), - view: ServiceFlags, - })), - ), - - "/data": compose( - withView(SettingsContainer), - - route(async (req) => ({ - title: makeTitle([req.params.team, req.params.flow, "data"].join("/")), - view: DataManagerSettings, - })), - ), - - "/submissions-log": compose( - withView(SettingsContainer), - - route(async (req) => ({ - title: makeTitle( - [req.params.team, req.params.flow, "submissions-log"].join("/"), - ), - view: Submissions, - })), - ), + "/nodes": nodeRoutes, }), ); diff --git a/editor.planx.uk/src/routes/serviceSettings.tsx b/editor.planx.uk/src/routes/serviceSettings.tsx new file mode 100644 index 0000000000..ca4a16e8b5 --- /dev/null +++ b/editor.planx.uk/src/routes/serviceSettings.tsx @@ -0,0 +1,65 @@ +import { gql } from "@apollo/client"; +import { FlowStatus } from "@opensystemslab/planx-core/types"; +import { compose, mount, NaviRequest, route, withData } from "navi"; +import ServiceSettings from "pages/FlowEditor/components/Settings/ServiceSettings"; + +import { client } from "../lib/graphql"; +import { useStore } from "../pages/FlowEditor/lib/store"; +import type { FlowSettings } from "../types"; +import { makeTitle } from "./utils"; + +interface GetFlowSettings { + flows: { + id: string; + settings: FlowSettings; + status: FlowStatus; + }[]; +} + +export const getFlowSettings = async (req: NaviRequest) => { + const { + data: { + flows: [{ settings, status }], + }, + } = await client.query({ + query: gql` + query GetFlow($slug: String!, $team_slug: String!) { + flows( + limit: 1 + where: { slug: { _eq: $slug }, team: { slug: { _eq: $team_slug } } } + ) { + id + settings + status + } + } + `, + variables: { + slug: req.params.flow, + team_slug: req.params.team, + }, + }); + + useStore.getState().setFlowSettings(settings); + useStore.getState().setFlowStatus(status); +}; + +const serviceSettingsRoutes = compose( + withData((req) => ({ + mountpath: req.mountpath, + })), + + mount({ + "/": compose( + route(async (req) => ({ + getData: await getFlowSettings(req), + title: makeTitle( + [req.params.team, req.params.flow, "service"].join("/"), + ), + view: ServiceSettings, + })), + ), + }), +); + +export default serviceSettingsRoutes; diff --git a/editor.planx.uk/src/routes/submissionsLog.tsx b/editor.planx.uk/src/routes/submissionsLog.tsx new file mode 100644 index 0000000000..8d9bd29516 --- /dev/null +++ b/editor.planx.uk/src/routes/submissionsLog.tsx @@ -0,0 +1,23 @@ +import { compose, mount, route, withData } from "navi"; +import Submissions from "pages/FlowEditor/components/Settings/Submissions"; + +import { makeTitle } from "./utils"; + +const submissionsLogRoutes = compose( + withData((req) => ({ + mountpath: req.mountpath, + })), + + mount({ + "/": compose( + route(async (req) => ({ + title: makeTitle( + [req.params.team, req.params.flow, "submissions-log"].join("/"), + ), + view: Submissions, + })), + ), + }), +); + +export default submissionsLogRoutes; diff --git a/editor.planx.uk/src/routes/team.tsx b/editor.planx.uk/src/routes/team.tsx index ad89c46704..02e14e96b4 100644 --- a/editor.planx.uk/src/routes/team.tsx +++ b/editor.planx.uk/src/routes/team.tsx @@ -1,5 +1,5 @@ import gql from "graphql-tag"; -import { compose, lazy, mount, route, withData, withView } from "navi"; +import { compose, lazy, map, mount, route, withData, withView } from "navi"; import DesignSettings from "pages/FlowEditor/components/Settings/DesignSettings"; import GeneralSettings from "pages/FlowEditor/components/Settings/GeneralSettings"; import React from "react"; @@ -15,6 +15,13 @@ let cached: { flowSlug?: string; teamSlug?: string } = { teamSlug: undefined, }; +const setFlowAndLazyLoad = (importComponent: Parameters[0]) => { + return map(async (request) => { + useStore.getState().setFlowSlug(request.params.flow); + return lazy(importComponent); + }); +}; + const routes = compose( withData((req) => ({ team: req.params.team, @@ -74,6 +81,12 @@ const routes = compose( return import("./flow"); }), + "/:flow/feedback": setFlowAndLazyLoad(() => import("./feedback")), + + "/:flow/service": setFlowAndLazyLoad(() => import("./serviceSettings")), + + "/:flow/submissions-log": setFlowAndLazyLoad(() => import("./submissionsLog")), + "/members": lazy(() => import("./teamMembers")), "/design": compose( route(async (req) => ({