Skip to content

Commit

Permalink
Working on app cards
Browse files Browse the repository at this point in the history
  • Loading branch information
khill-fbmc committed Oct 2, 2024
1 parent da68d5f commit 29ac858
Show file tree
Hide file tree
Showing 31 changed files with 1,243 additions and 529 deletions.
70 changes: 67 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
},
"license": "MIT",
"scripts": {
"build": "node utils/build.js",
"build": "DEBUG=FALSE node utils/build.js",
"fix": "eslint src --fix",
"prettier": "prettier --write '**/*.{js,jsx,ts,tsx,json,css,scss,md}'",
"start": "node utils/webserver.js"
"start": "DEBUG=true node utils/webserver.js"
},
"dependencies": {
"@extend-chrome/messages": "^1.2.2",
Expand All @@ -25,7 +25,8 @@
"react-hot-toast": "^2.4.1",
"simple-git": "^3.25.0",
"swr": "^2.2.5",
"tiny-typed-emitter": "^2.1.0"
"tiny-typed-emitter": "^2.1.0",
"zustand": "^5.0.0-rc.2"
},
"devDependencies": {
"@babel/core": "^7.24.9",
Expand Down
22 changes: 22 additions & 0 deletions src/hooks/useComposedUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useExtensionState } from "./useExtensionState";

export function useComposedUrl() {
return useExtensionState((state) => {
const app = state.getActiveApp();

if (app) {
const url = new URL(
`${app.public ? "p" : "app"}/${app.name}}`,
`https://${state.domain}.retool.com/`
);
app.query.forEach((q) => url.searchParams.append(q.param, q.value));

if (app.hash.length === 0) {
return `${url.toString()}`;
}

const hashParams = new URLSearchParams(app.hash.map((h) => [h.param, h.value]));
return `${url.toString()}#${hashParams.toString()}`;
}
});
}
57 changes: 57 additions & 0 deletions src/hooks/useExtensionState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";

import ChromeStateStorage from "../lib/chrome/ChromeStateStorage";
import { DEMO_APPS, INSPECTOR_APP } from "../pages/Options/EmbeddableApps";

import type { RetoolApp } from "../types";

type State = {
domain: string;
apps: RetoolApp[];
activeAppName: RetoolApp["name"] | undefined;
// activeApp: RetoolApp | undefined;
workflowUrl: string;
workflowApiKey: string;
};

interface Actions {
reset: () => void;
getActiveApp: () => RetoolApp | undefined;
setDomain: (domain: State["domain"]) => void;
setActiveApp: (name: State["domain"]) => void;
addApp: (app: RetoolApp) => void;
updateApp: (name: string, app: Partial<RetoolApp>) => void;
}

export const STORAGE_KEY = "app-embedder-for-retool";

const initialState: State = {
domain: "",
activeAppName: INSPECTOR_APP["name"],
workflowUrl: "",
workflowApiKey: "",
apps: [INSPECTOR_APP, ...DEMO_APPS],
};

export const useExtensionState = create<State & Actions>()(
persist(
(set, get) => ({
...initialState,
reset: () => set(initialState),
setDomain: (domain) => set(() => ({ domain })),
setActiveApp: (name) => set(() => ({ activeAppName: name })),
addApp: (app) => set((state) => ({ apps: [...state.apps, app] })),
updateApp: (name, props) => {
set((state) => ({
apps: state.apps.map((app) => (app.name === name ? { ...app, ...props } : app)),
}));
},
getActiveApp: () => get().apps.find((app) => app.name === get().activeAppName),
}),
{
name: STORAGE_KEY,
storage: createJSONStorage(() => ChromeStateStorage),
}
)
);
35 changes: 35 additions & 0 deletions src/hooks/useRetoolAppStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { create } from "zustand";

import type { RetoolApp, UrlParamSpec } from "../types";

type Actions = {
setAppId: (appName: string) => void;
setEnvironment: (env: RetoolApp["env"]) => void;
setVersion: (version: RetoolApp["version"]) => void;
setHashParams: (spec: UrlParamSpec[]) => void;
setQueryParams: (spec: UrlParamSpec[]) => void;
updateParam: (which: "query" | "hash", index: number, spec: UrlParamSpec[]) => void;
resetApp: () => void;
};

const initialState: RetoolApp = {
id: "",
env: "development",
version: "latest",
hash: [],
query: [],
};

