Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

i51: Deep Linking #79

Merged
merged 14 commits into from
Sep 8, 2023
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
12 changes: 11 additions & 1 deletion src/context/GlobalContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,21 @@ export function ContextProvider({ children }: { children: React.ReactNode }) {

// On initial mount, this will fetch HistoricResponse from local storage once
useEffect(() => {
const linkedRequest = new URLSearchParams(window.location.search).get(
"request"
);
const historicResponsesFromStorage: HistoricResponse[] = storage.get(
storageKeys.historicResponses
);
if (linkedRequest) {
const data = JSON.parse(window.atob(linkedRequest)) as {
route: Route;
response: HistoricResponse;
};

if (historicResponsesFromStorage?.length) {
setActiveRoute(data.route);
setHistoricResponseInState([data.response]);
} else if (historicResponsesFromStorage?.length) {
setHistoricResponseInState(historicResponsesFromStorage);
}
}, []);
Expand Down
33 changes: 28 additions & 5 deletions src/layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { TopBar } from "./TopBar";
import { History } from "../common/History";

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

export function Layout({
workspaces,
Expand Down Expand Up @@ -87,6 +88,18 @@ export function Layout({
historyActive,
]);

const { copy, getUrl } = useCopyCurrentRoute({ activeRoute });
const [copied, setCopied] = useState(false);
const handleCopy = useCallback(async () => {
try {
await copy();
setCopied(true);
window.setTimeout(() => setCopied(false), 1000);
} catch {
window.alert(getUrl());
}
}, [copy, getUrl]);

return (
<div
className={`overflow-y-hidden min-h-full flex flex-col ${styles.background}`}
Expand All @@ -108,11 +121,21 @@ export function Layout({
) : null}
{activeRoute ? (
<div className="p-4 col-span-3 overflow-y-scroll">
<div
onClick={toggleSidebar}
className={`${styles.textColor} cursor-pointer mb-2 text-sm`}
>
{sidebarExpanded ? "Hide" : "Show"} Sidebar
<div className="flex gap-3">
<div
onClick={toggleSidebar}
className={`${styles.textColor} cursor-pointer mb-2 text-sm`}
>
{sidebarExpanded ? "Hide" : "Show"} Sidebar
</div>
<div
onClick={handleCopy}
className={`${
copied ? "text-green-400" : styles.textColor
} cursor-pointer mb-2 text-sm`}
>
{copied ? "Copied!" : "Copy link"}
</div>
</div>

{activeRoute && workspaceConfig ? (
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]);
}
12 changes: 11 additions & 1 deletion 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 Down
29 changes: 29 additions & 0 deletions src/lib/links.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useGlobalContext } from "../context/GlobalContext";
import { HistoricResponse, Route } from "../types";
import { useCallback } from "react";
import { useActiveResponse } from "./activeResponse";

export function useCopyCurrentRoute({
activeRoute,
}: {
activeRoute: Route | null;
}) {
const activeResponse: HistoricResponse = useActiveResponse(
activeRoute || { name: "", path: "" }
);

const getUrl = useCallback(() => {
const request = JSON.stringify({
route: activeResponse.route,
response: activeResponse,
});

return `${window.location.origin}?request=${window.btoa(request)}`;
jollyjerr marked this conversation as resolved.
Show resolved Hide resolved
}, [activeResponse]);

const copy = useCallback(() => navigator.clipboard.writeText(getUrl()), [
getUrl,
]);

return { copy, getUrl };
}
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export type Route = {
documentation?: string;
example?: Record<string, any>; // e.g. {first_name: "John", last_name: "Doe", ...}
baseUrl?: string; // if not specified on the route but exists on workspace.config.baseUrl, it will default to that
workspaceName?: string;

responses?: OpenApiResponses; // OpenApi Responses
tags?: string[];
Expand Down