From df803e4afab9675c2d897dfcba9d89eddbba9763 Mon Sep 17 00:00:00 2001 From: Kevin Hill Date: Mon, 23 Sep 2024 16:26:00 -0700 Subject: [PATCH] refactoring to add url params and hash params to retool embed url --- .../chrome/{storage.ts => ChromeStorage.ts} | 18 +- src/lib/chrome/index.ts | 2 +- src/lib/storage.ts | 10 + src/pages/Background/index.ts | 4 +- src/pages/Options/Options.tsx | 5 +- src/pages/Options/OptionsForm.tsx | 201 +++++++++++++++++- src/pages/Options/index.tsx | 2 +- src/pages/Panel/Panel.tsx | 44 +++- src/pages/Panel/RetoolFrame.tsx | 37 ---- src/pages/Panel/index.tsx | 2 +- src/types/index.ts | 6 + 11 files changed, 263 insertions(+), 68 deletions(-) rename src/lib/chrome/{storage.ts => ChromeStorage.ts} (86%) create mode 100644 src/lib/storage.ts delete mode 100644 src/pages/Panel/RetoolFrame.tsx diff --git a/src/lib/chrome/storage.ts b/src/lib/chrome/ChromeStorage.ts similarity index 86% rename from src/lib/chrome/storage.ts rename to src/lib/chrome/ChromeStorage.ts index fc2ede7..821b724 100644 --- a/src/lib/chrome/storage.ts +++ b/src/lib/chrome/ChromeStorage.ts @@ -1,8 +1,12 @@ -import type { ExtensionSettings } from "../../types"; - type Options = Record; -class ChromeStorage { +export type StorageUpdated = { + source: "storage"; + event: "update"; + data: T; +}; + +export class ChromeStorage { private _updateHandler: ((data: T) => void) | null = null; constructor() { @@ -49,11 +53,3 @@ class ChromeStorage { } } } - -export const storage = new ChromeStorage>(); - -type StorageUpdated = { - source: "storage"; - event: "update"; - data: T; -}; diff --git a/src/lib/chrome/index.ts b/src/lib/chrome/index.ts index 154f8ed..3051035 100644 --- a/src/lib/chrome/index.ts +++ b/src/lib/chrome/index.ts @@ -1,2 +1,2 @@ +export * from "./ChromeStorage"; export * from "./events"; -export * from "./storage"; diff --git a/src/lib/storage.ts b/src/lib/storage.ts new file mode 100644 index 0000000..6df8c00 --- /dev/null +++ b/src/lib/storage.ts @@ -0,0 +1,10 @@ +import { ChromeStorage } from "./chrome/ChromeStorage"; + +import type { ExtensionSettings, ParamEntry } from "../types"; + +export type SerializedSettings = { + urlParams: ParamEntry[]; + hashParams: ParamEntry[]; +} & Required; + +export const storage = new ChromeStorage(); diff --git a/src/pages/Background/index.ts b/src/pages/Background/index.ts index f8eecd6..6521426 100644 --- a/src/pages/Background/index.ts +++ b/src/pages/Background/index.ts @@ -1,7 +1,7 @@ /// -import { storage } from "../../lib/chrome"; import { log } from "../../lib/logger"; +import { storage } from "../../lib/storage"; chrome.commands.onCommand.addListener(() => { chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => { @@ -24,6 +24,8 @@ chrome.runtime.onInstalled.addListener(async () => { version: "latest", workflowUrl: "", workflowApiKey: "", + urlParams: [], + hashParams: [], }); log("Default settings set."); diff --git a/src/pages/Options/Options.tsx b/src/pages/Options/Options.tsx index 3d9f63a..41939e8 100644 --- a/src/pages/Options/Options.tsx +++ b/src/pages/Options/Options.tsx @@ -4,7 +4,6 @@ import React from "react"; import Alert from "react-bootstrap/Alert"; import Col from "react-bootstrap/Col"; import Container from "react-bootstrap/Container"; -import Form from "react-bootstrap/Form"; import Navbar from "react-bootstrap/Navbar"; import Row from "react-bootstrap/Row"; @@ -12,10 +11,10 @@ import extLogo from "../../assets/img/logo_32.png"; import retoolLogo from "../../assets/img/retool.svg"; import OptionsForm from "./OptionsForm"; -import type { ExtensionSettings } from "../../types"; +import type { SerializedSettings } from "../../lib/storage"; interface Props { - settings: ExtensionSettings; + settings: SerializedSettings; } const Options: React.FC = ({ settings }) => { diff --git a/src/pages/Options/OptionsForm.tsx b/src/pages/Options/OptionsForm.tsx index 57e0a66..a76df01 100644 --- a/src/pages/Options/OptionsForm.tsx +++ b/src/pages/Options/OptionsForm.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useMemo, useState } from "react"; import Accordion from "react-bootstrap/Accordion"; import Button from "react-bootstrap/Button"; import Col from "react-bootstrap/Col"; @@ -11,17 +11,18 @@ import toast, { Toaster } from "react-hot-toast"; import { useRetoolUrl } from "../../hooks/useRetoolUrl"; import { useWorkflow } from "../../hooks/useWorkflow"; -import { storage } from "../../lib/chrome"; +import { type SerializedSettings, storage } from "../../lib/storage"; import type { Environment, RetoolUrlConfig, RetoolVersion } from "../../lib/RetoolURL"; -import type { ExtensionSettings } from "../../types"; +import type { ParamEntry } from "../../types"; type Props = { - settings: ExtensionSettings; + settings: SerializedSettings; }; const OptionsForm: React.FC = ({ settings }) => { - const [useWorkflowList, setUseWorkflowList] = useState(false); + const [urlParams, setUrlParams] = useState(settings.urlParams); + const [hashParams, setHashParams] = useState(settings.hashParams); const { data: appList, @@ -33,11 +34,23 @@ const OptionsForm: React.FC = ({ settings }) => { setWorkflowApiKey, } = useWorkflow(`${settings?.workflowUrl}`, `${settings.workflowApiKey}`); + const [useWorkflowList, setUseWorkflowList] = useState(false); const { url, domain, app, version, env, setApp, setDomain, setVersion, setEnv } = useRetoolUrl( settings as RetoolUrlConfig ); - const handleSaveSettings = async (reloadFrame = false) => { + const composedUrl = useMemo(() => { + const _url = new URL(url); + const _hashParams = new URLSearchParams(); + urlParams.forEach(({ param, value }) => _url.searchParams.append(param, value)); + hashParams.forEach(({ param, value }) => _hashParams.append(param, value)); + if (hashParams.length === 0) { + return `${_url.toString()}`; + } + return `${_url.toString()}#${_hashParams.toString()}`; + }, [url, urlParams, hashParams]); + + const handleSaveSettings = async () => { if (domain === "") { toast.error("Your Retool instance name cannot be blank, please fill in this field."); return; @@ -54,6 +67,8 @@ const OptionsForm: React.FC = ({ settings }) => { env, workflowUrl, workflowApiKey, + urlParams, + hashParams, }); toast.success("Settings saved."); storage.load(); @@ -89,6 +104,7 @@ const OptionsForm: React.FC = ({ settings }) => { This is your registered domain / instance name. + = ({ settings }) => { "app/" + = ({ settings }) => { + + + + + Extra URL Params + {urlParams.length > 0 && + urlParams.map((entry) => { + return ( + { + setUrlParams((old) => { + return old.map((entry) => { + if (entry.index === index) { + entry[target] = data; + } + return entry; + }); + }); + }} + onValueChange={(paramKey) => { + setUrlParams((old) => { + return old; + }); + }} + onRemove={(indexToRemove) => { + setUrlParams((old) => { + return old.filter((entry) => { + return entry.index !== indexToRemove; + }); + }); + }} + /> + ); + })} + + + + + + Hash Params + {hashParams.length > 0 && + hashParams.map((entry) => { + return ( + { + setHashParams((old) => { + return old.map((entry) => { + if (entry.index === index) { + entry[target] = data; + } + return entry; + }); + }); + }} + onValueChange={(paramKey) => { + setHashParams((old) => { + return old; + }); + }} + onRemove={(indexToRemove) => { + setHashParams((old) => { + return old.filter((entry) => { + return entry.index !== indexToRemove; + }); + }); + }} + /> + ); + })} + + + + + = ({ settings }) => { @@ -277,3 +414,51 @@ const OptionsForm: React.FC = ({ settings }) => { }; export default OptionsForm; + +type ParamUpdate = { + index: number; + target: "param" | "value"; + data: string; +}; + +const ParamInputGroup: React.FC<{ + index: number; + param: string; + value: string; + onRemove: (index: number) => void; + onKeyChange: (data: ParamUpdate) => void; + onValueChange: (data: ParamUpdate) => void; +}> = ({ index, param, value, onKeyChange, onValueChange, onRemove }) => { + return ( + + { + onKeyChange({ + index, + target: "param", + data: e.target.value, + }); + }} + /> + { + onValueChange({ + index, + target: "value", + data: e.target.value, + }); + }} + /> + + + ); +}; diff --git a/src/pages/Options/index.tsx b/src/pages/Options/index.tsx index a30e39c..2df1552 100644 --- a/src/pages/Options/index.tsx +++ b/src/pages/Options/index.tsx @@ -3,7 +3,7 @@ import "./index.css"; import React from "react"; import { createRoot } from "react-dom/client"; -import { storage } from "../../lib/chrome"; +import { storage } from "../../lib/storage"; import Options from "./Options"; const container = document.getElementById("app-container"); diff --git a/src/pages/Panel/Panel.tsx b/src/pages/Panel/Panel.tsx index 48b6fee..8df5cb7 100644 --- a/src/pages/Panel/Panel.tsx +++ b/src/pages/Panel/Panel.tsx @@ -1,23 +1,57 @@ import "./Panel.css"; -import React from "react"; +import React, { useMemo, useState } from "react"; -import RetoolFrame from "./RetoolFrame"; +import { useRetoolUrl } from "../../hooks/useRetoolUrl"; +import { storage } from "../../lib/storage"; import UnsetSettingError from "./UnsetSettingError"; -import type { ExtensionSettings } from "../../types"; +import type { SerializedSettings } from "../../lib/storage"; +import type { ParamEntry } from "../../types"; interface Props { - settings: Required>; + settings: Required>; } const Panel: React.FC = ({ settings }) => { + const [urlParams, setUrlParams] = useState(settings.urlParams); + const [hashParams, setHashParams] = useState(settings.hashParams); + + const { url, setApp, setEnv, setDomain, setVersion } = useRetoolUrl(settings); + + storage.onUpdate((settings) => { + settings?.env && setEnv(settings.env); + settings?.app && setApp(settings.app); + settings?.domain && setDomain(settings.domain); + settings?.version && setVersion(settings.version); + settings?.hashParams && setHashParams(settings.hashParams); + settings?.urlParams && setHashParams(settings.urlParams); + }); + + const composedUrl = useMemo(() => { + const _url = new URL(url); + const _hashParams = new URLSearchParams(); + urlParams.forEach(({ param, value }) => _url.searchParams.append(param, value)); + hashParams.forEach(({ param, value }) => _hashParams.append(param, value)); + if (hashParams.length === 0) { + return `${_url.toString()}`; + } + return `${_url.toString()}#${_hashParams.toString()}`; + }, [url, urlParams, hashParams]); + return settings.domain === "" ? ( ) : settings.app === "" ? ( ) : ( - +