- History ({filteredHistory.length})
+
+
+ History ({filteredHistory.length})
+
+
{!filteredHistory.length ? (
diff --git a/src/common/Success.tsx b/src/common/Success.tsx
index 5251bb8..ad8d759 100644
--- a/src/common/Success.tsx
+++ b/src/common/Success.tsx
@@ -2,7 +2,7 @@ import React, { PropsWithChildren } from "react";
import Helpers from "../lib/helpers";
-const Success: React.FC = ({ children }: PropsWithChildren<{}>) => {
+const Success: React.FC = ({ children }: PropsWithChildren) => {
return children ? (
{children}
diff --git a/src/common/route/AddCustomParam.tsx b/src/common/route/AddCustomParam.tsx
index f5d0dd8..2cc515f 100644
--- a/src/common/route/AddCustomParam.tsx
+++ b/src/common/route/AddCustomParam.tsx
@@ -49,7 +49,7 @@ export function AddCustomParam({
setParam(paramType === "qsParams" ? { name: "", type: "text" } : null);
setIsVisible(false);
}
- }, [customParams, param, paramType]);
+ }, [customParams, param, paramType, setCustomParams]);
if (!isVisible) {
return (
@@ -70,7 +70,7 @@ export function AddCustomParam({
value=""
label="Select Param Type"
onChange={(type: string) => {
- let newParam: Param = { name: "" };
+ const newParam: Param = { name: "" };
if (type == "datetime") {
// Using this placeholder will trigger the `Now` assist button to popup
newParam.placeholder = new Date().toISOString();
diff --git a/src/common/route/ApiError.tsx b/src/common/route/ApiError.tsx
index 1e01b82..49b72c1 100644
--- a/src/common/route/ApiError.tsx
+++ b/src/common/route/ApiError.tsx
@@ -1,9 +1,7 @@
-import React, { useMemo } from "react";
+import React from "react";
import { isObject } from "lodash-es";
import ReactJson from "react-json-view";
-import { ApiResponseStatus } from "./ApiResponseStatus";
-
import { useGlobalContext } from "../../context/GlobalContext";
import Helpers from "../../lib/helpers";
@@ -11,29 +9,11 @@ import { ApiError } from "../../types";
export default function ApiError({ error }: { error: null | ApiError }) {
const { darkMode } = useGlobalContext();
- const styles = useMemo(() => {
- return {
- responseStatus: darkMode
- ? "bg-gray-800 border-gray-900"
- : "bg-gray-200 border-gray-400",
- textColor: darkMode ? "text-white" : "",
- };
- }, [darkMode]);
+
if (!error?.response) {
return null;
}
- let responseColor;
- if (
- error?.response?.status &&
- error.response.status >= 200 &&
- error.response.status < 300
- ) {
- responseColor = Helpers.colors.green;
- } else if (error?.response?.status && error.response.status >= 400) {
- responseColor = Helpers.colors.red;
- }
-
const isJSON = error.response.data && isObject(error.response.data);
return (
@@ -43,6 +23,7 @@ export default function ApiError({ error }: { error: null | ApiError }) {
enableClipboard={true}
displayObjectSize={false}
displayDataTypes={false}
+ sortKeys
src={error.response.data}
theme={Helpers.reactJsonViewTheme(darkMode)}
/>
diff --git a/src/common/route/ApiResponse.tsx b/src/common/route/ApiResponse.tsx
index 63796f3..c64cab3 100644
--- a/src/common/route/ApiResponse.tsx
+++ b/src/common/route/ApiResponse.tsx
@@ -4,7 +4,6 @@ import ReactJson from "react-json-view";
import { useGlobalContext } from "../../context/GlobalContext";
import Helpers from "../../lib/helpers";
-import { ApiResponseStatus } from "./ApiResponseStatus";
import { ApiResponse } from "../../types";
@@ -33,6 +32,7 @@ export default function ApiResponse({
enableClipboard={true}
displayObjectSize={false}
displayDataTypes={false}
+ sortKeys
src={response.data}
theme={Helpers.reactJsonViewTheme(darkMode)}
shouldCollapse={({ type, src, namespace, name }) => {
diff --git a/src/common/route/BodyPreview.tsx b/src/common/route/BodyPreview.tsx
index 20e8b52..322e19a 100644
--- a/src/common/route/BodyPreview.tsx
+++ b/src/common/route/BodyPreview.tsx
@@ -30,6 +30,7 @@ export default function BodyPreview({ body }: BodyPreviewProps) {
enableClipboard={true}
displayObjectSize={false}
displayDataTypes={false}
+ sortKeys
src={{ ...body }}
theme={Helpers.reactJsonViewTheme(darkMode)}
/>
diff --git a/src/common/route/Documentation.tsx b/src/common/route/Documentation.tsx
index d70c098..01731ec 100644
--- a/src/common/route/Documentation.tsx
+++ b/src/common/route/Documentation.tsx
@@ -1,7 +1,13 @@
import React from "react";
import ReactMarkdown from "react-markdown";
+import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
+import {
+ materialDark,
+ materialLight,
+} from "react-syntax-highlighter/dist/esm/styles/prism";
import { useGlobalContext } from "../../context/GlobalContext";
+import { CodeProps } from "react-markdown/lib/ast-to-react";
export interface DocumentationProps {
documentation?: string;
@@ -20,9 +26,40 @@ export function Documentation({ documentation }: DocumentationProps) {
darkMode ? "bg-gray-900 border-gray-700" : "bg-gray-200 border-gray-300"
}`}
>
-
+
{documentation}
);
}
+
+function CodeComponent({ children, className, node, ...rest }: CodeProps) {
+ const { darkMode } = useGlobalContext();
+ const match = /language-(\w+)/.exec(className || "");
+
+ if (!match) {
+ return (
+
+ {children}
+
+ );
+ }
+
+ const [, language] = match;
+ className += " highlighted";
+
+ return (
+
+ {children as string | string[]}
+
+ );
+}
diff --git a/src/common/route/Params.tsx b/src/common/route/Params.tsx
index be99f02..a6e7fa6 100644
--- a/src/common/route/Params.tsx
+++ b/src/common/route/Params.tsx
@@ -21,13 +21,11 @@ import {
Route,
Param,
ParamType,
- Size,
OnSubmitFn,
RenderInputsProps,
RenderInputByDataTypeProps,
RenderObjectProps,
RenderArrayOfInputsProps,
- Option,
} from "../../types";
function RenderArrayOfInputs({
@@ -311,7 +309,7 @@ export default function Params({
setValues: (values: any) => void;
}) {
const [customParams, setCustomParams] = useState
([]);
- let inputs = fetchInputsFromRouteDefinition(route, paramType, customParams);
+ const inputs = fetchInputsFromRouteDefinition(route, paramType, customParams);
// As the user switches between routes, ensure the custom params created on the previous route don't persist
useEffect(() => {
diff --git a/src/common/route/Request.tsx b/src/common/route/Request.tsx
index ea38538..45107a2 100644
--- a/src/common/route/Request.tsx
+++ b/src/common/route/Request.tsx
@@ -58,7 +58,7 @@ export function Request({
}
}
return reFetch();
- }, [urlParams, route]);
+ }, [urlParams, route, reFetch]);
return (
diff --git a/src/common/route/RequestResponse.tsx b/src/common/route/RequestResponse.tsx
index 66e1b07..ed71f05 100644
--- a/src/common/route/RequestResponse.tsx
+++ b/src/common/route/RequestResponse.tsx
@@ -25,13 +25,10 @@ export function RequestResponse({
route: Route;
applyAxiosInterceptors?: ApplyAxiosInterceptors;
}) {
- const {
- darkMode,
- storeHistoricResponse,
- setPartialRequestResponse,
- } = useGlobalContext();
+ const { darkMode, storeHistoricResponse, setPartialRequestResponse } =
+ useGlobalContext();
- const requestResponse: HistoricResponse = useActiveResponse(route);
+ const requestResponse: HistoricResponse = useActiveResponse(route, true);
const setUrlParams = useCallback(
(urlParams: Record
) => {
@@ -84,7 +81,7 @@ export function RequestResponse({
storeHistoricResponse: storeHistoricResponseWithRoute,
})
: axiosInstance;
- }, [route, storeHistoricResponseWithRoute]);
+ }, [applyAxiosInterceptors, route?.baseUrl, storeHistoricResponseWithRoute]);
const { response, loading, error, reFetch } = useAxios({
axios: axiosInstance,
@@ -105,7 +102,7 @@ export function RequestResponse({
error: null,
response: null,
});
- }, [requestResponse]);
+ }, [requestResponse, setPartialRequestResponse]);
const styles = useMemo(() => {
return {
diff --git a/src/common/route/TopBar.tsx b/src/common/route/TopBar.tsx
index 6c04db3..f848425 100644
--- a/src/common/route/TopBar.tsx
+++ b/src/common/route/TopBar.tsx
@@ -13,8 +13,8 @@ export function TopBar({
qsParams,
}: {
route: Route;
- urlParams: Record;
- qsParams: Record;
+ urlParams?: Record;
+ qsParams?: Record;
}) {
const { darkMode } = useGlobalContext();
const pathWithQS = useMemo(() => {
@@ -32,7 +32,11 @@ export function TopBar({
};
}, [darkMode]);
- const { copy, copied } = useCopyCurrentRoute({ activeRoute: route });
+ const { copy, copied } = useCopyCurrentRoute({
+ activeRoute: route,
+ urlParams,
+ qsParams,
+ });
if (!route) {
return null;
diff --git a/src/context/ConfigContext.tsx b/src/context/ConfigContext.tsx
new file mode 100644
index 0000000..f7eda71
--- /dev/null
+++ b/src/context/ConfigContext.tsx
@@ -0,0 +1,23 @@
+import React, { ReactNode, createContext, useContext } from "react";
+import { BadMagicProps } from "../types";
+import { useShallowMemo } from "../lib/shallowMemo";
+
+const ConfigContext = createContext(undefined as any);
+
+type ConfigProviderProps = {
+ config: BadMagicProps;
+ children: ReactNode;
+};
+
+export function ConfigProvider({ config, children }: ConfigProviderProps) {
+ const workspaces = useShallowMemo(config.workspaces);
+ const context = useShallowMemo({ ...config, workspaces });
+
+ return (
+ {children}
+ );
+}
+
+export function useConfigContext(): BadMagicProps {
+ return useContext(ConfigContext);
+}
diff --git a/src/context/GlobalContext.tsx b/src/context/GlobalContext.tsx
index b399871..38fb1cd 100644
--- a/src/context/GlobalContext.tsx
+++ b/src/context/GlobalContext.tsx
@@ -1,45 +1,37 @@
-import React, {
- useState,
- useCallback,
- useContext,
- useEffect,
- useMemo,
-} from "react";
-import { getLinkedRouteFromUrl } from "../lib/links";
+import React, { useState, useCallback, useContext, useMemo } from "react";
import * as storage from "../lib/storage";
-const storageKeys = {
- darkMode: "darkMode",
- hideDeprecatedRoutes: "hideDeprecatedRoutes",
- historicResponses: "historic-responses",
- collapsedWorkspaces: "collapsed-workspaces",
-};
-
import { HistoricResponse, Route, Workspace } from "../types";
+import { useConfigContext } from "./ConfigContext";
+import { routeHref } from "../lib/routing";
export const Context = React.createContext({
workspaces: [] as Workspace[],
- darkMode: storage.get(storageKeys.darkMode),
+ darkMode: storage.get(storage.keys.darkMode) || false,
setDarkMode: (darkMode: boolean) => {
// noop
},
- hideDeprecatedRoutes: storage.get(storageKeys.hideDeprecatedRoutes),
+ hideDeprecatedRoutes: storage.get(storage.keys.hideDeprecatedRoutes) || false,
setHideDeprecatedRoutes: (hideDeprecatedRoutes: boolean) => {
// noop
},
- historicResponses: (storage.get(storageKeys.historicResponses) ||
+ historicResponses: (storage.get(storage.keys.historicResponses) ||
[]) as HistoricResponse[],
storeHistoricResponse: (historicResponse: HistoricResponse) => {
return historicResponse;
},
+ clearHistoricResponses: (historicResponses: HistoricResponse[]) => {
+ // noop
+ },
partialRequestResponses: {} as Record,
setPartialRequestResponse: (historicResponse: HistoricResponse) => {
// noop
},
activeRoute: null as null | Route,
- setActiveRoute: (activeRoute: Route) => {
+ activeHref: "",
+ setActiveRoute: (activeRoute: Route | null) => {
// noop
},
@@ -56,20 +48,23 @@ export const Context = React.createContext({
export const useGlobalContext = () => useContext(Context);
-export function ContextProvider({
- children,
- workspaces,
-}: {
- children: React.ReactNode;
- workspaces: Workspace[];
-}) {
+export function ContextProvider({ children }: { children: React.ReactNode }) {
+ const { workspaces, basename } = useConfigContext();
+
const [activeRoute, setActiveRoute] = useState(null);
- const [keywords, setKeywords] = useState("");
+ const [keywords, setKeywordsInState] = useState(
+ () => storage.get(storage.keys.searchKeywords) ?? ""
+ );
const [collapsedWorkspaces, setCollapsedWorkspacesInState] = useState<
string[]
- >(storage.get(storageKeys.collapsedWorkspaces) || []);
+ >(() => storage.get(storage.keys.collapsedWorkspaces) || []);
const [darkMode, setDarkModeInState] = useState(
- storage.get(storageKeys.darkMode)
+ () => storage.get(storage.keys.darkMode) || false
+ );
+
+ const activeHref = useMemo(
+ () => routeHref(activeRoute, basename),
+ [activeRoute, basename]
);
// Used to track the state of a Request for a particular Route before it becomes a HistoricResponse
@@ -89,17 +84,22 @@ export function ContextProvider({
[partialRequestResponses]
);
+ const setKeywords = useCallback((keywords: string) => {
+ storage.set(storage.keys.searchKeywords, keywords);
+ setKeywordsInState(keywords);
+ }, []);
+
const setDarkMode = useCallback((darkMode: boolean) => {
- storage.set(storageKeys.darkMode, darkMode);
+ storage.set(storage.keys.darkMode, darkMode);
setDarkModeInState(darkMode);
}, []);
const [hideDeprecatedRoutes, setHideDeprecatedRoutesInState] =
- useState(storage.get(storageKeys.hideDeprecatedRoutes));
+ useState(storage.get(storage.keys.hideDeprecatedRoutes) || false);
const setCollapsedWorkspaces = useCallback(
(collapsedWorkspaces: string[]) => {
- storage.set(storageKeys.collapsedWorkspaces, collapsedWorkspaces);
+ storage.set(storage.keys.collapsedWorkspaces, collapsedWorkspaces);
setCollapsedWorkspacesInState(collapsedWorkspaces);
},
[]
@@ -107,7 +107,7 @@ export function ContextProvider({
const setHideDeprecatedRoutes = useCallback(
(hideDeprecatedRoutes: boolean) => {
- storage.set(storageKeys.hideDeprecatedRoutes, hideDeprecatedRoutes);
+ storage.set(storage.keys.hideDeprecatedRoutes, hideDeprecatedRoutes);
setHideDeprecatedRoutesInState(hideDeprecatedRoutes);
},
[]
@@ -115,7 +115,7 @@ export function ContextProvider({
const [historicResponses, setHistoricResponseInState] = useState<
HistoricResponse[]
- >([]);
+ >(() => storage.get(storage.keys.historicResponses) || []);
const storeHistoricResponse = useCallback(
({
@@ -163,7 +163,15 @@ export function ContextProvider({
// 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);
+ if (newResponse.response || newResponse.error) {
+ storage.set(
+ storage.keys.historicResponses,
+ newHistoricResponses.filter(
+ (historicResponse) =>
+ historicResponse.response || historicResponse.error
+ )
+ );
+ }
return newHistoricResponses;
});
@@ -173,6 +181,16 @@ export function ContextProvider({
[]
);
+ const clearHistoricResponses = useCallback((records: HistoricResponse[]) => {
+ setHistoricResponseInState((responses) => {
+ const recordsSet = new Set(records);
+ responses = responses.filter((response) => !recordsSet.has(response));
+
+ storage.set(storage.keys.historicResponses, responses);
+ return responses;
+ });
+ }, []);
+
const workspacesWithDefaults = useMemo(
() =>
workspaces.map((workspace) => ({
@@ -181,52 +199,51 @@ export function ContextProvider({
...route,
baseUrl: workspace.config.baseUrl || window.location.origin,
workspaceName: workspace.name,
+ workspaceId: workspace.id,
})),
})),
[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({
+ const context = useMemo(
+ () => ({
+ darkMode,
+ setDarkMode,
+ hideDeprecatedRoutes,
+ setHideDeprecatedRoutes,
+ historicResponses,
+ storeHistoricResponse,
+ clearHistoricResponses,
+ partialRequestResponses,
+ setPartialRequestResponse,
+ activeRoute,
+ activeHref,
+ setActiveRoute,
+ keywords,
+ setKeywords,
+ collapsedWorkspaces,
+ setCollapsedWorkspaces,
workspaces: workspacesWithDefaults,
- });
-
- if (route && historicResponse) {
- setActiveRoute(route);
- storeHistoricResponse(historicResponse);
- }
- }, [storeHistoricResponse, workspacesWithDefaults]);
-
- return (
-
- {children}
-
+ }),
+ [
+ activeRoute,
+ activeHref,
+ collapsedWorkspaces,
+ darkMode,
+ hideDeprecatedRoutes,
+ historicResponses,
+ clearHistoricResponses,
+ keywords,
+ partialRequestResponses,
+ setCollapsedWorkspaces,
+ setDarkMode,
+ setHideDeprecatedRoutes,
+ setPartialRequestResponse,
+ setKeywords,
+ storeHistoricResponse,
+ workspacesWithDefaults,
+ ]
);
+
+ return {children};
}
diff --git a/src/context/Router.tsx b/src/context/Router.tsx
new file mode 100644
index 0000000..6c10513
--- /dev/null
+++ b/src/context/Router.tsx
@@ -0,0 +1,182 @@
+import React, {
+ AnchorHTMLAttributes,
+ ReactNode,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+} from "react";
+
+import { Layout } from "../layout/Layout";
+
+import { useGlobalContext } from "..";
+import { useConfigContext } from "./ConfigContext";
+import {
+ extractEndpointParams,
+ routeHref,
+ routeLookupFactory,
+} from "../lib/routing";
+import { getLinkedRouteFromUrl } from "../lib/links";
+import { Route } from "../types";
+
+export function Router() {
+ const { basename } = useConfigContext();
+ const { workspaces, setActiveRoute } = useGlobalContext();
+
+ const lookupRoute = useMemo(() => {
+ return routeLookupFactory(workspaces);
+ }, [workspaces]);
+
+ useEffect(() => {
+ const params = extractEndpointParams(location.pathname, basename ?? "");
+
+ if (params) {
+ const route = lookupRoute(
+ params.workspaceId,
+ params.method,
+ params.path,
+ params.name
+ );
+ if (route) {
+ setActiveRoute(route);
+ }
+ } else {
+ setActiveRoute(null);
+ }
+
+ const handlePathChange = (): void => {
+ const params = extractEndpointParams(location.pathname, basename ?? "");
+ if (params) {
+ const route = lookupRoute(
+ params.workspaceId,
+ params.method,
+ params.path,
+ params.name
+ );
+ if (route) {
+ setActiveRoute(route);
+ }
+ } else {
+ setActiveRoute(null);
+ }
+ };
+
+ window.addEventListener("popstate", handlePathChange);
+
+ return () => {
+ window.removeEventListener("popstate", handlePathChange);
+ };
+ }, [basename, lookupRoute, setActiveRoute]);
+
+ return (
+
+
+
+ );
+}
+
+export type NavigateFn = (
+ route: Route | null,
+ replace?: boolean | undefined
+) => void;
+
+export function useNavigate(): NavigateFn {
+ const { basename } = useConfigContext();
+ const { activeHref, setActiveRoute } = useGlobalContext();
+ const activeHrefRef = useRef(activeHref);
+
+ activeHrefRef.current = activeHref;
+
+ return useCallback(
+ (route, replace): void => {
+ const href = routeHref(route, basename);
+
+ const push =
+ replace === undefined ? href !== activeHrefRef.current : !replace;
+
+ if (push) {
+ history.pushState({}, "", href);
+ } else {
+ history.replaceState({}, "", href);
+ }
+
+ [
+ window,
+ ...Array.from(document.querySelectorAll(".overflow-y-scroll")),
+ ].forEach((scrollContainer) => {
+ scrollContainer.scrollTo(0, 0);
+ });
+
+ setActiveRoute(route);
+ },
+ [basename, setActiveRoute]
+ );
+}
+
+type NavLinkProps = Omit, "href"> & {
+ to: Route | null;
+ replace?: boolean | undefined;
+ children: ReactNode;
+ className: string;
+ activeClassName?: string | undefined;
+};
+
+export function NavLink({
+ to,
+ children,
+ className,
+ activeClassName,
+ onClick,
+ replace,
+ ...props
+}: NavLinkProps) {
+ const { basename } = useConfigContext();
+ const { activeHref } = useGlobalContext();
+ const linkHref = useMemo(() => routeHref(to, basename), [to, basename]);
+ const navigate = useNavigate();
+
+ return (
+ {
+ onClick?.(event);
+
+ if (
+ !event.isDefaultPrevented() &&
+ (!props.target || props.target === "_self") &&
+ !event.metaKey &&
+ !event.ctrlKey
+ ) {
+ event.preventDefault();
+ navigate(to, replace);
+ }
+ }}
+ {...props}
+ >
+ {children}
+
+ );
+}
+
+function RouteProvider({ children }: { children: ReactNode }) {
+ const { workspaces, storeHistoricResponse } = useGlobalContext();
+ const navigate = useNavigate();
+
+ // On initial mount, this will fetch HistoricResponse from local storage
+ // and load any request that was deep linked
+ useEffect(() => {
+ const { route, historicResponse } = getLinkedRouteFromUrl({
+ workspaces,
+ });
+
+ if (route?.workspaceId && historicResponse) {
+ storeHistoricResponse(historicResponse);
+ navigate(route);
+ }
+ }, [workspaces, storeHistoricResponse, navigate]);
+
+ return <>{children}>;
+}
diff --git a/src/css/markdown.css b/src/css/markdown.css
index ba285eb..18e9b8d 100644
--- a/src/css/markdown.css
+++ b/src/css/markdown.css
@@ -217,7 +217,7 @@
padding: 0.2em 0.4em;
}
-.badmagic-markdown pre {
+.badmagic-markdown pre:not(:has(.highlighted)) {
word-wrap: normal;
background-color: #f6f8fa;
border-radius: 0.25em;
@@ -227,7 +227,7 @@
padding: 1rem;
}
-.badmagic-markdown pre > code {
+.badmagic-markdown pre:not(:has(.highlighted)) > code {
background: transparent;
border: 0;
font-size: 100%;
@@ -237,7 +237,7 @@
word-break: normal;
}
-.badmagic-markdown pre code {
+.badmagic-markdown pre:not(:has(.highlighted)) code {
background-color: transparent;
border: 0;
display: inline;
@@ -250,11 +250,15 @@
}
/* Dark Mode */
-.badmagic-markdown.dark pre {
+.badmagic-markdown.dark pre:not(:has(.highlighted)) {
color: #4a5568;
background-color: #edf2f7;
}
+.badmagic-markdown.dark pre:has(.highlighted) {
+ background-color: transparent;
+}
+
.badmagic-markdown.dark {
color: rgb(206, 206, 206);
}
diff --git a/src/css/markdown.min.css b/src/css/markdown.min.css
deleted file mode 100644
index 154c0c7..0000000
--- a/src/css/markdown.min.css
+++ /dev/null
@@ -1 +0,0 @@
-.badmagic-markdown{color:#24292e;line-height:1.5;font-size:16px;line-height:1.5;word-wrap:break-word}.badmagic-markdown:before{content:"";display:table}.badmagic-markdown:after{clear:both;content:"";display:table}.badmagic-markdown.dark{color:#cecece}.badmagic-markdown h1,.badmagic-markdown h2,.badmagic-markdown h3,.badmagic-markdown h4,.badmagic-markdown h5,.badmagic-markdown h6{font-weight:600;line-height:1.25;margin:1em 0 1em}.badmagic-markdown h1{font-size:2em}.badmagic-markdown h1,.badmagic-markdown h2{border-bottom:1px solid #eaecef;padding-bottom:.3em}.badmagic-markdown h2{font-size:1.5em}.badmagic-markdown h3{font-size:1.25em}.badmagic-markdown h4{font-size:1em}.badmagic-markdown h5{font-size:.875em}.badmagic-markdown h6{color:#6a737d;font-size:.85em}.badmagic-markdown a{background-color:transparent;color:#0366d6;text-decoration:none}.badmagic-markdown a:active,.badmagic-markdown a:hover{outline-width:0}.badmagic-markdown a:hover{text-decoration:underline}.badmagic-markdown a:not([href]){color:inherit;text-decoration:none}.badmagic-markdown strong{font-weight:600;font-weight:bolder}.badmagic-markdown hr{box-sizing:content-box;background:0 0;border-bottom:1px solid #dfe2e5;overflow:hidden;background-color:#e1e4e8;border-bottom-color:#eee;height:.25em;margin:1.25em 0;padding:0}.badmagic-markdown hr:before{content:"";display:table}.badmagic-markdown hr:after{clear:both;content:"";display:table}.badmagic-markdown table{border-collapse:collapse;border-spacing:0}.badmagic-markdown td,.badmagic-markdown th{padding:0}.badmagic-markdown p{margin-bottom:.6em;margin-top:0}.badmagic-markdown ol,.badmagic-markdown ul{margin-bottom:0;margin-top:0;padding-left:0}.badmagic-markdown ol ol,.badmagic-markdown ul ol{list-style-type:lower-roman}.badmagic-markdown ol ol ol,.badmagic-markdown ol ul ol,.badmagic-markdown ul ol ol,.badmagic-markdown ul ul ol{list-style-type:lower-alpha}.badmagic-markdown blockquote,.badmagic-markdown p,.badmagic-markdown pre,.badmagic-markdown table,.badmagic-markdown ul{margin-bottom:0 0 1em}.badmagic-markdown blockquote{border-left:.25em solid #dfe2e5;color:#6a737d;padding:0 1em;margin:0}.badmagic-markdown ol,.badmagic-markdown ul{padding-left:2em}.badmagic-markdown ol ol,.badmagic-markdown ol ul,.badmagic-markdown ul ol,.badmagic-markdown ul ul{margin-bottom:0;margin-top:0}.badmagic-markdown li{display:list-item;list-style-type:disc;list-style-position:inside;word-wrap:break-all}.badmagic-markdown li>p{margin-top:1em}.badmagic-markdown li+li{margin-top:.25em}.badmagic-markdown table{display:block;overflow:auto;width:100%}.badmagic-markdown table th{font-weight:600}.badmagic-markdown table td,.badmagic-markdown table th{border:1px solid #dfe2e5;padding:.2em .4em}.badmagic-markdown table tr{background-color:#fff;border-top:1px solid #c6cbd1}.badmagic-markdown table tr:nth-child(2n){background-color:#f6f8fa}.badmagic-markdown code{background-color:rgba(27,31,35,.05);border-radius:.25em;font-size:85%;margin:0;padding:.2em .4em}.badmagic-markdown pre{word-wrap:normal;background-color:#f6f8fa;border-radius:.25em;font-size:85%;line-height:1.45;overflow:auto;padding:1rem}.badmagic-markdown.dark pre{color:#4a5568;background-color:#edf2f7}.badmagic-markdown pre>code{background:0 0;border:0;font-size:100%;margin:0;padding:0;white-space:pre;word-break:normal}.badmagic-markdown pre code{background-color:transparent;border:0;display:inline;line-height:inherit;margin:0;max-width:auto;overflow:visible;padding:0;word-wrap:normal}
diff --git a/src/layout/Config.tsx b/src/layout/Config.tsx
index 89d27d9..770a723 100644
--- a/src/layout/Config.tsx
+++ b/src/layout/Config.tsx
@@ -53,7 +53,10 @@ export default function Config({
[collapsed]
);
- const toggleDarkMode = useCallback(() => setDarkMode(!darkMode), [darkMode]);
+ const toggleDarkMode = useCallback(
+ () => setDarkMode(!darkMode),
+ [darkMode, setDarkMode]
+ );
const styles = useMemo(() => {
return {
@@ -94,6 +97,8 @@ export default function Config({
{route.path}
-
+
))}
);
diff --git a/src/lib/activeResponse.ts b/src/lib/activeResponse.ts
index 260c192..ac41383 100644
--- a/src/lib/activeResponse.ts
+++ b/src/lib/activeResponse.ts
@@ -4,12 +4,22 @@ import { Helpers } from "..";
import { useGlobalContext } from "../context/GlobalContext";
import { HistoricResponse, Route } from "../types";
-export function useActiveResponse(activeRoute: Route): HistoricResponse {
- const { historicResponses, partialRequestResponses } = useGlobalContext();
+export function useActiveResponse(
+ activeRoute: Route,
+ includeIncomplete = false
+): HistoricResponse {
+ const { historicResponses, partialRequestResponses, workspaces } =
+ useGlobalContext();
const filteredHistory = useMemo(
- () => Helpers.filterHistory(historicResponses, activeRoute),
- [historicResponses, activeRoute]
+ () =>
+ Helpers.filterHistory(
+ historicResponses,
+ workspaces,
+ activeRoute,
+ includeIncomplete
+ ),
+ [historicResponses, workspaces, activeRoute, includeIncomplete]
);
return useMemo(() => {
diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts
index d75c86a..921aa82 100644
--- a/src/lib/helpers.ts
+++ b/src/lib/helpers.ts
@@ -4,6 +4,7 @@ import { stringify } from "querystring";
import OpenApi from "./openapi";
import { Route, Workspace, Param, HistoricResponse } from "../types";
+import { routeLookupFactory } from "./routing";
// Given a Route, URL Params, and QSParams, returns a route's path with the QS params included
function buildPathWithQS({
@@ -71,13 +72,40 @@ const Helpers = {
},
/** 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
- );
+ filterHistory(
+ historicResponses: HistoricResponse[],
+ workspaces: Workspace[],
+ route?: Route | null,
+ includeIncomplete = false
+ ) {
+ if (route) {
+ return historicResponses.filter(
+ (historicResponse: HistoricResponse) =>
+ historicResponse?.route?.path === route.path &&
+ historicResponse?.route?.method === route.method &&
+ (!historicResponse?.route?.workspaceId ||
+ historicResponse.route.workspaceId === route.workspaceId) &&
+ (includeIncomplete ||
+ historicResponse.response ||
+ historicResponse.error)
+ );
+ } else {
+ const lookupRoute = routeLookupFactory(workspaces);
+
+ return historicResponses.filter(
+ (historicResponse) =>
+ historicResponse?.route?.workspaceId !== undefined &&
+ (includeIncomplete ||
+ historicResponse.response ||
+ historicResponse.error) &&
+ lookupRoute(
+ historicResponse.route.workspaceId,
+ historicResponse.route.method,
+ historicResponse.route.path,
+ historicResponse.route.name
+ ) !== undefined
+ );
+ }
},
buildUrl({
diff --git a/src/lib/links.ts b/src/lib/links.ts
index b66bfc1..b52c307 100644
--- a/src/lib/links.ts
+++ b/src/lib/links.ts
@@ -4,8 +4,12 @@ import { useActiveResponse } from "./activeResponse";
export function useCopyCurrentRoute({
activeRoute,
+ urlParams,
+ qsParams,
}: {
activeRoute: Route | null;
+ urlParams?: Record