-
Notifications
You must be signed in to change notification settings - Fork 22
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
Add onUpdate event listener for installing starter blueprints on Playground urls #4249
Changes from all commits
61d9d76
841cfa8
814e831
121ce90
44847c0
e255c71
00af9ef
cd48429
9da961c
381c180
df0222f
84c0171
0d81973
dc0c521
efcf5df
5ba96b4
271a258
f35c539
c50cdf1
0ef4033
e72687f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,74 +23,147 @@ import { forEachTab } from "@/background/util"; | |
import { queueReactivateTab } from "@/contentScript/messenger/api"; | ||
import { ExtensionOptionsState } from "@/store/extensionsTypes"; | ||
import reportError from "@/telemetry/reportError"; | ||
import { debounce } from "lodash"; | ||
|
||
const { reducer, actions } = extensionsSlice; | ||
|
||
function installStarterBlueprint( | ||
const PLAYGROUND_URL = "https://www.pixiebrix.com/playground"; | ||
let isInstallingBlueprints = false; | ||
const BLUEPRINT_INSTALLATION_DEBOUNCE_MS = 10_000; | ||
const BLUEPRINT_INSTALLATION_MAX_MS = 60_000; | ||
|
||
function installBlueprint( | ||
state: ExtensionOptionsState, | ||
starterBlueprint: RecipeDefinition | ||
blueprint: RecipeDefinition | ||
): ExtensionOptionsState { | ||
return reducer( | ||
state, | ||
actions.installRecipe({ | ||
recipe: starterBlueprint, | ||
extensionPoints: starterBlueprint.extensionPoints, | ||
recipe: blueprint, | ||
extensionPoints: blueprint.extensionPoints, | ||
}) | ||
); | ||
} | ||
|
||
export async function installStarterBlueprints(): Promise<void> { | ||
async function installBlueprints( | ||
blueprints: RecipeDefinition[] | ||
): Promise<boolean> { | ||
let installed = false; | ||
if (blueprints.length === 0) { | ||
return installed; | ||
} | ||
|
||
let extensionsState = await loadOptions(); | ||
for (const blueprint of blueprints) { | ||
const blueprintAlreadyInstalled = extensionsState.extensions.some( | ||
(extension) => extension._recipe.id === blueprint.metadata.id | ||
); | ||
|
||
if (!blueprintAlreadyInstalled) { | ||
extensionsState = installBlueprint(extensionsState, blueprint); | ||
installed = true; | ||
} | ||
} | ||
|
||
await saveOptions(extensionsState); | ||
await forEachTab(queueReactivateTab); | ||
return installed; | ||
} | ||
|
||
async function getShouldFirstTimeInstall(): Promise<boolean> { | ||
const client = await maybeGetLinkedApiClient(); | ||
if (client == null) { | ||
console.debug( | ||
"Skipping starter blueprint installation because the extension is not linked to the PixieBrix service" | ||
); | ||
return; | ||
return false; | ||
} | ||
|
||
try { | ||
const { | ||
data: { install_starter_blueprints: shouldInstall }, | ||
} = await client.get("/api/onboarding/starter-blueprints/install/"); | ||
|
||
if (shouldInstall) { | ||
// If the starter blueprint request fails for some reason, or the user's primary organization | ||
// gets removed, we'd still like to mark starter blueprints as installed for this user | ||
// so that they don't see onboarding views/randomly have starter blueprints installed | ||
// the next time they open the extension | ||
await client.post("/api/onboarding/starter-blueprints/install/"); | ||
} | ||
|
||
return shouldInstall; | ||
} catch (error) { | ||
reportError(error); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is going to be returned from this function when it catches an error? I think There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 TypeScript in loose mode misses a lot of such warnings. It should have complained that the signature is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for catching this Ben! This was indeed a mistake. |
||
return false; | ||
} | ||
} | ||
|
||
async function getStarterBlueprints(): Promise<RecipeDefinition[]> { | ||
const client = await maybeGetLinkedApiClient(); | ||
if (client == null) { | ||
console.debug( | ||
"Skipping starter blueprint installation because the extension is not linked to the PixieBrix service" | ||
); | ||
return []; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💙 |
||
} | ||
|
||
try { | ||
const { data: starterBlueprints } = await client.get<RecipeDefinition[]>( | ||
"/api/onboarding/starter-blueprints/" | ||
); | ||
return starterBlueprints; | ||
} catch (error) { | ||
reportError(error); | ||
return []; | ||
} | ||
} | ||
|
||
// If the starter blueprint request fails for some reason, or the user's primary organization | ||
// gets removed, we'd still like to mark starter blueprints as installed for this user | ||
// so that they don't see onboarding views/randomly have starter blueprints installed | ||
// the next time they open the extension | ||
await client.post("/api/onboarding/starter-blueprints/install/"); | ||
|
||
if (starterBlueprints.length === 0) { | ||
return; | ||
} | ||
const _installStarterBlueprints = async (): Promise<boolean> => { | ||
if (isInstallingBlueprints) { | ||
return false; | ||
} | ||
|
||
let extensionsState = await loadOptions(); | ||
isInstallingBlueprints = true; | ||
const starterBlueprints = await getStarterBlueprints(); | ||
const installed = await installBlueprints(starterBlueprints); | ||
isInstallingBlueprints = false; | ||
return installed; | ||
}; | ||
|
||
for (const starterBlueprint of starterBlueprints) { | ||
const blueprintAlreadyInstalled = extensionsState.extensions.some( | ||
(extension) => extension._recipe.id === starterBlueprint.metadata.id | ||
); | ||
const debouncedInstallStarterBlueprints = debounce( | ||
_installStarterBlueprints, | ||
BLUEPRINT_INSTALLATION_DEBOUNCE_MS, | ||
{ | ||
leading: true, | ||
trailing: false, | ||
maxWait: BLUEPRINT_INSTALLATION_MAX_MS, | ||
} | ||
); | ||
|
||
if (!blueprintAlreadyInstalled) { | ||
extensionsState = installStarterBlueprint( | ||
extensionsState, | ||
starterBlueprint | ||
); | ||
} | ||
} | ||
export async function firstTimeInstallStarterBlueprints(): Promise<void> { | ||
const shouldInstall = await getShouldFirstTimeInstall(); | ||
if (!shouldInstall) { | ||
return; | ||
} | ||
|
||
await saveOptions(extensionsState); | ||
const installed = await debouncedInstallStarterBlueprints(); | ||
|
||
await forEachTab(queueReactivateTab); | ||
if (installed) { | ||
void browser.tabs.create({ | ||
url: "https://www.pixiebrix.com/playground", | ||
url: PLAYGROUND_URL, | ||
}); | ||
} catch (error) { | ||
reportError(error); | ||
} | ||
} | ||
|
||
function initStarterBlueprints(): void { | ||
void installStarterBlueprints(); | ||
browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { | ||
if (tab?.url?.startsWith(PLAYGROUND_URL)) { | ||
void debouncedInstallStarterBlueprints(); | ||
} | ||
}); | ||
|
||
void firstTimeInstallStarterBlueprints(); | ||
} | ||
|
||
export default initStarterBlueprints; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To ensure that it's not called undebounced (as it currently is on line 150), we can prepend
_
to the name.I generally inline the function in the
debounce
definition (debounce(() => {}, opts)
) but for long functions it's less readable.