Skip to content

Commit

Permalink
i51: Deep Linking (#79)
Browse files Browse the repository at this point in the history
* get all information needed for requests without a body

* wip

* copy request information in hash

* get mvp working

* style and handle old browsers

* clean

* abstract out reused code

* handle badmagic mounted on routes other than root

* make link itself the copy button

* only encode necessary data

* fix: eslint config for lsp

* fix: load full route definition from workspaces

* fix: everything loading correctly and handle undefined base url

* fix: move workspace defaults to top level and handle broken link
  • Loading branch information
jollyjerr authored Sep 8, 2023
1 parent 147ffcb commit 8f0ae24
Show file tree
Hide file tree
Showing 12 changed files with 249 additions and 80 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ module.exports = {
"plugin:react/recommended",
"plugin:react-hooks/recommended",
],
ignorePatterns: [/dist/],
ignorePatterns: ["dist/**/*"],
rules: {
"@typescript-eslint/no-explicit-any": ["off"],
"@typescript-eslint/no-inferrable-types": ["off"],
Expand Down
2 changes: 1 addition & 1 deletion src/BadMagic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { BadMagicProps } from "./types";

export function BadMagic(props: BadMagicProps) {
return (
<ContextProvider>
<ContextProvider workspaces={props.workspaces}>
<Layout {...props} />
</ContextProvider>
);
Expand Down
16 changes: 5 additions & 11 deletions src/Route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import {
WorkspaceConfig,
AuthForm,
HistoryMetadata,
HistoricResponse,
} from "./types";
import Helpers from "./lib/helpers";

export default function Route({
route,
Expand All @@ -30,23 +30,17 @@ export default function Route({
}) {
const { historicResponses } = useGlobalContext();

// If `activeRoute` is specified, filter displayed History to records matching just that route
const filteredHistory = useMemo(() => {
return !route
? historicResponses
: historicResponses.filter(
(historicResponse: HistoricResponse) =>
historicResponse?.route?.path === route.path
);
}, [historicResponses, route]);
const filteredHistory = useMemo(
() => Helpers.filterHistory(historicResponses, route),
[historicResponses, route]
);

return (
<>
{AuthForm && workspaceConfig ? (
<AuthForm workspaceConfig={workspaceConfig} />
) : null}
<RequestResponse
filteredHistory={filteredHistory}
route={route}
applyAxiosInterceptors={applyAxiosInterceptors}
/>
Expand Down
26 changes: 2 additions & 24 deletions src/common/route/RequestResponse.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, { useCallback, useMemo } from "react";
import useAxios from "@smartrent/use-axios";
import axios from "axios";
import { first, cloneDeep } from "lodash-es";

import { useGlobalContext } from "../../context/GlobalContext";

Expand All @@ -17,43 +16,22 @@ import {
ApplyAxiosInterceptors,
HistoricResponse,
} from "../../types";
import { useActiveResponse } from "../../lib/activeResponse";

export function RequestResponse({
route,
applyAxiosInterceptors,
filteredHistory,
}: {
route: Route;
applyAxiosInterceptors?: ApplyAxiosInterceptors;
filteredHistory: HistoricResponse[];
}) {
const {
darkMode,
storeHistoricResponse,
setPartialRequestResponse,
partialRequestResponses,
} = useGlobalContext();

const requestResponse: HistoricResponse = useMemo(() => {
// Prefers in-memory state changes that already began since the session started
// Falls back to loading the last HistoricResponse from history if set
// Falls back to a new partial HistoricRepsonse if the first two conditions aren't met.
if (partialRequestResponses[route.path]) {
return partialRequestResponses[route.path];
} else if (filteredHistory.length) {
return cloneDeep(first(filteredHistory)) as HistoricResponse;
}

return {
metadata: {},
response: null,
error: null,
urlParams: {},
qsParams: helpers.reduceDefaultParamValues(route?.qsParams),
body: helpers.reduceDefaultParamValues(route?.body),
route,
};
}, [route, filteredHistory, partialRequestResponses]);
const requestResponse: HistoricResponse = useActiveResponse(route);

const setUrlParams = useCallback(
(urlParams: Record<string, any>) => {
Expand Down
16 changes: 14 additions & 2 deletions src/common/route/TopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import helpers from "../../lib/helpers";
import { useGlobalContext } from "../../context/GlobalContext";

import { Route } from "../../types";
import { useCopyCurrentRoute } from "../../lib/links";

export function TopBar({
route,
Expand All @@ -31,6 +32,8 @@ export function TopBar({
};
}, [darkMode]);

const { copy, copied } = useCopyCurrentRoute({ activeRoute: route });

if (!route) {
return null;
}
Expand All @@ -53,9 +56,18 @@ export function TopBar({
</div>
)}

<div className={`flex flex-grow-2 mr-auto ${styles.headerText}`}>
{pathWithQS}
<div
className={`flex flex-grow-2 mr-auto gap-3 items-center cursor-pointer ${styles.headerText}`}
>
<div
onClick={copy}
className="cursor-pointer rounded hover:bg-gray-500 hover:bg-opacity-25 p-1"
>
{pathWithQS}
</div>
{copied ? <div className="text-green-500 text-sm">Copied!</div> : null}
</div>

<div
className={`flex text-right ml-2 mr-1 items-center ${styles.headerText}`}
>
Expand Down
80 changes: 59 additions & 21 deletions src/context/GlobalContext.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import React, { useState, useCallback, useContext, useEffect } from "react";
import React, {
useState,
useCallback,
useContext,
useEffect,
useMemo,
} from "react";
import { getLinkedRouteFromUrl } from "../lib/links";

import * as storage from "../lib/storage";

Expand All @@ -9,9 +16,10 @@ const storageKeys = {
collapsedWorkspaces: "collapsed-workspaces",
};

import { HistoricResponse, Route } from "../types";
import { HistoricResponse, Route, Workspace } from "../types";

export const Context = React.createContext({
workspaces: [] as Workspace[],
darkMode: storage.get(storageKeys.darkMode),
setDarkMode: (darkMode: boolean) => {
// noop
Expand Down Expand Up @@ -48,7 +56,13 @@ export const Context = React.createContext({

export const useGlobalContext = () => useContext(Context);

export function ContextProvider({ children }: { children: React.ReactNode }) {
export function ContextProvider({
children,
workspaces,
}: {
children: React.ReactNode;
workspaces: Workspace[];
}) {
const [activeRoute, setActiveRoute] = useState<null | Route>(null);
const [keywords, setKeywords] = useState("");
const [collapsedWorkspaces, setCollapsedWorkspacesInState] = useState<
Expand Down Expand Up @@ -103,17 +117,6 @@ export function ContextProvider({ children }: { children: React.ReactNode }) {
HistoricResponse[]
>([]);

// On initial mount, this will fetch HistoricResponse from local storage once
useEffect(() => {
const historicResponsesFromStorage: HistoricResponse[] = storage.get(
storageKeys.historicResponses
);

if (historicResponsesFromStorage?.length) {
setHistoricResponseInState(historicResponsesFromStorage);
}
}, []);

const storeHistoricResponse = useCallback(
({
metadata,
Expand Down Expand Up @@ -156,19 +159,53 @@ export function ContextProvider({ children }: { children: React.ReactNode }) {
},
};

const newHistoricResponses = [newResponse, ...historicResponses].slice(
0,
100
); // prepend the new HistoricResponse, and ensure the array has a max of 100 cells
setHistoricResponseInState((responses) => {
// prepend the new HistoricResponse, and ensure the array has a max of 100 cells
const newHistoricResponses = [newResponse, ...responses].slice(0, 100);

storage.set(storageKeys.historicResponses, newHistoricResponses);
setHistoricResponseInState(newHistoricResponses);
storage.set(storageKeys.historicResponses, newHistoricResponses);

return newHistoricResponses;
});

return newResponse;
},
[historicResponses]
[]
);

const workspacesWithDefaults = useMemo(
() =>
workspaces.map((workspace) => ({
...workspace,
routes: workspace.routes.map((route) => ({
...route,
baseUrl: workspace.config.baseUrl || window.location.origin,
workspaceName: workspace.name,
})),
})),
[workspaces]
);

// On initial mount, this will fetch HistoricResponse from local storage
// and load any request that was deep linked
useEffect(() => {
const historicResponsesFromStorage: HistoricResponse[] = storage.get(
storageKeys.historicResponses
);
if (historicResponsesFromStorage?.length) {
setHistoricResponseInState(historicResponsesFromStorage);
}

const { route, historicResponse } = getLinkedRouteFromUrl({
workspaces: workspacesWithDefaults,
});

if (route && historicResponse) {
setActiveRoute(route);
storeHistoricResponse(historicResponse);
}
}, [storeHistoricResponse, workspacesWithDefaults]);

return (
<Context.Provider
value={{
Expand All @@ -186,6 +223,7 @@ export function ContextProvider({ children }: { children: React.ReactNode }) {
setKeywords,
collapsedWorkspaces,
setCollapsedWorkspaces,
workspaces: workspacesWithDefaults,
}}
>
{children}
Expand Down
8 changes: 6 additions & 2 deletions src/layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@ import { History } from "../common/History";
import { BadMagicProps } from "../types";

export function Layout({
workspaces,
AuthForm,
HistoryMetadata,
applyAxiosInterceptors,
}: BadMagicProps) {
const { darkMode, historicResponses, activeRoute } = useGlobalContext();
const {
darkMode,
historicResponses,
activeRoute,
workspaces,
} = useGlobalContext();
const [activeWorkspaceNames, setActiveWorkspaceNamesInState] = useState<
string[]
>([]);
Expand Down
24 changes: 8 additions & 16 deletions src/layout/SideBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,14 @@ export function SideBar({ workspaces }: { workspaces: Workspace[] }) {
return workspaces.map(({ routes, name, config }) => {
return {
name,
routes: routes
.filter(
({ name, path, deprecated }) =>
(!keywords ||
name.toLowerCase().includes(keywords.toLowerCase()) ||
path.toLowerCase().includes(keywords.toLowerCase())) &&
((hideDeprecatedRoutes && deprecated !== true) ||
!hideDeprecatedRoutes)
)
.map((route) => {
return {
...route,
baseUrl: config?.baseUrl || window.location.origin,
workspaceName: name,
};
}),
routes: routes.filter(
({ name, path, deprecated }) =>
(!keywords ||
name.toLowerCase().includes(keywords.toLowerCase()) ||
path.toLowerCase().includes(keywords.toLowerCase())) &&
((hideDeprecatedRoutes && deprecated !== true) ||
!hideDeprecatedRoutes)
),
};
});
}, [keywords, workspaces, hideDeprecatedRoutes]);
Expand Down
35 changes: 35 additions & 0 deletions src/lib/activeResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { cloneDeep, first } from "lodash-es";
import { useMemo } from "react";
import { Helpers } from "..";
import { useGlobalContext } from "../context/GlobalContext";
import { HistoricResponse, Route } from "../types";

export function useActiveResponse(activeRoute: Route): HistoricResponse {
const { historicResponses, partialRequestResponses } = useGlobalContext();

const filteredHistory = useMemo(
() => Helpers.filterHistory(historicResponses, activeRoute),
[historicResponses, activeRoute]
);

return useMemo(() => {
// Prefers in-memory state changes that already began since the session started
// Falls back to loading the last HistoricResponse from history if set
// Falls back to a new partial HistoricRepsonse if the first two conditions aren't met.
if (partialRequestResponses[activeRoute.path]) {
return partialRequestResponses[activeRoute.path];
} else if (filteredHistory.length) {
return cloneDeep(first(filteredHistory)) as HistoricResponse;
}

return {
metadata: {},
response: null,
error: null,
urlParams: {},
qsParams: Helpers.reduceDefaultParamValues(activeRoute?.qsParams),
body: Helpers.reduceDefaultParamValues(activeRoute?.body),
route: activeRoute,
};
}, [activeRoute, filteredHistory, partialRequestResponses]);
}
18 changes: 16 additions & 2 deletions src/lib/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { get, reduce, compact, map, startCase } from "lodash-es";
import { stringify } from "querystring";

import OpenApi from "./openapi";
import { Route, Workspace, Param } from "../types";
import { Route, Workspace, Param, HistoricResponse } from "../types";

// Given a Route, URL Params, and QSParams, returns a route's path with the QS params included
function buildPathWithQS({
Expand Down Expand Up @@ -70,6 +70,16 @@ const Helpers = {
}, {} as Record<string, any>);
},

/** If `activeRoute` is specified, filter displayed History to records matching just that route */
filterHistory(historicResponses: HistoricResponse[], route?: Route | null) {
return !route
? historicResponses
: historicResponses.filter(
(historicResponse: HistoricResponse) =>
historicResponse?.route?.path === route.path
);
},

buildUrl({
route,
urlParams,
Expand All @@ -79,7 +89,11 @@ const Helpers = {
urlParams: Record<string, any>;
qsParams?: Record<string, any>;
}) {
return `${route.baseUrl}${buildPathWithQS({ route, urlParams, qsParams })}`;
return `${route.baseUrl}${buildPathWithQS({
route,
urlParams,
qsParams,
})}`;
},

buildPathWithQS,
Expand Down
Loading

0 comments on commit 8f0ae24

Please sign in to comment.