export const useRetoolAppStore = create<RetoolApp & Actions>()((set) => ({
...initialState,
setAppId: (id) => set(() => ({ id })),
setEnvironment: (env) => set(() => ({ env })),
setVersion: (version) => set(() => ({ version })),
setHashParams: (hash) => set(() => ({ hash })),
setQueryParams: (query) => set(() => ({ query })),
updateParam: (which, index, param) =>
set((state) => ({
hash: state[which].map((item, idx) => (idx === index ? { ...item, ...param } : item)),
})),
resetApp: () => set(() => initialState),
}));
14 changes: 2 additions & 12 deletions src/hooks/useWorkflow.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,7 @@
import { useState } from "react";
import useSWR from "swr";

const callWorkflow = async (url: string, workflowApiKey: string): Promise<string[]> => {
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Workflow-Api-Key": workflowApiKey,
},
});
const data = (await res.json()) as { apps: string[] };
return data.apps ?? [];
};
import { callWorkflow } from "../lib/workflows";

export function useWorkflow(url: string, apiKey: string) {
const [workflowUrl, setWorkflowUrl] = useState<string>(url);
Expand All @@ -22,6 +12,6 @@ export function useWorkflow(url: string, apiKey: string) {
workflowApiKey,
setWorkflowUrl,
setWorkflowApiKey,
...useSWR<string[]>(url, (keyIsUrl: string) => callWorkflow(keyIsUrl, workflowApiKey)),
...useSWR<string[]>(url, () => callWorkflow(url, workflowApiKey)),
};
}
4 changes: 4 additions & 0 deletions src/lib/RetoolURL.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export function retoolAppToUrl() {
//
}

export function retoolUrl(config: RetoolUrlConfig) {
const url = new RetoolURL(config.domain);
if (config?.app) url.app(config.app);
Expand Down
51 changes: 51 additions & 0 deletions src/lib/chrome/ChromeStateStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { StateStorage } from "zustand/middleware";

const ChromeStateStorage: StateStorage = {
getItem,
setItem,
removeItem,
};

export default ChromeStateStorage;

function getItem(name: string): string | null | Promise<string | null> {
return runtimePromise<string>((resolve, rejectIfRuntimeError) => {
chrome.storage.sync.get(name, (items) => {
rejectIfRuntimeError();
resolve(items[name]);
});
});
}

function setItem(name: string, value: string): unknown | Promise<unknown> {
return runtimePromise((resolve, rejectIfRuntimeError) => {
chrome.storage.sync.set({ [name]: value }, () => {
rejectIfRuntimeError();
resolve(void 0);
});
});
}

function removeItem(name: string): unknown | Promise<unknown> {
return runtimePromise((resolve, rejectIfRuntimeError) => {
chrome.storage.sync.remove(name, () => {
rejectIfRuntimeError();
resolve(void 0);
});
});
}

function runtimePromise<T = unknown>(handler: PromiseHandler<T>): Promise<T> {
const { promise, resolve, reject } = Promise.withResolvers<T>();
const rejectIfRuntimeError = () => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError.message);
}
};
handler(resolve, rejectIfRuntimeError);
return promise;
}

type Resolver<T> = (value: T | PromiseLike<T>) => void;

type PromiseHandler<T> = (resolve: Resolver<T>, rejectIfRuntimeError: () => void) => void;
6 changes: 6 additions & 0 deletions src/lib/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@ export const log = (...args: unknown[]) => {
...args
);
};

export const debug = (...args: unknown[]) => {
if (process.env.DEBUG) {
log(...args);
}
};
7 changes: 4 additions & 3 deletions src/lib/storage.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { ChromeStorage } from "./chrome/ChromeStorage";

import type { ExtensionSettings, ParamEntry } from "../types";
import type { UrlParamSpec } from "../hooks/useExtensionState";
import type { ExtensionSettings } from "../types";

export type SerializedSettings = {
urlParams: ParamEntry[];
hashParams: ParamEntry[];
urlParams: UrlParamSpec[];
hashParams: UrlParamSpec[];
} & Required<ExtensionSettings>;

export const storage = new ChromeStorage<SerializedSettings>();
11 changes: 11 additions & 0 deletions src/lib/workflows.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const callWorkflow = async (url: string, workflowApiKey: string): Promise<string[]> => {
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Workflow-Api-Key": workflowApiKey,
},
});
const data = (await res.json()) as { apps: string[] };
return data.apps ?? [];
};
Loading

0 comments on commit 29ac858

Please sign in to comment.