From c8c7364ffd83fca4b28dce081f240d3619b392de Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Thu, 21 Nov 2024 13:05:17 +0000 Subject: [PATCH 01/33] chore: prod only config --- static/scripts/config-parser.ts | 27 ++++---- static/scripts/render-manifest.ts | 104 ++++++------------------------ 2 files changed, 31 insertions(+), 100 deletions(-) diff --git a/static/scripts/config-parser.ts b/static/scripts/config-parser.ts index c5631b5..c331090 100644 --- a/static/scripts/config-parser.ts +++ b/static/scripts/config-parser.ts @@ -45,12 +45,12 @@ export class ConfigParser { return exists; } - async repoFileExistenceCheck(org: string, env: "development" | "production", octokit: Octokit, repo: string, path: string) { + async repoFileExistenceCheck(org: string, octokit: Octokit, repo: string, path: string) { try { const { data } = await octokit.repos.getContent({ owner: org, repo, - path: env === "production" ? path : path.replace(".yml", ".dev.yml"), + path }); return data; @@ -61,12 +61,12 @@ export class ConfigParser { return null; } - async fetchUserInstalledConfig(org: string, env: "development" | "production", octokit: Octokit, repo = CONFIG_ORG_REPO, path = CONFIG_FULL_PATH) { + async fetchUserInstalledConfig(org: string, octokit: Octokit, repo = CONFIG_ORG_REPO, path = CONFIG_FULL_PATH) { if (repo === CONFIG_ORG_REPO) { await this.configRepoExistenceCheck(org, repo, octokit); } - let existingConfig = await this.repoFileExistenceCheck(org, env, octokit, repo, path); + let existingConfig = await this.repoFileExistenceCheck(org, octokit, repo, path); if (!existingConfig) { try { @@ -74,14 +74,14 @@ export class ConfigParser { await octokit.repos.createOrUpdateFileContents({ owner: org, repo, - path: env === "production" ? path : path.replace(".yml", ".dev.yml"), - message: `chore: creating ${env} config`, + path, + message: `chore: creating config`, content: btoa(this.newConfigYml), }); - toastNotification(`We couldn't locate your ${env} config file, so we created an empty one for you.`, { type: "success" }); + toastNotification(`We couldn't locate your config file, so we created an empty one for you.`, { type: "success" }); - existingConfig = await this.repoFileExistenceCheck(org, env, octokit, repo, path); + existingConfig = await this.repoFileExistenceCheck(org, octokit, repo, path); } catch (er) { console.log(er); throw new Error("Config file creation failed"); @@ -110,7 +110,6 @@ export class ConfigParser { async updateConfig( org: string, - env: "development" | "production", octokit: Octokit, option: "add" | "remove", path = CONFIG_FULL_PATH, @@ -147,14 +146,14 @@ export class ConfigParser { } this.saveConfig(); - return this.createOrUpdateFileContents(org, repo, path, env, octokit); + return this.createOrUpdateFileContents(org, repo, path, octokit); } - async createOrUpdateFileContents(org: string, repo: string, path: string, env: "development" | "production", octokit: Octokit) { + async createOrUpdateFileContents(org: string, repo: string, path: string, octokit: Octokit) { const recentSha = await octokit.repos.getContent({ owner: org, repo: repo, - path: env === "production" ? path : path.replace(".yml", ".dev.yml"), + path, }); const sha = "sha" in recentSha.data ? recentSha.data.sha : null; @@ -170,8 +169,8 @@ export class ConfigParser { return octokit.repos.createOrUpdateFileContents({ owner: org, repo: repo, - path: env === "production" ? path : path.replace(".yml", ".dev.yml"), - message: `chore: updating ${env} config`, + path, + message: `chore: updating config`, content: btoa(this.newConfigYml), sha, }); diff --git a/static/scripts/render-manifest.ts b/static/scripts/render-manifest.ts index 66a98e9..3127483 100644 --- a/static/scripts/render-manifest.ts +++ b/static/scripts/render-manifest.ts @@ -22,7 +22,7 @@ export class ManifestRenderer { private _configDefaults: { [key: string]: { type: string; value: string; items: { type: string } | null } } = {}; private _auth: AuthService; private _backButton: HTMLButtonElement; - private _currentStep: "orgPicker" | "configSelector" | "pluginSelector" | "configEditor" = "orgPicker"; + private _currentStep: "orgPicker" | "pluginSelector" | "configEditor" = "orgPicker"; private _orgs: string[] = []; constructor(auth: AuthService) { @@ -52,18 +52,12 @@ export class ManifestRenderer { private _handleBackButtonClick(): void { switch (this._currentStep) { - case "configSelector": { - this.renderOrgPicker(this._orgs); - break; - } case "pluginSelector": { - const selectedConfig = localStorage.getItem("selectedConfig") as "development" | "production"; - this._renderConfigSelector(selectedConfig); + this.renderOrgPicker(this._orgs); break; } case "configEditor": { - const selectedConfig = localStorage.getItem("selectedConfig") as "development" | "production"; - this._renderPluginSelector(selectedConfig); + this._renderPluginSelector(); break; } default: @@ -78,7 +72,17 @@ export class ManifestRenderer { const selectedOrg = selectElement.value; if (selectedOrg) { localStorage.setItem("selectedOrg", selectedOrg); - this._renderConfigSelector(selectedOrg); + + const fetchOrgConfig = async () => { + const octokit = this._auth.octokit; + if (!octokit) { + throw new Error("No org or octokit found"); + } + await this._configParser.fetchUserInstalledConfig(selectedOrg, octokit); + } + + fetchOrgConfig().catch(console.error); + this._renderPluginSelector(); } } @@ -96,28 +100,6 @@ export class ManifestRenderer { } } - private _handleConfigSelection(event: Event): void { - try { - const selectElement = event.target as HTMLSelectElement; - const selectedConfig = selectElement.value as "development" | "production"; - if (selectedConfig) { - const fetchOrgConfig = async () => { - const org = localStorage.getItem("selectedOrg"); - const octokit = this._auth.octokit; - if (!org || !octokit) { - throw new Error("No org or octokit found"); - } - await this._configParser.fetchUserInstalledConfig(org, selectedConfig, octokit); - }; - localStorage.setItem("selectedConfig", selectedConfig); - this._renderPluginSelector(selectedConfig); - fetchOrgConfig().catch(console.error); - } - } catch (error) { - console.error("Error handling configuration selection:", error); - alert("An error occurred while selecting the configuration."); - } - } // UI Rendering @@ -188,47 +170,7 @@ export class ManifestRenderer { this._manifestGui?.classList.add("rendered"); } - private _renderConfigSelector(selectedOrg: string): void { - this._currentStep = "configSelector"; - this._backButton.style.display = "block"; - this._manifestGuiBody.innerHTML = null; - this._controlButtons(true); - - const pickerRow = document.createElement("tr"); - const pickerCell = document.createElement("td"); - pickerCell.colSpan = 2; - pickerCell.className = TDV_CENTERED; - - const configSelect = createElement("select", { - id: "config-selector-select", - class: PICKER_SELECT_STR, - }); - - const defaultOption = createElement("option", { - value: null, - textContent: "Select a configuration", - }); - configSelect.appendChild(defaultOption); - - const configs = ["development", "production"]; - configs.forEach((config) => { - const option = createElement("option", { - value: config, - textContent: config.charAt(0).toUpperCase() + config.slice(1), - }); - configSelect.appendChild(option); - }); - - configSelect.removeEventListener("change", this._handleConfigSelection.bind(this)); - configSelect.addEventListener("change", this._handleConfigSelection.bind(this)); - pickerCell.appendChild(configSelect); - pickerRow.appendChild(pickerCell); - - this._updateGuiTitle(`Select a Configuration for ${selectedOrg}`); - this._manifestGuiBody.appendChild(pickerRow); - } - - private _renderPluginSelector(selectedConfig: "development" | "production"): void { + private _renderPluginSelector(): void { this._currentStep = "pluginSelector"; this._backButton.style.display = "block"; this._manifestGuiBody.innerHTML = null; @@ -275,7 +217,7 @@ export class ManifestRenderer { pickerCell.appendChild(pluginSelect); pickerRow.appendChild(pickerCell); - this._updateGuiTitle(`Select a Plugin for ${selectedConfig}`); + this._updateGuiTitle(`Select a Plugin`); this._manifestGuiBody.appendChild(pickerRow); } @@ -480,18 +422,13 @@ export class ManifestRenderer { } const org = localStorage.getItem("selectedOrg"); - const config = localStorage.getItem("selectedConfig") as "development" | "production"; if (!org) { throw new Error("No selected org found"); } - if (!config) { - throw new Error("No selected config found"); - } - try { - await this._configParser.updateConfig(org, config, octokit, "add"); + await this._configParser.updateConfig(org, octokit, "add"); } catch (error) { console.error("Error pushing config to GitHub:", error); toastNotification("An error occurred while pushing the configuration to GitHub.", { @@ -521,18 +458,13 @@ export class ManifestRenderer { } const org = localStorage.getItem("selectedOrg"); - const config = localStorage.getItem("selectedConfig") as "development" | "production"; if (!org) { throw new Error("No selected org found"); } - if (!config) { - throw new Error("No selected config found"); - } - try { - await this._configParser.updateConfig(org, config, octokit, "remove"); + await this._configParser.updateConfig(org, octokit, "remove"); } catch (error) { console.error("Error pushing config to GitHub:", error); toastNotification("An error occurred while pushing the configuration to GitHub.", { From be54fb7f1e74c6e41cd6e6ac7ba24ac36422aa6b Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Thu, 21 Nov 2024 16:18:52 +0000 Subject: [PATCH 02/33] chore: fetch and render readme --- static/main.ts | 18 ++- static/manifest-gui.css | 180 ++++++++++++++++++++++-------- static/scripts/fetch-manifest.ts | 31 ++++- static/scripts/render-manifest.ts | 119 ++++++++++++++------ static/types/plugins.ts | 1 + static/utils/toaster.ts | 5 +- 6 files changed, 263 insertions(+), 91 deletions(-) diff --git a/static/main.ts b/static/main.ts index 7c00194..0510fc4 100644 --- a/static/main.ts +++ b/static/main.ts @@ -2,6 +2,7 @@ import { AuthService } from "./scripts/authentication"; import { ManifestDecoder } from "./scripts/decode-manifest"; import { ManifestFetcher } from "./scripts/fetch-manifest"; import { ManifestRenderer } from "./scripts/render-manifest"; +import { ManifestPreDecode } from "./types/plugins"; import { toastNotification } from "./utils/toaster"; async function handleAuth() { @@ -27,13 +28,20 @@ export async function mainModule() { const cache = fetcher.checkManifestCache(); if (auth.isActiveSession()) { const userOrgs = await auth.getGitHubUserOrgs(); - renderer.renderOrgPicker(userOrgs); + let fetchPromise: Promise> = Promise.resolve(cache); if (Object.keys(cache).length === 0) { - const manifestCache = await fetcher.fetchMarketplaceManifests(); - localStorage.setItem("manifestCache", JSON.stringify(manifestCache)); - // this is going to extract URLs from our official config which we'll inject into `- plugin: ...` - await fetcher.fetchOfficialPluginConfig(); + const killNotification = toastNotification("Fetching manifest data...", { type: "info", duration: 0 }); + fetchPromise = new Promise(async (resolve) => { + const manifestCache = await fetcher.fetchMarketplaceManifests(); + localStorage.setItem("manifestCache", JSON.stringify(manifestCache)); + // this is going to extract URLs from our official config which we'll inject into `- plugin: ...` + await fetcher.fetchOfficialPluginConfig(); + resolve(manifestCache); + killNotification(); + }); } + + renderer.renderOrgPicker(userOrgs, fetchPromise); } else { renderer.renderOrgPicker([]); } diff --git a/static/manifest-gui.css b/static/manifest-gui.css index 01ae511..bb5b381 100644 --- a/static/manifest-gui.css +++ b/static/manifest-gui.css @@ -1,7 +1,8 @@ html { background-image: radial-gradient(#000000, #101010); - background-color: #101010; /* height: 100vh; */ + background-color: #101010; } + body { margin: 0; color: #fff; @@ -9,9 +10,6 @@ body { background-image: url(./grid-25.png); height: calc(100vh - 24px); } -controls button { - background: #101010; -} header { margin: 24px; @@ -20,73 +18,191 @@ header { white-space: nowrap; mask-image: linear-gradient(90deg, #000, transparent); } + header:hover { opacity: 1; } + header #uos-logo-container { padding-right: 12px; } + header > * { display: inline-block; vertical-align: middle; } + header h1 { margin: 0; font-weight: 400; font-size: 24px; } + header #uos-logo { height: 36px; } + header #uos-logo path { fill: #fff; } + +#controls { + position: fixed; + right: 24px; + top: 24px; +} + +#controls button { + background: #101010; +} + +#viewport { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: calc(100vh - 96px); +} + +#viewport-cell { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; +} + +.readme-container { + width: auto; + max-width: 700px; + user-select: text; + background-image: linear-gradient(0deg, #101010, #202020); + border-radius: 4px; + margin: 16px auto; + box-shadow: 0 24px 48px #000000; + overflow: auto; + padding: 16px; + color: #fff; + font-family: "Proxima Nova", sans-serif; + line-height: 1.6; + opacity: 0.85; + overflow-y: auto; +} + #manifest-gui { + width: auto; + min-width: 400px; + max-width: 1300px; user-select: none; background-image: linear-gradient(0deg, #101010, #202020); - margin-top: 16px; - border-radius: 4px; margin: 0 auto; + border-radius: 4px; box-shadow: 0 24px 48px #000000; overflow: hidden; - opacity: 0.75; - transition: 0.25s opacity cubic-bezier(0, 1, 1, 1); opacity: 0; - width: 400px; + transition: opacity 0.25s cubic-bezier(0, 1, 1, 1); } + #manifest-gui.plugin-editor { - width: 800px; + width: 100%; } + #manifest-gui.rendered { opacity: 0.75; } + #manifest-gui:hover { opacity: 1; } + +#manifest-gui > tr > td > * { + vertical-align: middle; + margin: 8px; +} + +#manifest-gui thead td { + border-bottom: 1px solid #303030; +} + +#manifest-gui thead td:last-of-type { + text-align: right; + padding: 8px 16px; + color: #808080; +} + +#manifest-gui tfoot tr td button { + width: calc(50% - 4px); + display: inline-block; + font-weight: 900; + margin: 0 2px; +} + +#manifest-gui pre { + margin: 0 0 16px; +} + + + +.readme-container h1, +.readme-container h2, +.readme-container h3 { + color: #fff; + font-weight: bold; + margin-top: 24px; +} + +.readme-container p { + color: #ccc; + margin-bottom: 16px; +} + +.readme-container a { + color: #1e90ff; + text-decoration: none; +} + +.readme-container a:hover { + text-decoration: underline; +} + +.readme-container code { + background-color: #202020; + padding: 2px 4px; + border-radius: 4px; + color: #1e90ff; +} + +.readme-container pre { + background-color: #202020; + padding: 16px; + border-radius: 4px; + overflow: auto; + color: #ccc; + margin-bottom: 16px; +} + +/* Table Data Styles */ .table-data-header { text-align: right; color: grey; text-transform: capitalize; - text-rendering: geometricPrecision; /* padding-left:8px; */ + text-rendering: geometricPrecision; } -/* #manifest-gui .table-data-header>div{opacity:0;width:0;transition:1s width ease;overflow: hidden;} */ -/* #manifest-gui:hover .table-data-header>div{opacity:1;width:100px} */ + .table-data-header > div { padding: 8px 16px; } + .table-data-value { user-select: text; } + .table-data-value > div { padding: 8px 16px; border-bottom-left-radius: 4px; border-left: 1px solid #303030; - border-bottom: 1px solid #303030; /* font-weight: bold; */ -} -#manifest-gui > tr > td > * { - vertical-align: middle; - margin: 8px; + border-bottom: 1px solid #303030; } + #controls { position: fixed; right: 24px; @@ -111,31 +227,6 @@ button:hover { button:active { background-color: #606060; } -#manifest-gui thead td { - border-bottom: 1px solid #303030; /* text-align: end; */ -} - -#manifest-gui thead td:last-of-type { - text-align: right; - padding: 8px 16px; - color: #808080; -} -#viewport { - width: 100%; - display: table; - height: calc(100vh - ((36px + 8px) * 2) * 2); /* border-collapse: collapse; */ -} -#viewport-cell { - height: 100%; - display: table-cell; - vertical-align: middle; /* width: 100%; */ -} -#manifest-gui tfoot tr td button { - width: calc(50% - 4px); - display: inline-block; - font-weight: 900; - margin: 0 2px; -} button#remove::before { content: "−"; @@ -174,9 +265,6 @@ button#add:active::before { content: "+++"; } -#manifest-gui pre { - margin: 0 0 16px; -} .picker-select { width: 100%; diff --git a/static/scripts/fetch-manifest.ts b/static/scripts/fetch-manifest.ts index e0a92a3..b3fca16 100644 --- a/static/scripts/fetch-manifest.ts +++ b/static/scripts/fetch-manifest.ts @@ -26,14 +26,16 @@ export class ManifestFetcher { } const repos = await this._octokit.repos.listForOrg({ org }); const manifestCache = this.checkManifestCache(); + function makeUrl(org: string, repo: string, file: string) { return `https://raw.githubusercontent.com/${org}/${repo}/development/${file}` }; for (const repo of repos.data) { - const manifestUrl = `https://raw.githubusercontent.com/${org}/${repo.name}/development/manifest.json`; + const manifestUrl = makeUrl(org, repo.name, "manifest.json"); const manifest = await this.fetchActionManifest(manifestUrl); const decoded = this._decoder.decodeManifestFromFetch(manifest); + const readme = await this._fetchPluginReadme(makeUrl(org, repo.name, "README.md")); if (decoded) { - manifestCache[manifestUrl] = decoded; + manifestCache[manifestUrl] = { ...decoded, readme }; } } @@ -129,6 +131,31 @@ export class ManifestFetcher { } } + private async _fetchPluginReadme(pluginUrl: string) { + try { + const response = await fetch(pluginUrl, { + headers: { + "Content-Type": "application/json", + }, + method: "GET", + }); + return await response.text(); + } catch (e) { + let error = e; + try { + const res = await fetch(pluginUrl.replace(/development/g, "main")); + return await res.text(); + } catch (e) { + error = e; + } + console.error(error); + if (error instanceof Error) { + return error.message; + } + return String(error); + } + } + async fetchOrgsUbiquityOsConfigs() { const configFileContents: Record = {}; diff --git a/static/scripts/render-manifest.ts b/static/scripts/render-manifest.ts index 3127483..49f04a6 100644 --- a/static/scripts/render-manifest.ts +++ b/static/scripts/render-manifest.ts @@ -63,11 +63,17 @@ export class ManifestRenderer { default: break; } + + const readmeContainer = document.querySelector(".readme-container"); + if (readmeContainer) { + readmeContainer.remove(); + this._manifestGui?.classList.remove("plugin-editor"); + } } // Event Handlers - private _handleOrgSelection(event: Event): void { + private _handleOrgSelection(event: Event, fetchPromise?: Promise>): void { const selectElement = event.target as HTMLSelectElement; const selectedOrg = selectElement.value; if (selectedOrg) { @@ -82,7 +88,23 @@ export class ManifestRenderer { } fetchOrgConfig().catch(console.error); - this._renderPluginSelector(); + + if (fetchPromise) { + fetchPromise.then((manifestCache) => { + localStorage.setItem("manifestCache", JSON.stringify(manifestCache)); + this._renderPluginSelector(); + }).catch((error) => { + console.error("Error fetching manifest cache:", error); + toastNotification(`An error occurred while fetching the manifest cache: ${String(error)}`, { + type: "error", + shouldAutoDismiss: true, + }); + }) + + + } else { + this._renderPluginSelector(); + } } } @@ -100,7 +122,6 @@ export class ManifestRenderer { } } - // UI Rendering private _controlButtons(hide: boolean): void { @@ -116,7 +137,7 @@ export class ManifestRenderer { this._manifestGui?.classList.add("rendered"); } - public renderOrgPicker(orgs: string[]): void { + public renderOrgPicker(orgs: string[], fetchPromise?: Promise>): void { this._orgs = orgs; this._currentStep = "orgPicker"; this._controlButtons(true); @@ -163,7 +184,7 @@ export class ManifestRenderer { orgSelect.appendChild(option); }); - orgSelect.addEventListener("change", this._handleOrgSelection.bind(this)); + orgSelect.addEventListener("change", (event) => this._handleOrgSelection(event, fetchPromise)); pickerCell.appendChild(orgSelect); pickerRow.appendChild(pickerCell); this._manifestGuiBody.appendChild(pickerRow); @@ -206,6 +227,7 @@ export class ManifestRenderer { if (!cleanManifestCache[url]?.name) { return; } + const option = createElement("option", { value: JSON.stringify(cleanManifestCache[url]), textContent: cleanManifestCache[url]?.name, @@ -221,6 +243,61 @@ export class ManifestRenderer { this._manifestGuiBody.appendChild(pickerRow); } + private _boundConfigAdd = this._writeNewConfig.bind(this, "add"); + private _boundConfigRemove = this._writeNewConfig.bind(this, "remove"); + private _renderConfigEditor(manifestStr: string): void { + this._currentStep = "configEditor"; + this._backButton.style.display = "block"; + this._manifestGuiBody.innerHTML = null; + this._controlButtons(false); + + const pluginManifest = JSON.parse(manifestStr) as Manifest; + const configProps = pluginManifest.configuration?.properties || {}; + this._processProperties(configProps); + + const add = document.getElementById("add"); + const remove = document.getElementById("remove"); + if (!add || !remove) { + throw new Error("Add or remove button not found"); + } + add.addEventListener("click", this._boundConfigAdd); + remove.addEventListener("click", this._boundConfigRemove); + + const manifestCache = JSON.parse(localStorage.getItem("manifestCache") || "{}") as ManifestCache; + const pluginUrls = Object.keys(manifestCache); + const pluginUrl = pluginUrls.find((url) => { + return manifestCache[url].name === pluginManifest.name; + }) + + if (!pluginUrl) { + throw new Error("Plugin URL not found"); + } + const readme = manifestCache[pluginUrl].readme; + + if (readme) { + const viewportCell = document.getElementById("viewport-cell"); + if (!viewportCell) { + throw new Error("Viewport cell not found"); + } + const readmeContainer = document.createElement("div"); + readmeContainer.className = 'readme-container'; + readmeContainer.innerHTML = readme; + viewportCell.insertAdjacentElement("afterend", readmeContainer); + } + + this._updateGuiTitle(`Editing Configuration for ${pluginManifest.name}`); + this._manifestGui?.classList.add("plugin-editor"); + this._manifestGui?.classList.add("rendered"); + } + + private _updateGuiTitle(title: string): void { + const guiTitle = document.querySelector("#manifest-gui-title"); + if (!guiTitle) { + throw new Error("GUI Title not found"); + } + guiTitle.textContent = title; + } + renderManifest(decodedManifest: ManifestPreDecode) { if (!decodedManifest) { throw new Error("No decoded manifest found!"); @@ -253,42 +330,10 @@ export class ManifestRenderer { }); this._manifestGuiBody.appendChild(table); - this._manifestGui?.classList.add("rendered"); - } - private _boundConfigAdd = this._writeNewConfig.bind(this, "add"); - private _boundConfigRemove = this._writeNewConfig.bind(this, "remove"); - private _renderConfigEditor(manifestStr: string): void { - this._currentStep = "configEditor"; - this._backButton.style.display = "block"; - this._manifestGuiBody.innerHTML = null; - this._controlButtons(false); - - const pluginManifest = JSON.parse(manifestStr) as Manifest; - const configProps = pluginManifest.configuration?.properties || {}; - this._processProperties(configProps); - - const add = document.getElementById("add"); - const remove = document.getElementById("remove"); - if (!add || !remove) { - throw new Error("Add or remove button not found"); - } - add.addEventListener("click", this._boundConfigAdd); - remove.addEventListener("click", this._boundConfigRemove); - - this._updateGuiTitle(`Editing Configuration for ${pluginManifest.name}`); - this._manifestGui?.classList.add("plugin-editor"); this._manifestGui?.classList.add("rendered"); } - private _updateGuiTitle(title: string): void { - const guiTitle = document.querySelector("#manifest-gui-title"); - if (!guiTitle) { - throw new Error("GUI Title not found"); - } - guiTitle.textContent = title; - } - // Configuration Parsing private _processProperties(props: Record, prefix: string | null = null) { diff --git a/static/types/plugins.ts b/static/types/plugins.ts index 3fe14b8..3d63a01 100644 --- a/static/types/plugins.ts +++ b/static/types/plugins.ts @@ -38,6 +38,7 @@ export type Manifest = { }; }; }; + readme?: string; }; export type ManifestProps = { type: string; default: string; items?: { type: string }; properties?: Record }; diff --git a/static/utils/toaster.ts b/static/utils/toaster.ts index abbb6db..1cc11b1 100644 --- a/static/utils/toaster.ts +++ b/static/utils/toaster.ts @@ -9,7 +9,7 @@ export function toastNotification( shouldAutoDismiss?: boolean; duration?: number; } = {} -): void { +): () => void { const { type = "info", actionText, action, shouldAutoDismiss = false, duration = 5000 } = options; const toastElement = createElement("div", { @@ -64,4 +64,7 @@ export function toastNotification( setTimeout(() => toastElement.remove(), 250); }, duration); } + + + return toastElement.remove; } From a102b43e1787e9d2e6500f2634e6dfe9dde40d77 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Thu, 21 Nov 2024 18:30:11 +0000 Subject: [PATCH 03/33] feat: new dropdown, installed indicator --- static/main.ts | 15 ++- static/manifest-gui.css | 93 +++++++++---- static/scripts/render-manifest.ts | 217 +++++++++++++++++++----------- static/utils/element-helpers.ts | 6 +- static/utils/toaster.ts | 16 ++- 5 files changed, 237 insertions(+), 110 deletions(-) diff --git a/static/main.ts b/static/main.ts index 0510fc4..588057c 100644 --- a/static/main.ts +++ b/static/main.ts @@ -3,6 +3,7 @@ import { ManifestDecoder } from "./scripts/decode-manifest"; import { ManifestFetcher } from "./scripts/fetch-manifest"; import { ManifestRenderer } from "./scripts/render-manifest"; import { ManifestPreDecode } from "./types/plugins"; +import { manifestGuiBody } from "./utils/element-helpers"; import { toastNotification } from "./utils/toaster"; async function handleAuth() { @@ -26,16 +27,26 @@ export async function mainModule() { const ubiquityOrgsToFetchOfficialConfigFrom = ["ubiquity-os"]; const fetcher = new ManifestFetcher(ubiquityOrgsToFetchOfficialConfigFrom, auth.octokit, decoder); const cache = fetcher.checkManifestCache(); + if (!manifestGuiBody) { + throw new Error("Manifest GUI body not found"); + } + manifestGuiBody.dataset.loading = "false"; + if (auth.isActiveSession()) { const userOrgs = await auth.getGitHubUserOrgs(); let fetchPromise: Promise> = Promise.resolve(cache); if (Object.keys(cache).length === 0) { - const killNotification = toastNotification("Fetching manifest data...", { type: "info", duration: 0 }); + const killNotification = toastNotification("Fetching manifest data...", { type: "info", shouldAutoDismiss: true }); + manifestGuiBody.dataset.loading = "true"; + fetchPromise = new Promise(async (resolve) => { + if (!manifestGuiBody) { + throw new Error("Manifest GUI body not found"); + } const manifestCache = await fetcher.fetchMarketplaceManifests(); localStorage.setItem("manifestCache", JSON.stringify(manifestCache)); - // this is going to extract URLs from our official config which we'll inject into `- plugin: ...` await fetcher.fetchOfficialPluginConfig(); + manifestGuiBody.dataset.loading = "false"; resolve(manifestCache); killNotification(); }); diff --git a/static/manifest-gui.css b/static/manifest-gui.css index bb5b381..7b9b65e 100644 --- a/static/manifest-gui.css +++ b/static/manifest-gui.css @@ -180,7 +180,6 @@ header #uos-logo path { margin-bottom: 16px; } -/* Table Data Styles */ .table-data-header { text-align: right; color: grey; @@ -198,9 +197,6 @@ header #uos-logo path { .table-data-value > div { padding: 8px 16px; - border-bottom-left-radius: 4px; - border-left: 1px solid #303030; - border-bottom: 1px solid #303030; } #controls { @@ -213,7 +209,7 @@ button { background: 0 0; color: #fff; font-size: 16px; - border: none; /* background-color: transparent; */ + border: none; padding: 8px 16px; border-radius: 4px; opacity: 0.75; @@ -265,7 +261,6 @@ button#add:active::before { content: "+++"; } - .picker-select { width: 100%; padding: 8px 16px; @@ -286,6 +281,75 @@ button#add:active::before { outline: none; border-color: #808080; } +select option { + background-color: #101010; + color: #fff; + font-family: "Proxima Nova", sans-serif; + display: flex; + justify-content: space-between; +} + +.custom-select { + position: relative; + font-family: "Proxima Nova", sans-serif; + width: inherit +} + +.select-selected { + background-color: #101010; + color: #fff; + padding: 8px 16px; + border: 1px solid #303030; + border-radius: 4px; + cursor: pointer; + user-select: none; + position: relative; +} + +.select-selected::after { + content: ""; + position: absolute; + top: 18px; + right: 16px; + width: 0; + height: 0; + border: 6px solid transparent; + border-color: #fff transparent transparent transparent; +} + +.select-selected.select-arrow-active::after { + border-color: transparent transparent #fff transparent; + top: 12px; +} + +.select-items { + background-color: #101010; + border: 1px solid #303030; + border-radius: 8px; + z-index: 99; + width: inherit; + max-height: 400px; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: #303030 #101010; +} + +.select-items .select-option { + color: #fff; + padding: 8px 16px; + display: flex; + justify-content: space-between; + cursor: pointer; + user-select: none; +} + +.select-items .select-option:hover { + background-color: #303030; +} + +.select-hide { + display: none; +} .config-input { width: calc(100% - 32px); @@ -347,23 +411,6 @@ button#add:active::before { margin-bottom: 16px; } -/* Select Elements */ -.picker-select { - width: 100%; - padding: 8px 16px; - background-color: #101010; - color: #fff; - border: 1px solid #303030; - border-radius: 4px; - font-size: 16px; - font-family: "Proxima Nova", sans-serif; -} - -.picker-select:focus { - outline: none; - border-color: #606060; -} - .save-button { appearance: none; background: #101010; diff --git a/static/scripts/render-manifest.ts b/static/scripts/render-manifest.ts index 49f04a6..cf3dc3c 100644 --- a/static/scripts/render-manifest.ts +++ b/static/scripts/render-manifest.ts @@ -9,7 +9,10 @@ import { toastNotification } from "../utils/toaster"; const ajv = new AJV({ allErrors: true, coerceTypes: true, strict: true }); const TDV_CENTERED = "table-data-value centered"; -const PICKER_SELECT_STR = "picker-select"; +const SELECT_ITEMS = ".select-items" +const SELECT_SELECTED = ".select-selected" +const SELECT_HIDE = "select-hide" +const SELECT_ARROW_ACTIVE = "select-arrow-active" type ExtendedHtmlElement = { [key in keyof T]: T[key] extends HTMLElement["innerHTML"] ? string | null : T[key]; @@ -73,53 +76,37 @@ export class ManifestRenderer { // Event Handlers - private _handleOrgSelection(event: Event, fetchPromise?: Promise>): void { - const selectElement = event.target as HTMLSelectElement; - const selectedOrg = selectElement.value; - if (selectedOrg) { - localStorage.setItem("selectedOrg", selectedOrg); + private _handleOrgSelection(org: string, fetchPromise?: Promise>): void { + if (!org) { + throw new Error("No org selected"); + } + + localStorage.setItem("selectedOrg", org); + + if (fetchPromise) { + fetchPromise.then((manifestCache) => { + localStorage.setItem("manifestCache", JSON.stringify(manifestCache)); + }).catch((error) => { + console.error("Error fetching manifest cache:", error); + toastNotification(`An error occurred while fetching the manifest cache: ${String(error)}`, { + type: "error", + shouldAutoDismiss: true, + }); + }); const fetchOrgConfig = async () => { const octokit = this._auth.octokit; if (!octokit) { throw new Error("No org or octokit found"); } - await this._configParser.fetchUserInstalledConfig(selectedOrg, octokit); - } - - fetchOrgConfig().catch(console.error); - - if (fetchPromise) { - fetchPromise.then((manifestCache) => { - localStorage.setItem("manifestCache", JSON.stringify(manifestCache)); - this._renderPluginSelector(); - }).catch((error) => { - console.error("Error fetching manifest cache:", error); - toastNotification(`An error occurred while fetching the manifest cache: ${String(error)}`, { - type: "error", - shouldAutoDismiss: true, - }); - }) - - - } else { + await this._configParser.fetchUserInstalledConfig(org, octokit); this._renderPluginSelector(); } + fetchOrgConfig().catch(console.error); + } else { + this._renderPluginSelector(); } - } - private _handlePluginSelection(event: Event): void { - try { - const selectElement = event.target as HTMLSelectElement; - const selectedPluginManifest = selectElement.value; - if (selectedPluginManifest) { - localStorage.setItem("selectedPluginManifest", selectedPluginManifest); - this._renderConfigEditor(selectedPluginManifest); - } - } catch (error) { - console.error("Error handling plugin selection:", error); - alert("An error occurred while selecting the plugin."); - } } // UI Rendering @@ -150,12 +137,30 @@ export class ManifestRenderer { pickerCell.colSpan = 4; pickerCell.className = TDV_CENTERED; + const customSelect = createElement("div", { class: "custom-select" }); + + const selectSelected = createElement("div", { + class: "select-selected", + textContent: "Select an organization", + }); + + const selectItems = createElement("div", { + class: "select-items select-hide", + }); + + customSelect.appendChild(selectSelected); + customSelect.appendChild(selectItems); + + pickerCell.appendChild(customSelect); + pickerRow.appendChild(pickerCell); + + this._manifestGuiBody.appendChild(pickerRow); + this._manifestGui?.classList.add("rendered"); + if (!orgs.length) { const hasSession = this._auth.isActiveSession(); if (hasSession) { this._updateGuiTitle("No organizations found"); - this._manifestGuiBody.appendChild(pickerRow); - this._manifestGui?.classList.add("rendered"); } else { this._updateGuiTitle("Please sign in to GitHub"); } @@ -164,31 +169,40 @@ export class ManifestRenderer { this._updateGuiTitle("Select an Organization"); - const orgSelect = createElement("select", { - id: "org-picker-select", - class: PICKER_SELECT_STR, - style: "width: 100%", + orgs.forEach((org) => { + const optionDiv = createElement("div", { class: "select-option" }); + const textSpan = createElement("span", { textContent: org }); + + optionDiv.appendChild(textSpan); + + optionDiv.addEventListener("click", () => { + this._handleOrgSelection(org, fetchPromise); + selectSelected.textContent = org; + localStorage.setItem("selectedOrg", org); + }); + + selectItems.appendChild(optionDiv); }); - const defaultOption = createElement("option", { - value: null, - textContent: "Found Organizations...", + selectSelected.addEventListener("click", (e) => { + e.stopPropagation(); + closeAllSelect(); + selectItems.classList.toggle(SELECT_HIDE); + selectSelected.classList.toggle(SELECT_ARROW_ACTIVE); }); - orgSelect.appendChild(defaultOption); - orgs.forEach((org) => { - const option = createElement("option", { - value: org, - textContent: org, + function closeAllSelect() { + const selectItemsList = document.querySelectorAll(SELECT_ITEMS); + const selectSelectedList = document.querySelectorAll(SELECT_SELECTED); + selectItemsList.forEach((item) => { + item.classList.add(SELECT_HIDE); }); - orgSelect.appendChild(option); - }); + selectSelectedList.forEach((item) => { + item.classList.remove(SELECT_ARROW_ACTIVE); + }); + } - orgSelect.addEventListener("change", (event) => this._handleOrgSelection(event, fetchPromise)); - pickerCell.appendChild(orgSelect); - pickerRow.appendChild(pickerCell); - this._manifestGuiBody.appendChild(pickerRow); - this._manifestGui?.classList.add("rendered"); + document.addEventListener("click", closeAllSelect); } private _renderPluginSelector(): void { @@ -205,16 +219,12 @@ export class ManifestRenderer { pickerCell.colSpan = 2; pickerCell.className = TDV_CENTERED; - const pluginSelect = createElement("select", { - id: "plugin-selector-select", - class: PICKER_SELECT_STR, - }); + const userConfig = this._configParser.repoConfig; + let installedPlugins: string[] = []; - const defaultOption = createElement("option", { - value: null, - textContent: "Select a plugin", - }); - pluginSelect.appendChild(defaultOption); + if (userConfig) { + installedPlugins = this._configParser.parseConfig(userConfig).plugins.flatMap((plugin) => plugin.uses.map((use) => use.plugin)); + } const cleanManifestCache = Object.keys(manifestCache).reduce((acc, key) => { if (manifestCache[key]?.name) { @@ -223,24 +233,74 @@ export class ManifestRenderer { return acc; }, {} as ManifestCache); + const customSelect = createElement("div", { class: "custom-select" }); + + const selectSelected = createElement("div", { + class: "select-selected", + textContent: "Select a plugin", + }); + + const selectItems = createElement("div", { + class: "select-items select-hide", + }); + + customSelect.appendChild(selectSelected); + customSelect.appendChild(selectItems); + + pickerCell.appendChild(customSelect); + pickerRow.appendChild(pickerCell); + + this._manifestGuiBody.appendChild(pickerRow); + pluginUrls.forEach((url) => { if (!cleanManifestCache[url]?.name) { return; } - const option = createElement("option", { - value: JSON.stringify(cleanManifestCache[url]), - textContent: cleanManifestCache[url]?.name, + const [, repo] = url.replace("https://raw.githubusercontent.com/", "").split("/"); + const reg = new RegExp(`${repo}`, "gi"); + const isInstalled = installedPlugins.some((plugin) => reg.test(plugin)); + + const optionText = cleanManifestCache[url]?.name; + const indicator = isInstalled ? "🟢" : "🔴"; + + const optionDiv = createElement("div", { class: "select-option" }); + const textSpan = createElement("span", { textContent: optionText }); + const indicatorSpan = createElement("span", { textContent: indicator }); + + optionDiv.appendChild(textSpan); + optionDiv.appendChild(indicatorSpan); + + optionDiv.addEventListener("click", () => { + selectSelected.textContent = optionText; + selectSelected.setAttribute("data-value", JSON.stringify(cleanManifestCache[url])); + closeAllSelect(); + this._renderConfigEditor(JSON.stringify(cleanManifestCache[url])); }); - pluginSelect.appendChild(option); + + selectItems.appendChild(optionDiv); }); - pluginSelect.addEventListener("change", this._handlePluginSelection.bind(this)); - pickerCell.appendChild(pluginSelect); - pickerRow.appendChild(pickerCell); + selectSelected.addEventListener("click", (e) => { + e.stopPropagation(); + closeAllSelect(); + selectItems.classList.toggle(SELECT_HIDE); + selectSelected.classList.toggle(SELECT_ARROW_ACTIVE); + }); + + function closeAllSelect() { + const selectItemsList = document.querySelectorAll(SELECT_ITEMS); + const selectSelectedList = document.querySelectorAll(SELECT_SELECTED); + selectItemsList.forEach((item) => { + item.classList.add(SELECT_HIDE); + }); + selectSelectedList.forEach((item) => { + item.classList.remove(SELECT_ARROW_ACTIVE); + }); + } + document.addEventListener("click", closeAllSelect); this._updateGuiTitle(`Select a Plugin`); - this._manifestGuiBody.appendChild(pickerRow); } private _boundConfigAdd = this._writeNewConfig.bind(this, "add"); @@ -457,7 +517,8 @@ export class ManifestRenderer { private _handleAddPlugin(plugin: Plugin, pluginManifest: Manifest): void { this._configParser.addPlugin(plugin); - toastNotification(`Configuration for ${pluginManifest.name} saved successfully. Do you want to push to GitHub?`, { + toastNotification(`Configuration for ${pluginManifest.name + } saved successfully.Do you want to push to GitHub ? `, { type: "success", actionText: "Push to GitHub", action: async () => { @@ -493,7 +554,7 @@ export class ManifestRenderer { private _handleRemovePlugin(plugin: Plugin, pluginManifest: Manifest): void { this._configParser.removePlugin(plugin); - toastNotification(`Configuration for ${pluginManifest.name} removed successfully. Do you want to push to GitHub?`, { + toastNotification(`Configuration for ${pluginManifest.name} removed successfully.Do you want to push to GitHub ? `, { type: "success", actionText: "Push to GitHub", action: async () => { diff --git a/static/utils/element-helpers.ts b/static/utils/element-helpers.ts index dc9116a..d52bcfb 100644 --- a/static/utils/element-helpers.ts +++ b/static/utils/element-helpers.ts @@ -4,13 +4,13 @@ const CONFIG_INPUT_STR = "config-input"; export const manifestGuiBody = document.getElementById("manifest-gui-body"); -export function createElement(tagName: TK, attributes: { [key: string]: string | null }): HTMLElementTagNameMap[TK] { +export function createElement(tagName: TK, attributes: { [key: string]: string | boolean | null }): HTMLElementTagNameMap[TK] { const element = document.createElement(tagName); Object.keys(attributes).forEach((key) => { if (key === "textContent") { - element.textContent = attributes[key]; + element.textContent = attributes[key] as string; } else if (key in element) { - (element as Record)[key] = attributes[key]; + (element as Record)[key] = attributes[key]; } else { element.setAttribute(key, `${attributes[key]}`); } diff --git a/static/utils/toaster.ts b/static/utils/toaster.ts index 1cc11b1..7fd85c3 100644 --- a/static/utils/toaster.ts +++ b/static/utils/toaster.ts @@ -58,13 +58,21 @@ export function toastNotification( toastElement.classList.add("show"); }); - if (shouldAutoDismiss) { - setTimeout(() => { + function kill(withTimeout = false) { + if (withTimeout) { + setTimeout(() => { + toastElement.classList.remove("show"); + setTimeout(() => toastElement.remove(), 250); + }, duration); + } else { toastElement.classList.remove("show"); setTimeout(() => toastElement.remove(), 250); - }, duration); + } } + if (shouldAutoDismiss) { + kill(shouldAutoDismiss); + } - return toastElement.remove; + return kill; } From 20a403dbaa902d73af5945e19ce6425bfbe71af7 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Thu, 21 Nov 2024 19:19:47 +0000 Subject: [PATCH 04/33] chore: load installed config values if present --- static/scripts/render-manifest.ts | 64 +++++++++++++++++++++++-------- static/types/plugins.ts | 15 +++----- static/utils/element-helpers.ts | 6 +-- 3 files changed, 56 insertions(+), 29 deletions(-) diff --git a/static/scripts/render-manifest.ts b/static/scripts/render-manifest.ts index cf3dc3c..299611c 100644 --- a/static/scripts/render-manifest.ts +++ b/static/scripts/render-manifest.ts @@ -1,5 +1,4 @@ -import { Manifest } from "@ubiquity-os/ubiquity-os-kernel"; -import { ManifestCache, ManifestPreDecode, ManifestProps, Plugin } from "../types/plugins"; +import { ManifestCache, ManifestPreDecode, Plugin, Manifest } from "../types/plugins"; import { ConfigParser } from "./config-parser"; import { AuthService } from "./authentication"; import AJV, { AnySchemaObject } from "ajv"; @@ -220,10 +219,10 @@ export class ManifestRenderer { pickerCell.className = TDV_CENTERED; const userConfig = this._configParser.repoConfig; - let installedPlugins: string[] = []; + let installedPlugins: Plugin[] = []; if (userConfig) { - installedPlugins = this._configParser.parseConfig(userConfig).plugins.flatMap((plugin) => plugin.uses.map((use) => use.plugin)); + installedPlugins = this._configParser.parseConfig(userConfig).plugins } const cleanManifestCache = Object.keys(manifestCache).reduce((acc, key) => { @@ -259,10 +258,10 @@ export class ManifestRenderer { const [, repo] = url.replace("https://raw.githubusercontent.com/", "").split("/"); const reg = new RegExp(`${repo}`, "gi"); - const isInstalled = installedPlugins.some((plugin) => reg.test(plugin)); - - const optionText = cleanManifestCache[url]?.name; - const indicator = isInstalled ? "🟢" : "🔴"; + const installedPlugin: Plugin | undefined = installedPlugins.find((plugin) => plugin.uses[0].plugin.match(reg)); + const defaultForInstalled: ManifestPreDecode | null = cleanManifestCache[url]; + const optionText = defaultForInstalled.name; + const indicator = installedPlugin ? "🟢" : "🔴"; const optionDiv = createElement("div", { class: "select-option" }); const textSpan = createElement("span", { textContent: optionText }); @@ -273,9 +272,9 @@ export class ManifestRenderer { optionDiv.addEventListener("click", () => { selectSelected.textContent = optionText; - selectSelected.setAttribute("data-value", JSON.stringify(cleanManifestCache[url])); closeAllSelect(); - this._renderConfigEditor(JSON.stringify(cleanManifestCache[url])); + localStorage.setItem("selectedPluginManifest", JSON.stringify(defaultForInstalled)); + this._renderConfigEditor(defaultForInstalled, installedPlugin?.uses[0].with); }); selectItems.appendChild(optionDiv); @@ -305,15 +304,46 @@ export class ManifestRenderer { private _boundConfigAdd = this._writeNewConfig.bind(this, "add"); private _boundConfigRemove = this._writeNewConfig.bind(this, "remove"); - private _renderConfigEditor(manifestStr: string): void { + private _renderConfigEditor(pluginManifest: Manifest | null, plugin?: Plugin["uses"][0]["with"]): void { this._currentStep = "configEditor"; this._backButton.style.display = "block"; this._manifestGuiBody.innerHTML = null; this._controlButtons(false); + this._processProperties(pluginManifest?.configuration?.properties || {}); + const configInputs = document.querySelectorAll(".config-input"); + + if (plugin) { + configInputs.forEach((input) => { + const key = input.getAttribute("data-config-key"); + if (!key) { + throw new Error("Input key is required"); + } + + const keys = key.split("."); + let currentObj = plugin + for (let i = 0; i < keys.length; i++) { + if (!currentObj[keys[i]]) { + break; + } + currentObj = currentObj[keys[i]] as Record; + } - const pluginManifest = JSON.parse(manifestStr) as Manifest; - const configProps = pluginManifest.configuration?.properties || {}; - this._processProperties(configProps); + let value: string; + + if (typeof currentObj === "object") { + value = JSON.stringify(currentObj, null, 2); + } else { + value = currentObj as string; + } + + if (input.tagName === "TEXTAREA") { + (input as HTMLTextAreaElement).value = value; + } else { + (input as HTMLInputElement).value = value; + } + } + ); + } const add = document.getElementById("add"); const remove = document.getElementById("remove"); @@ -326,7 +356,7 @@ export class ManifestRenderer { const manifestCache = JSON.parse(localStorage.getItem("manifestCache") || "{}") as ManifestCache; const pluginUrls = Object.keys(manifestCache); const pluginUrl = pluginUrls.find((url) => { - return manifestCache[url].name === pluginManifest.name; + return manifestCache[url].name === pluginManifest?.name; }) if (!pluginUrl) { @@ -345,7 +375,7 @@ export class ManifestRenderer { viewportCell.insertAdjacentElement("afterend", readmeContainer); } - this._updateGuiTitle(`Editing Configuration for ${pluginManifest.name}`); + this._updateGuiTitle(`Editing Configuration for ${pluginManifest?.name}`); this._manifestGui?.classList.add("plugin-editor"); this._manifestGui?.classList.add("rendered"); } @@ -396,7 +426,7 @@ export class ManifestRenderer { // Configuration Parsing - private _processProperties(props: Record, prefix: string | null = null) { + private _processProperties(props: Record, prefix: string | null = null) { Object.keys(props).forEach((key) => { const fullKey = prefix ? `${prefix}.${key}` : key; const prop = props[key]; diff --git a/static/types/plugins.ts b/static/types/plugins.ts index 3d63a01..ee938eb 100644 --- a/static/types/plugins.ts +++ b/static/types/plugins.ts @@ -31,14 +31,11 @@ export type Manifest = { }; configuration: { type: string; - properties: { - [key: string]: { - default: unknown; - type?: string; - }; + default: string; + items?: { + type: string; }; - }; + properties?: Record; + } readme?: string; -}; - -export type ManifestProps = { type: string; default: string; items?: { type: string }; properties?: Record }; +}; \ No newline at end of file diff --git a/static/utils/element-helpers.ts b/static/utils/element-helpers.ts index d52bcfb..e608475 100644 --- a/static/utils/element-helpers.ts +++ b/static/utils/element-helpers.ts @@ -1,4 +1,4 @@ -import { ManifestProps } from "../types/plugins"; +import { Manifest } from "../types/plugins"; const CONFIG_INPUT_STR = "config-input"; @@ -19,7 +19,7 @@ export function createElement(tagName: T } export function createInputRow( key: string, - prop: ManifestProps, + prop: Manifest["configuration"], configDefaults: Record ): void { const row = document.createElement("tr"); @@ -44,7 +44,7 @@ export function createInputRow( items: prop.items ? { type: prop.items.type } : null, }; } -export function createInput(key: string, defaultValue: unknown, prop: ManifestProps): HTMLElement { +export function createInput(key: string, defaultValue: unknown, prop: Manifest["configuration"]): HTMLElement { if (!key) { throw new Error("Input name is required"); } From f207e69878c8ff2e01a712feaba5ae001c1089e0 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Thu, 21 Nov 2024 19:41:43 +0000 Subject: [PATCH 05/33] feat: reset to defaults --- static/index.html | 2 +- static/manifest-gui.css | 24 +++++++++++++++++++++--- static/scripts/render-manifest.ts | 26 ++++++++++++++++++++++++-- 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/static/index.html b/static/index.html index 79f76d5..fa71ad7 100644 --- a/static/index.html +++ b/static/index.html @@ -26,7 +26,7 @@ diff --git a/static/manifest-gui.css b/static/manifest-gui.css index 7b9b65e..ac104b5 100644 --- a/static/manifest-gui.css +++ b/static/manifest-gui.css @@ -130,7 +130,7 @@ header #uos-logo path { } #manifest-gui tfoot tr td button { - width: calc(50% - 4px); + width: calc(33% - 4px); display: inline-block; font-weight: 900; margin: 0 2px; @@ -140,8 +140,6 @@ header #uos-logo path { margin: 0 0 16px; } - - .readme-container h1, .readme-container h2, .readme-container h3 { @@ -224,6 +222,26 @@ button:active { background-color: #606060; } +button#reset-to-default { + background-color: #303030; + color: #fff; +} +button#reset-to-default:hover { + background-color: #404040; +} +button#reset-to-default:active { + background-color: #505050; +} +button#reset-to-default::before { + content: "♻️"; +} +button#reset-to-default:hover::before { + content: "Use Defaults"; +} +button#reset-to-default:active::before { + content: "♻️♻️♻️♻️♻️"; +} + button#remove::before { content: "−"; } diff --git a/static/scripts/render-manifest.ts b/static/scripts/render-manifest.ts index 299611c..9b24b02 100644 --- a/static/scripts/render-manifest.ts +++ b/static/scripts/render-manifest.ts @@ -113,11 +113,17 @@ export class ManifestRenderer { private _controlButtons(hide: boolean): void { const addButton = document.getElementById("add"); const removeButton = document.getElementById("remove"); + const resetToDefaultButton = document.getElementById("reset-to-default"); + const hideOrDisplay = hide ? "none" : "inline-block"; if (addButton) { - addButton.style.display = hide ? "none" : "inline-block"; + addButton.style.display = hideOrDisplay; } if (removeButton) { - removeButton.style.display = hide ? "none" : "inline-block"; + removeButton.style.display = hideOrDisplay; + } + + if (resetToDefaultButton) { + resetToDefaultButton.style.display = hideOrDisplay; } this._manifestGui?.classList.add("rendered"); @@ -353,6 +359,22 @@ export class ManifestRenderer { add.addEventListener("click", this._boundConfigAdd); remove.addEventListener("click", this._boundConfigRemove); + const resetToDefaultButton = document.getElementById("reset-to-default"); + + if (!resetToDefaultButton) { + throw new Error("Reset to default button not found"); + } + + resetToDefaultButton.addEventListener("click", () => { + this._renderConfigEditor(pluginManifest); + const readmeContainer = document.querySelector(".readme-container"); + if (readmeContainer) { + readmeContainer.remove(); + } + }); + + resetToDefaultButton.hidden = !!plugin; + const manifestCache = JSON.parse(localStorage.getItem("manifestCache") || "{}") as ManifestCache; const pluginUrls = Object.keys(manifestCache); const pluginUrl = pluginUrls.find((url) => { From 07bc7461428b6cde928e69c8a0d7a819cc129529 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Thu, 21 Nov 2024 19:44:16 +0000 Subject: [PATCH 06/33] chore: oauth redirect --- static/scripts/authentication.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/static/scripts/authentication.ts b/static/scripts/authentication.ts index dfd81f0..16fa585 100644 --- a/static/scripts/authentication.ts +++ b/static/scripts/authentication.ts @@ -65,7 +65,15 @@ export class AuthService { public async signInWithGithub(): Promise { const search = window.location.search; localStorage.setItem("manifest", search); - const { data } = await this.supabase.auth.signInWithOAuth({ provider: "github", options: { scopes: "read:org read:user user:email repo" } }); + const { data } = await this.supabase.auth.signInWithOAuth( + { + provider: "github", + options: { + scopes: "read:org read:user user:email repo", + redirectTo: `${window.location.href}`, + } + } + ); if (!data) throw new Error("Failed to sign in with GitHub"); } From afe5ab1e7d8e43238043ced4071ca0ab1b0fe61b Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Thu, 21 Nov 2024 20:04:48 +0000 Subject: [PATCH 07/33] chore: remove search parsing --- static/main.ts | 10 +------ static/scripts/decode-manifest.ts | 49 ------------------------------- static/scripts/fetch-manifest.ts | 25 ++++++++++++---- 3 files changed, 20 insertions(+), 64 deletions(-) delete mode 100644 static/scripts/decode-manifest.ts diff --git a/static/main.ts b/static/main.ts index 588057c..01a1317 100644 --- a/static/main.ts +++ b/static/main.ts @@ -1,5 +1,4 @@ import { AuthService } from "./scripts/authentication"; -import { ManifestDecoder } from "./scripts/decode-manifest"; import { ManifestFetcher } from "./scripts/fetch-manifest"; import { ManifestRenderer } from "./scripts/render-manifest"; import { ManifestPreDecode } from "./types/plugins"; @@ -14,18 +13,11 @@ async function handleAuth() { export async function mainModule() { const auth = await handleAuth(); - const decoder = new ManifestDecoder(); const renderer = new ManifestRenderer(auth); - const search = window.location.search.substring(1); - - if (search) { - const decodedManifest = await decoder.decodeManifestFromSearch(search); - return renderer.renderManifest(decodedManifest); - } try { const ubiquityOrgsToFetchOfficialConfigFrom = ["ubiquity-os"]; - const fetcher = new ManifestFetcher(ubiquityOrgsToFetchOfficialConfigFrom, auth.octokit, decoder); + const fetcher = new ManifestFetcher(ubiquityOrgsToFetchOfficialConfigFrom, auth.octokit); const cache = fetcher.checkManifestCache(); if (!manifestGuiBody) { throw new Error("Manifest GUI body not found"); diff --git a/static/scripts/decode-manifest.ts b/static/scripts/decode-manifest.ts deleted file mode 100644 index 623319e..0000000 --- a/static/scripts/decode-manifest.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Manifest, ManifestPreDecode } from "../types/plugins"; - -export class ManifestDecoder { - constructor() {} - - decodeManifestFromFetch(manifest: ManifestPreDecode) { - if (manifest.error) { - return null; - } - - const decodedManifest: Manifest = { - name: manifest.name, - description: manifest.description, - "ubiquity:listeners": manifest["ubiquity:listeners"], - configuration: manifest.configuration, - }; - - return decodedManifest; - } - - decodeManifestFromSearch(search: string) { - const parsed = this.stringUriParser(search); - - const encodedManifestEnvelope = parsed.find((pair) => pair["manifest"]); - if (!encodedManifestEnvelope) { - throw new Error("No encoded manifest found!"); - } - const encodedManifest = encodedManifestEnvelope["manifest"]; - const decodedManifest = decodeURI(encodedManifest); - - this.renderManifest(decodedManifest); - return JSON.parse(decodedManifest); - } - - stringUriParser(input: string): Array<{ [key: string]: string }> { - const buffer: Array<{ [key: string]: string }> = []; - const sections = input.split("&"); - for (const section of sections) { - const keyValues = section.split("="); - buffer.push({ [keyValues[0]]: keyValues[1] }); - } - return buffer; - } - - renderManifest(manifest: string) { - const dfg = document.createDocumentFragment(); - dfg.textContent = manifest; - } -} diff --git a/static/scripts/fetch-manifest.ts b/static/scripts/fetch-manifest.ts index b3fca16..fa3bd42 100644 --- a/static/scripts/fetch-manifest.ts +++ b/static/scripts/fetch-manifest.ts @@ -1,22 +1,19 @@ import { Octokit } from "@octokit/rest"; -import { ManifestDecoder } from "./decode-manifest"; -import { ManifestPreDecode } from "../types/plugins"; +import { Manifest, ManifestPreDecode } from "../types/plugins"; import { DEV_CONFIG_FULL_PATH, CONFIG_FULL_PATH, CONFIG_ORG_REPO } from "@ubiquity-os/plugin-sdk/constants"; export class ManifestFetcher { private _orgs: string[]; private _octokit: Octokit | null; - private _decoder: ManifestDecoder; workerUrlRegex = /https:\/\/([a-z0-9-]+)\.ubiquity\.workers\.dev/g; actionUrlRegex = /[a-z0-9-]+\/[a-z0-9-]+(?:\/[^@]+)?@[a-z0-9-]+/g; workerUrls = new Set(); actionUrls = new Set(); - constructor(orgs: string[], octokit: Octokit | null, decoder: ManifestDecoder) { + constructor(orgs: string[], octokit: Octokit | null) { this._orgs = orgs; this._octokit = octokit; - this._decoder = decoder; } async fetchMarketplaceManifests() { @@ -31,7 +28,7 @@ export class ManifestFetcher { for (const repo of repos.data) { const manifestUrl = makeUrl(org, repo.name, "manifest.json"); const manifest = await this.fetchActionManifest(manifestUrl); - const decoded = this._decoder.decodeManifestFromFetch(manifest); + const decoded = this.decodeManifestFromFetch(manifest); const readme = await this._fetchPluginReadme(makeUrl(org, repo.name, "README.md")); if (decoded) { @@ -198,4 +195,20 @@ export class ManifestFetcher { this.captureActionUrls(config); } } + + decodeManifestFromFetch(manifest: ManifestPreDecode) { + if (manifest.error) { + return null; + } + + const decodedManifest: Manifest = { + name: manifest.name, + description: manifest.description, + "ubiquity:listeners": manifest["ubiquity:listeners"], + configuration: manifest.configuration, + }; + + return decodedManifest; + } + } From ff91ddefd60650b8dc35d94e6ac1248b31347193 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Thu, 21 Nov 2024 20:36:41 +0000 Subject: [PATCH 08/33] chore: eslint and cleanup --- static/main.ts | 1 + static/manifest-gui.css | 10 +-- static/scripts/authentication.ts | 16 ++-- static/scripts/config-parser.ts | 10 +-- static/scripts/fetch-manifest.ts | 5 +- static/scripts/render-manifest.ts | 137 +++++++++--------------------- static/types/github.ts | 4 + static/types/plugins.ts | 6 +- static/utils/element-helpers.ts | 5 +- static/utils/strings.ts | 7 ++ 10 files changed, 76 insertions(+), 125 deletions(-) create mode 100644 static/utils/strings.ts diff --git a/static/main.ts b/static/main.ts index 01a1317..40e6e42 100644 --- a/static/main.ts +++ b/static/main.ts @@ -31,6 +31,7 @@ export async function mainModule() { const killNotification = toastNotification("Fetching manifest data...", { type: "info", shouldAutoDismiss: true }); manifestGuiBody.dataset.loading = "true"; + // eslint-disable-next-line no-async-promise-executor fetchPromise = new Promise(async (resolve) => { if (!manifestGuiBody) { throw new Error("Manifest GUI body not found"); diff --git a/static/manifest-gui.css b/static/manifest-gui.css index ac104b5..e472432 100644 --- a/static/manifest-gui.css +++ b/static/manifest-gui.css @@ -58,16 +58,16 @@ header #uos-logo path { #viewport { display: flex; - align-items: center; - justify-content: center; + align-items: center; + justify-content: center; width: 100%; - height: calc(100vh - 96px); + height: calc(100vh - 96px); } #viewport-cell { display: flex; flex-direction: column; - align-items: center; + align-items: center; width: 100%; } @@ -310,7 +310,7 @@ select option { .custom-select { position: relative; font-family: "Proxima Nova", sans-serif; - width: inherit + width: inherit; } .select-selected { diff --git a/static/scripts/authentication.ts b/static/scripts/authentication.ts index 16fa585..327614a 100644 --- a/static/scripts/authentication.ts +++ b/static/scripts/authentication.ts @@ -65,15 +65,13 @@ export class AuthService { public async signInWithGithub(): Promise { const search = window.location.search; localStorage.setItem("manifest", search); - const { data } = await this.supabase.auth.signInWithOAuth( - { - provider: "github", - options: { - scopes: "read:org read:user user:email repo", - redirectTo: `${window.location.href}`, - } - } - ); + const { data } = await this.supabase.auth.signInWithOAuth({ + provider: "github", + options: { + scopes: "read:org read:user user:email repo", + redirectTo: `${window.location.href}`, + }, + }); if (!data) throw new Error("Failed to sign in with GitHub"); } diff --git a/static/scripts/config-parser.ts b/static/scripts/config-parser.ts index c331090..ff43695 100644 --- a/static/scripts/config-parser.ts +++ b/static/scripts/config-parser.ts @@ -50,7 +50,7 @@ export class ConfigParser { const { data } = await octokit.repos.getContent({ owner: org, repo, - path + path, }); return data; @@ -108,13 +108,7 @@ export class ConfigParser { return YAML.parse(`${this.newConfigYml}`); } - async updateConfig( - org: string, - octokit: Octokit, - option: "add" | "remove", - path = CONFIG_FULL_PATH, - repo = CONFIG_ORG_REPO - ) { + async updateConfig(org: string, octokit: Octokit, option: "add" | "remove", path = CONFIG_FULL_PATH, repo = CONFIG_ORG_REPO) { let repoPlugins = this.parseConfig(this.repoConfig).plugins; const newPlugins = this.parseConfig().plugins; diff --git a/static/scripts/fetch-manifest.ts b/static/scripts/fetch-manifest.ts index fa3bd42..ca09647 100644 --- a/static/scripts/fetch-manifest.ts +++ b/static/scripts/fetch-manifest.ts @@ -23,7 +23,9 @@ export class ManifestFetcher { } const repos = await this._octokit.repos.listForOrg({ org }); const manifestCache = this.checkManifestCache(); - function makeUrl(org: string, repo: string, file: string) { return `https://raw.githubusercontent.com/${org}/${repo}/development/${file}` }; + function makeUrl(org: string, repo: string, file: string) { + return `https://raw.githubusercontent.com/${org}/${repo}/development/${file}`; + } for (const repo of repos.data) { const manifestUrl = makeUrl(org, repo.name, "manifest.json"); @@ -210,5 +212,4 @@ export class ManifestFetcher { return decodedManifest; } - } diff --git a/static/scripts/render-manifest.ts b/static/scripts/render-manifest.ts index 9b24b02..4bfd444 100644 --- a/static/scripts/render-manifest.ts +++ b/static/scripts/render-manifest.ts @@ -4,19 +4,11 @@ import { AuthService } from "./authentication"; import AJV, { AnySchemaObject } from "ajv"; import { createElement, createInputRow } from "../utils/element-helpers"; import { toastNotification } from "../utils/toaster"; +import { ExtendedHtmlElement } from "../types/github"; +import { STRINGS } from "../utils/strings"; const ajv = new AJV({ allErrors: true, coerceTypes: true, strict: true }); -const TDV_CENTERED = "table-data-value centered"; -const SELECT_ITEMS = ".select-items" -const SELECT_SELECTED = ".select-selected" -const SELECT_HIDE = "select-hide" -const SELECT_ARROW_ACTIVE = "select-arrow-active" - -type ExtendedHtmlElement = { - [key in keyof T]: T[key] extends HTMLElement["innerHTML"] ? string | null : T[key]; -}; - export class ManifestRenderer { private _manifestGui: HTMLElement; private _manifestGuiBody: ExtendedHtmlElement; @@ -83,15 +75,17 @@ export class ManifestRenderer { localStorage.setItem("selectedOrg", org); if (fetchPromise) { - fetchPromise.then((manifestCache) => { - localStorage.setItem("manifestCache", JSON.stringify(manifestCache)); - }).catch((error) => { - console.error("Error fetching manifest cache:", error); - toastNotification(`An error occurred while fetching the manifest cache: ${String(error)}`, { - type: "error", - shouldAutoDismiss: true, + fetchPromise + .then((manifestCache) => { + localStorage.setItem("manifestCache", JSON.stringify(manifestCache)); + }) + .catch((error) => { + console.error("Error fetching manifest cache:", error); + toastNotification(`An error occurred while fetching the manifest cache: ${String(error)}`, { + type: "error", + shouldAutoDismiss: true, + }); }); - }); const fetchOrgConfig = async () => { const octokit = this._auth.octokit; @@ -100,12 +94,11 @@ export class ManifestRenderer { } await this._configParser.fetchUserInstalledConfig(org, octokit); this._renderPluginSelector(); - } + }; fetchOrgConfig().catch(console.error); } else { this._renderPluginSelector(); } - } // UI Rendering @@ -140,7 +133,7 @@ export class ManifestRenderer { const pickerRow = document.createElement("tr"); const pickerCell = document.createElement("td"); pickerCell.colSpan = 4; - pickerCell.className = TDV_CENTERED; + pickerCell.className = STRINGS.TDV_CENTERED; const customSelect = createElement("div", { class: "custom-select" }); @@ -191,23 +184,12 @@ export class ManifestRenderer { selectSelected.addEventListener("click", (e) => { e.stopPropagation(); - closeAllSelect(); - selectItems.classList.toggle(SELECT_HIDE); - selectSelected.classList.toggle(SELECT_ARROW_ACTIVE); + this._closeAllSelect(); + selectItems.classList.toggle(STRINGS.SELECT_HIDE); + selectSelected.classList.toggle(STRINGS.SELECT_ARROW_ACTIVE); }); - function closeAllSelect() { - const selectItemsList = document.querySelectorAll(SELECT_ITEMS); - const selectSelectedList = document.querySelectorAll(SELECT_SELECTED); - selectItemsList.forEach((item) => { - item.classList.add(SELECT_HIDE); - }); - selectSelectedList.forEach((item) => { - item.classList.remove(SELECT_ARROW_ACTIVE); - }); - } - - document.addEventListener("click", closeAllSelect); + document.addEventListener("click", this._closeAllSelect); } private _renderPluginSelector(): void { @@ -222,13 +204,13 @@ export class ManifestRenderer { const pickerRow = document.createElement("tr"); const pickerCell = document.createElement("td"); pickerCell.colSpan = 2; - pickerCell.className = TDV_CENTERED; + pickerCell.className = STRINGS.TDV_CENTERED; const userConfig = this._configParser.repoConfig; let installedPlugins: Plugin[] = []; if (userConfig) { - installedPlugins = this._configParser.parseConfig(userConfig).plugins + installedPlugins = this._configParser.parseConfig(userConfig).plugins; } const cleanManifestCache = Object.keys(manifestCache).reduce((acc, key) => { @@ -278,7 +260,7 @@ export class ManifestRenderer { optionDiv.addEventListener("click", () => { selectSelected.textContent = optionText; - closeAllSelect(); + this._closeAllSelect(); localStorage.setItem("selectedPluginManifest", JSON.stringify(defaultForInstalled)); this._renderConfigEditor(defaultForInstalled, installedPlugin?.uses[0].with); }); @@ -288,26 +270,25 @@ export class ManifestRenderer { selectSelected.addEventListener("click", (e) => { e.stopPropagation(); - closeAllSelect(); - selectItems.classList.toggle(SELECT_HIDE); - selectSelected.classList.toggle(SELECT_ARROW_ACTIVE); + this._closeAllSelect(); + selectItems.classList.toggle(STRINGS.SELECT_HIDE); + selectSelected.classList.toggle(STRINGS.SELECT_ARROW_ACTIVE); }); - function closeAllSelect() { - const selectItemsList = document.querySelectorAll(SELECT_ITEMS); - const selectSelectedList = document.querySelectorAll(SELECT_SELECTED); - selectItemsList.forEach((item) => { - item.classList.add(SELECT_HIDE); - }); - selectSelectedList.forEach((item) => { - item.classList.remove(SELECT_ARROW_ACTIVE); - }); - } - - document.addEventListener("click", closeAllSelect); this._updateGuiTitle(`Select a Plugin`); } + private _closeAllSelect() { + const selectItemsList = document.querySelectorAll(STRINGS.SELECT_ITEMS); + const selectSelectedList = document.querySelectorAll(STRINGS.SELECT_SELECTED); + selectItemsList.forEach((item) => { + item.classList.add(STRINGS.SELECT_HIDE); + }); + selectSelectedList.forEach((item) => { + item.classList.remove(STRINGS.SELECT_ARROW_ACTIVE); + }); + } + private _boundConfigAdd = this._writeNewConfig.bind(this, "add"); private _boundConfigRemove = this._writeNewConfig.bind(this, "remove"); private _renderConfigEditor(pluginManifest: Manifest | null, plugin?: Plugin["uses"][0]["with"]): void { @@ -326,7 +307,7 @@ export class ManifestRenderer { } const keys = key.split("."); - let currentObj = plugin + let currentObj = plugin; for (let i = 0; i < keys.length; i++) { if (!currentObj[keys[i]]) { break; @@ -347,8 +328,7 @@ export class ManifestRenderer { } else { (input as HTMLInputElement).value = value; } - } - ); + }); } const add = document.getElementById("add"); @@ -379,7 +359,7 @@ export class ManifestRenderer { const pluginUrls = Object.keys(manifestCache); const pluginUrl = pluginUrls.find((url) => { return manifestCache[url].name === pluginManifest?.name; - }) + }); if (!pluginUrl) { throw new Error("Plugin URL not found"); @@ -392,7 +372,7 @@ export class ManifestRenderer { throw new Error("Viewport cell not found"); } const readmeContainer = document.createElement("div"); - readmeContainer.className = 'readme-container'; + readmeContainer.className = "readme-container"; readmeContainer.innerHTML = readme; viewportCell.insertAdjacentElement("afterend", readmeContainer); } @@ -410,42 +390,6 @@ export class ManifestRenderer { guiTitle.textContent = title; } - renderManifest(decodedManifest: ManifestPreDecode) { - if (!decodedManifest) { - throw new Error("No decoded manifest found!"); - } - this._manifestGui?.classList.add("rendering"); - this._manifestGuiBody.innerHTML = null; - - const table = document.createElement("table"); - Object.entries(decodedManifest).forEach(([key, value]) => { - const row = document.createElement("tr"); - - const headerCell = document.createElement("td"); - headerCell.className = "table-data-header"; - headerCell.textContent = key.replace("ubiquity:", ""); - row.appendChild(headerCell); - - const valueCell = document.createElement("td"); - valueCell.className = "table-data-value"; - - if (typeof value === "string") { - valueCell.textContent = value; - } else { - const pre = document.createElement("pre"); - pre.textContent = JSON.stringify(value, null, 2); - valueCell.appendChild(pre); - } - - row.appendChild(valueCell); - table.appendChild(row); - }); - - this._manifestGuiBody.appendChild(table); - - this._manifestGui?.classList.add("rendered"); - } - // Configuration Parsing private _processProperties(props: Record, prefix: string | null = null) { @@ -569,8 +513,7 @@ export class ManifestRenderer { private _handleAddPlugin(plugin: Plugin, pluginManifest: Manifest): void { this._configParser.addPlugin(plugin); - toastNotification(`Configuration for ${pluginManifest.name - } saved successfully.Do you want to push to GitHub ? `, { + toastNotification(`Configuration for ${pluginManifest.name} saved successfully.Do you want to push to GitHub ? `, { type: "success", actionText: "Push to GitHub", action: async () => { diff --git a/static/types/github.ts b/static/types/github.ts index f1481bf..96a9b94 100644 --- a/static/types/github.ts +++ b/static/types/github.ts @@ -2,3 +2,7 @@ import { RestEndpointMethodTypes } from "@octokit/plugin-rest-endpoint-methods"; export type GitHubUserResponse = RestEndpointMethodTypes["users"]["getByUsername"]["response"]; export type GitHubUser = GitHubUserResponse["data"]; + +export type ExtendedHtmlElement = { + [key in keyof T]: T[key] extends HTMLElement["innerHTML"] ? string | null : T[key]; +}; diff --git a/static/types/plugins.ts b/static/types/plugins.ts index ee938eb..49f1914 100644 --- a/static/types/plugins.ts +++ b/static/types/plugins.ts @@ -31,11 +31,11 @@ export type Manifest = { }; configuration: { type: string; - default: string; + default: object; items?: { type: string; }; properties?: Record; - } + }; readme?: string; -}; \ No newline at end of file +}; diff --git a/static/utils/element-helpers.ts b/static/utils/element-helpers.ts index e608475..9b4011a 100644 --- a/static/utils/element-helpers.ts +++ b/static/utils/element-helpers.ts @@ -4,7 +4,10 @@ const CONFIG_INPUT_STR = "config-input"; export const manifestGuiBody = document.getElementById("manifest-gui-body"); -export function createElement(tagName: TK, attributes: { [key: string]: string | boolean | null }): HTMLElementTagNameMap[TK] { +export function createElement( + tagName: TK, + attributes: { [key: string]: string | boolean | null } +): HTMLElementTagNameMap[TK] { const element = document.createElement(tagName); Object.keys(attributes).forEach((key) => { if (key === "textContent") { diff --git a/static/utils/strings.ts b/static/utils/strings.ts new file mode 100644 index 0000000..23dffe2 --- /dev/null +++ b/static/utils/strings.ts @@ -0,0 +1,7 @@ +export const STRINGS = { + TDV_CENTERED: "table-data-value centered", + SELECT_ITEMS: ".select-items", + SELECT_SELECTED: ".select-selected", + SELECT_HIDE: "select-hide", + SELECT_ARROW_ACTIVE: "select-arrow-active", +}; From 61a536bd1f2ad02a19d6d271c1b56a9cde21af07 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Thu, 21 Nov 2024 21:30:55 +0000 Subject: [PATCH 09/33] chore: split renderer into files --- static/scripts/render-manifest.ts | 561 ++----------------- static/scripts/rendering/config-editor.ts | 97 ++++ static/scripts/rendering/control-buttons.ts | 24 + static/scripts/rendering/input-parsing.ts | 76 +++ static/scripts/rendering/navigation.ts | 28 + static/scripts/rendering/org-select.ts | 111 ++++ static/scripts/rendering/plugin-select.ts | 92 +++ static/scripts/rendering/utils.ts | 20 + static/scripts/rendering/write-add-remove.ts | 131 +++++ 9 files changed, 612 insertions(+), 528 deletions(-) create mode 100644 static/scripts/rendering/config-editor.ts create mode 100644 static/scripts/rendering/control-buttons.ts create mode 100644 static/scripts/rendering/input-parsing.ts create mode 100644 static/scripts/rendering/navigation.ts create mode 100644 static/scripts/rendering/org-select.ts create mode 100644 static/scripts/rendering/plugin-select.ts create mode 100644 static/scripts/rendering/utils.ts create mode 100644 static/scripts/rendering/write-add-remove.ts diff --git a/static/scripts/render-manifest.ts b/static/scripts/render-manifest.ts index 4bfd444..50dc897 100644 --- a/static/scripts/render-manifest.ts +++ b/static/scripts/render-manifest.ts @@ -1,13 +1,8 @@ -import { ManifestCache, ManifestPreDecode, Plugin, Manifest } from "../types/plugins"; import { ConfigParser } from "./config-parser"; import { AuthService } from "./authentication"; -import AJV, { AnySchemaObject } from "ajv"; -import { createElement, createInputRow } from "../utils/element-helpers"; -import { toastNotification } from "../utils/toaster"; import { ExtendedHtmlElement } from "../types/github"; -import { STRINGS } from "../utils/strings"; - -const ajv = new AJV({ allErrors: true, coerceTypes: true, strict: true }); +import { controlButtons } from "./rendering/control-buttons"; +import { createBackButton } from "./rendering/navigation"; export class ManifestRenderer { private _manifestGui: HTMLElement; @@ -30,556 +25,66 @@ export class ManifestRenderer { this._manifestGui = manifestGui as HTMLElement; this._manifestGuiBody = manifestGuiBody as HTMLElement; - this._controlButtons(true); - - this._backButton = createElement("button", { - id: "back-button", - class: "button", - textContent: "Back", - }) as HTMLButtonElement; + controlButtons({ hide: true }); const title = manifestGui.querySelector("#manifest-gui-title"); + this._backButton = createBackButton(this, this._currentStep); title?.previousSibling?.appendChild(this._backButton); - this._backButton.style.display = "none"; - this._backButton.addEventListener("click", this._handleBackButtonClick.bind(this)); } - private _handleBackButtonClick(): void { - switch (this._currentStep) { - case "pluginSelector": { - this.renderOrgPicker(this._orgs); - break; - } - case "configEditor": { - this._renderPluginSelector(); - break; - } - default: - break; - } - - const readmeContainer = document.querySelector(".readme-container"); - if (readmeContainer) { - readmeContainer.remove(); - this._manifestGui?.classList.remove("plugin-editor"); - } + get orgs(): string[] { + return this._orgs; } - // Event Handlers - - private _handleOrgSelection(org: string, fetchPromise?: Promise>): void { - if (!org) { - throw new Error("No org selected"); - } - - localStorage.setItem("selectedOrg", org); - - if (fetchPromise) { - fetchPromise - .then((manifestCache) => { - localStorage.setItem("manifestCache", JSON.stringify(manifestCache)); - }) - .catch((error) => { - console.error("Error fetching manifest cache:", error); - toastNotification(`An error occurred while fetching the manifest cache: ${String(error)}`, { - type: "error", - shouldAutoDismiss: true, - }); - }); - - const fetchOrgConfig = async () => { - const octokit = this._auth.octokit; - if (!octokit) { - throw new Error("No org or octokit found"); - } - await this._configParser.fetchUserInstalledConfig(org, octokit); - this._renderPluginSelector(); - }; - fetchOrgConfig().catch(console.error); - } else { - this._renderPluginSelector(); - } + set orgs(orgs: string[]) { + this._orgs = orgs; } - // UI Rendering - - private _controlButtons(hide: boolean): void { - const addButton = document.getElementById("add"); - const removeButton = document.getElementById("remove"); - const resetToDefaultButton = document.getElementById("reset-to-default"); - const hideOrDisplay = hide ? "none" : "inline-block"; - if (addButton) { - addButton.style.display = hideOrDisplay; - } - if (removeButton) { - removeButton.style.display = hideOrDisplay; - } - - if (resetToDefaultButton) { - resetToDefaultButton.style.display = hideOrDisplay; - } - - this._manifestGui?.classList.add("rendered"); + get currentStep(): "orgPicker" | "pluginSelector" | "configEditor" { + return this._currentStep; } - public renderOrgPicker(orgs: string[], fetchPromise?: Promise>): void { - this._orgs = orgs; - this._currentStep = "orgPicker"; - this._controlButtons(true); - this._backButton.style.display = "none"; - this._manifestGui?.classList.add("rendering"); - this._manifestGuiBody.innerHTML = null; - - const pickerRow = document.createElement("tr"); - const pickerCell = document.createElement("td"); - pickerCell.colSpan = 4; - pickerCell.className = STRINGS.TDV_CENTERED; - - const customSelect = createElement("div", { class: "custom-select" }); - - const selectSelected = createElement("div", { - class: "select-selected", - textContent: "Select an organization", - }); - - const selectItems = createElement("div", { - class: "select-items select-hide", - }); - - customSelect.appendChild(selectSelected); - customSelect.appendChild(selectItems); - - pickerCell.appendChild(customSelect); - pickerRow.appendChild(pickerCell); - - this._manifestGuiBody.appendChild(pickerRow); - this._manifestGui?.classList.add("rendered"); - - if (!orgs.length) { - const hasSession = this._auth.isActiveSession(); - if (hasSession) { - this._updateGuiTitle("No organizations found"); - } else { - this._updateGuiTitle("Please sign in to GitHub"); - } - return; - } - - this._updateGuiTitle("Select an Organization"); - - orgs.forEach((org) => { - const optionDiv = createElement("div", { class: "select-option" }); - const textSpan = createElement("span", { textContent: org }); - - optionDiv.appendChild(textSpan); - - optionDiv.addEventListener("click", () => { - this._handleOrgSelection(org, fetchPromise); - selectSelected.textContent = org; - localStorage.setItem("selectedOrg", org); - }); - - selectItems.appendChild(optionDiv); - }); - - selectSelected.addEventListener("click", (e) => { - e.stopPropagation(); - this._closeAllSelect(); - selectItems.classList.toggle(STRINGS.SELECT_HIDE); - selectSelected.classList.toggle(STRINGS.SELECT_ARROW_ACTIVE); - }); - - document.addEventListener("click", this._closeAllSelect); + set currentStep(step: "orgPicker" | "pluginSelector" | "configEditor") { + this._currentStep = step; } - private _renderPluginSelector(): void { - this._currentStep = "pluginSelector"; - this._backButton.style.display = "block"; - this._manifestGuiBody.innerHTML = null; - this._controlButtons(true); - - const manifestCache = JSON.parse(localStorage.getItem("manifestCache") || "{}") as ManifestCache; - const pluginUrls = Object.keys(manifestCache); - - const pickerRow = document.createElement("tr"); - const pickerCell = document.createElement("td"); - pickerCell.colSpan = 2; - pickerCell.className = STRINGS.TDV_CENTERED; - - const userConfig = this._configParser.repoConfig; - let installedPlugins: Plugin[] = []; - - if (userConfig) { - installedPlugins = this._configParser.parseConfig(userConfig).plugins; - } - - const cleanManifestCache = Object.keys(manifestCache).reduce((acc, key) => { - if (manifestCache[key]?.name) { - acc[key] = manifestCache[key]; - } - return acc; - }, {} as ManifestCache); - - const customSelect = createElement("div", { class: "custom-select" }); - - const selectSelected = createElement("div", { - class: "select-selected", - textContent: "Select a plugin", - }); - - const selectItems = createElement("div", { - class: "select-items select-hide", - }); - - customSelect.appendChild(selectSelected); - customSelect.appendChild(selectItems); - - pickerCell.appendChild(customSelect); - pickerRow.appendChild(pickerCell); - - this._manifestGuiBody.appendChild(pickerRow); - - pluginUrls.forEach((url) => { - if (!cleanManifestCache[url]?.name) { - return; - } - - const [, repo] = url.replace("https://raw.githubusercontent.com/", "").split("/"); - const reg = new RegExp(`${repo}`, "gi"); - const installedPlugin: Plugin | undefined = installedPlugins.find((plugin) => plugin.uses[0].plugin.match(reg)); - const defaultForInstalled: ManifestPreDecode | null = cleanManifestCache[url]; - const optionText = defaultForInstalled.name; - const indicator = installedPlugin ? "🟢" : "🔴"; - - const optionDiv = createElement("div", { class: "select-option" }); - const textSpan = createElement("span", { textContent: optionText }); - const indicatorSpan = createElement("span", { textContent: indicator }); - - optionDiv.appendChild(textSpan); - optionDiv.appendChild(indicatorSpan); - - optionDiv.addEventListener("click", () => { - selectSelected.textContent = optionText; - this._closeAllSelect(); - localStorage.setItem("selectedPluginManifest", JSON.stringify(defaultForInstalled)); - this._renderConfigEditor(defaultForInstalled, installedPlugin?.uses[0].with); - }); - - selectItems.appendChild(optionDiv); - }); - - selectSelected.addEventListener("click", (e) => { - e.stopPropagation(); - this._closeAllSelect(); - selectItems.classList.toggle(STRINGS.SELECT_HIDE); - selectSelected.classList.toggle(STRINGS.SELECT_ARROW_ACTIVE); - }); - - this._updateGuiTitle(`Select a Plugin`); + get backButton(): HTMLButtonElement { + return this._backButton; } - private _closeAllSelect() { - const selectItemsList = document.querySelectorAll(STRINGS.SELECT_ITEMS); - const selectSelectedList = document.querySelectorAll(STRINGS.SELECT_SELECTED); - selectItemsList.forEach((item) => { - item.classList.add(STRINGS.SELECT_HIDE); - }); - selectSelectedList.forEach((item) => { - item.classList.remove(STRINGS.SELECT_ARROW_ACTIVE); - }); + set backButton(button: HTMLButtonElement) { + this._backButton = button; } - private _boundConfigAdd = this._writeNewConfig.bind(this, "add"); - private _boundConfigRemove = this._writeNewConfig.bind(this, "remove"); - private _renderConfigEditor(pluginManifest: Manifest | null, plugin?: Plugin["uses"][0]["with"]): void { - this._currentStep = "configEditor"; - this._backButton.style.display = "block"; - this._manifestGuiBody.innerHTML = null; - this._controlButtons(false); - this._processProperties(pluginManifest?.configuration?.properties || {}); - const configInputs = document.querySelectorAll(".config-input"); - - if (plugin) { - configInputs.forEach((input) => { - const key = input.getAttribute("data-config-key"); - if (!key) { - throw new Error("Input key is required"); - } - - const keys = key.split("."); - let currentObj = plugin; - for (let i = 0; i < keys.length; i++) { - if (!currentObj[keys[i]]) { - break; - } - currentObj = currentObj[keys[i]] as Record; - } - - let value: string; - - if (typeof currentObj === "object") { - value = JSON.stringify(currentObj, null, 2); - } else { - value = currentObj as string; - } - - if (input.tagName === "TEXTAREA") { - (input as HTMLTextAreaElement).value = value; - } else { - (input as HTMLInputElement).value = value; - } - }); - } - - const add = document.getElementById("add"); - const remove = document.getElementById("remove"); - if (!add || !remove) { - throw new Error("Add or remove button not found"); - } - add.addEventListener("click", this._boundConfigAdd); - remove.addEventListener("click", this._boundConfigRemove); - - const resetToDefaultButton = document.getElementById("reset-to-default"); - - if (!resetToDefaultButton) { - throw new Error("Reset to default button not found"); - } - - resetToDefaultButton.addEventListener("click", () => { - this._renderConfigEditor(pluginManifest); - const readmeContainer = document.querySelector(".readme-container"); - if (readmeContainer) { - readmeContainer.remove(); - } - }); - - resetToDefaultButton.hidden = !!plugin; - - const manifestCache = JSON.parse(localStorage.getItem("manifestCache") || "{}") as ManifestCache; - const pluginUrls = Object.keys(manifestCache); - const pluginUrl = pluginUrls.find((url) => { - return manifestCache[url].name === pluginManifest?.name; - }); - - if (!pluginUrl) { - throw new Error("Plugin URL not found"); - } - const readme = manifestCache[pluginUrl].readme; - - if (readme) { - const viewportCell = document.getElementById("viewport-cell"); - if (!viewportCell) { - throw new Error("Viewport cell not found"); - } - const readmeContainer = document.createElement("div"); - readmeContainer.className = "readme-container"; - readmeContainer.innerHTML = readme; - viewportCell.insertAdjacentElement("afterend", readmeContainer); - } - - this._updateGuiTitle(`Editing Configuration for ${pluginManifest?.name}`); - this._manifestGui?.classList.add("plugin-editor"); - this._manifestGui?.classList.add("rendered"); + get manifestGui(): HTMLElement { + return this._manifestGui; } - private _updateGuiTitle(title: string): void { - const guiTitle = document.querySelector("#manifest-gui-title"); - if (!guiTitle) { - throw new Error("GUI Title not found"); - } - guiTitle.textContent = title; + set manifestGui(gui: HTMLElement) { + this._manifestGui = gui; } - // Configuration Parsing - - private _processProperties(props: Record, prefix: string | null = null) { - Object.keys(props).forEach((key) => { - const fullKey = prefix ? `${prefix}.${key}` : key; - const prop = props[key]; - - if (prop.type === "object" && prop.properties) { - this._processProperties(prop.properties, fullKey); - } else { - createInputRow(fullKey, prop, this._configDefaults); - } - }); + get manifestGuiBody(): ExtendedHtmlElement { + return this._manifestGuiBody; } - private _parseConfigInputs(configInputs: NodeListOf, manifest: Manifest): { [key: string]: unknown } { - const config: Record = {}; - const schema = manifest.configuration; - if (!schema) { - throw new Error("No schema found in manifest"); - } - const validate = ajv.compile(schema as AnySchemaObject); - - configInputs.forEach((input) => { - const key = input.getAttribute("data-config-key"); - if (!key) { - throw new Error("Input key is required"); - } - - const keys = key.split("."); - - let currentObj = config; - for (let i = 0; i < keys.length - 1; i++) { - const part = keys[i]; - if (!currentObj[part] || typeof currentObj[part] !== "object") { - currentObj[part] = {}; - } - currentObj = currentObj[part] as Record; - } - - let value: unknown; - const expectedType = input.getAttribute("data-type"); - - if (expectedType === "boolean") { - value = (input as HTMLInputElement).checked; - } else if (expectedType === "object" || expectedType === "array") { - try { - value = JSON.parse((input as HTMLTextAreaElement).value); - } catch (e) { - console.error(e); - throw new Error(`Invalid JSON input for ${expectedType} at key "${key}": ${input.value}`); - } - } else { - value = (input as HTMLInputElement).value; - } - - currentObj[keys[keys.length - 1]] = value; - }); - - if (validate(config)) { - return config; - } else { - throw new Error("Invalid configuration: " + JSON.stringify(validate.errors, null, 2)); - } + set manifestGuiBody(body: ExtendedHtmlElement) { + this._manifestGuiBody = body; } - private _writeNewConfig(option: "add" | "remove"): void { - const selectedManifest = localStorage.getItem("selectedPluginManifest"); - if (!selectedManifest) { - toastNotification("No selected plugin manifest found.", { - type: "error", - shouldAutoDismiss: true, - }); - throw new Error("No selected plugin manifest found"); - } - const pluginManifest = JSON.parse(selectedManifest) as Manifest; - const configInputs = document.querySelectorAll(".config-input"); - - const newConfig = this._parseConfigInputs(configInputs, pluginManifest); - - this._configParser.loadConfig(); - - const officialPluginConfig: Record = JSON.parse(localStorage.getItem("officialPluginConfig") || "{}"); - - const pluginName = pluginManifest.name; - - // this relies on the manifest matching the repo name - const normalizedPluginName = pluginName - .toLowerCase() - .replace(/ /g, "-") - .replace(/[^a-z0-9-]/g, "") - .replace(/-+/g, "-"); - - const pluginUrl = Object.keys(officialPluginConfig).find((url) => { - return url.includes(normalizedPluginName); - }); - - if (!pluginUrl) { - toastNotification(`No plugin URL found for ${pluginName}.`, { - type: "error", - shouldAutoDismiss: true, - }); - throw new Error("No plugin URL found"); - } - - const plugin: Plugin = { - uses: [ - { - plugin: pluginUrl, - with: newConfig, - }, - ], - }; - - if (option === "add") { - this._handleAddPlugin(plugin, pluginManifest); - } else { - this._handleRemovePlugin(plugin, pluginManifest); - } + get auth(): AuthService { + return this._auth; } - private _handleAddPlugin(plugin: Plugin, pluginManifest: Manifest): void { - this._configParser.addPlugin(plugin); - toastNotification(`Configuration for ${pluginManifest.name} saved successfully.Do you want to push to GitHub ? `, { - type: "success", - actionText: "Push to GitHub", - action: async () => { - const octokit = this._auth.octokit; - if (!octokit) { - throw new Error("Octokit not found"); - } - - const org = localStorage.getItem("selectedOrg"); - - if (!org) { - throw new Error("No selected org found"); - } - - try { - await this._configParser.updateConfig(org, octokit, "add"); - } catch (error) { - console.error("Error pushing config to GitHub:", error); - toastNotification("An error occurred while pushing the configuration to GitHub.", { - type: "error", - shouldAutoDismiss: true, - }); - return; - } - - toastNotification("Configuration pushed to GitHub successfully.", { - type: "success", - shouldAutoDismiss: true, - }); - }, - }); + get configParser(): ConfigParser { + return this._configParser; } - private _handleRemovePlugin(plugin: Plugin, pluginManifest: Manifest): void { - this._configParser.removePlugin(plugin); - toastNotification(`Configuration for ${pluginManifest.name} removed successfully.Do you want to push to GitHub ? `, { - type: "success", - actionText: "Push to GitHub", - action: async () => { - const octokit = this._auth.octokit; - if (!octokit) { - throw new Error("Octokit not found"); - } - - const org = localStorage.getItem("selectedOrg"); - - if (!org) { - throw new Error("No selected org found"); - } - - try { - await this._configParser.updateConfig(org, octokit, "remove"); - } catch (error) { - console.error("Error pushing config to GitHub:", error); - toastNotification("An error occurred while pushing the configuration to GitHub.", { - type: "error", - shouldAutoDismiss: true, - }); - return; - } + get configDefaults(): { [key: string]: { type: string; value: string; items: { type: string } | null } } { + return this._configDefaults; + } - toastNotification("Configuration pushed to GitHub successfully.", { - type: "success", - shouldAutoDismiss: true, - }); - }, - }); + set configDefaults(defaults: { [key: string]: { type: string; value: string; items: { type: string } | null } }) { + this._configDefaults = defaults; } } diff --git a/static/scripts/rendering/config-editor.ts b/static/scripts/rendering/config-editor.ts new file mode 100644 index 0000000..c85dd40 --- /dev/null +++ b/static/scripts/rendering/config-editor.ts @@ -0,0 +1,97 @@ +import { Manifest } from "@ubiquity-os/ubiquity-os-kernel"; +import { ManifestCache, Plugin } from "../../types/plugins"; +import { controlButtons } from "./control-buttons"; +import { ManifestRenderer } from "../render-manifest"; +import { processProperties } from "./input-parsing"; +import { updateGuiTitle } from "./utils"; +import { writeNewConfig } from "./write-add-remove"; + +export function renderConfigEditor(renderer: ManifestRenderer, pluginManifest: Manifest | null, plugin?: Plugin["uses"][0]["with"]): void { + renderer.currentStep = "configEditor"; + renderer.backButton.style.display = "block"; + renderer.manifestGuiBody.innerHTML = null; + controlButtons({ hide: false }); + processProperties(renderer, pluginManifest?.configuration?.properties || {}); + const configInputs = document.querySelectorAll(".config-input"); + + if (plugin) { + configInputs.forEach((input) => { + const key = input.getAttribute("data-config-key"); + if (!key) { + throw new Error("Input key is required"); + } + + const keys = key.split("."); + let currentObj = plugin; + for (let i = 0; i < keys.length; i++) { + if (!currentObj[keys[i]]) { + break; + } + currentObj = currentObj[keys[i]] as Record; + } + + let value: string; + + if (typeof currentObj === "object") { + value = JSON.stringify(currentObj, null, 2); + } else { + value = currentObj as string; + } + + if (input.tagName === "TEXTAREA") { + (input as HTMLTextAreaElement).value = value; + } else { + (input as HTMLInputElement).value = value; + } + }); + } + + const add = document.getElementById("add"); + const remove = document.getElementById("remove"); + if (!add || !remove) { + throw new Error("Add or remove button not found"); + } + add.addEventListener("click", writeNewConfig.bind(null, renderer, "add")); + remove.addEventListener("click", () => writeNewConfig.bind(null, renderer, "remove")); + + const resetToDefaultButton = document.getElementById("reset-to-default"); + if (!resetToDefaultButton) { + throw new Error("Reset to default button not found"); + } + + resetToDefaultButton.addEventListener("click", () => { + renderConfigEditor(renderer, pluginManifest); + const readmeContainer = document.querySelector(".readme-container"); + if (readmeContainer) { + readmeContainer.remove(); + } + }); + + resetToDefaultButton.hidden = !!plugin; + + const manifestCache = JSON.parse(localStorage.getItem("manifestCache") || "{}") as ManifestCache; + const pluginUrls = Object.keys(manifestCache); + const pluginUrl = pluginUrls.find((url) => { + return manifestCache[url].name === pluginManifest?.name; + }); + + if (!pluginUrl) { + throw new Error("Plugin URL not found"); + } + const readme = manifestCache[pluginUrl].readme; + + if (readme) { + const viewportCell = document.getElementById("viewport-cell"); + if (!viewportCell) { + throw new Error("Viewport cell not found"); + } + const readmeContainer = document.createElement("div"); + readmeContainer.className = "readme-container"; + readmeContainer.innerHTML = readme; + viewportCell.insertAdjacentElement("afterend", readmeContainer); + } + + updateGuiTitle(`Editing Configuration for ${pluginManifest?.name}`); + renderer.manifestGui?.classList.add("plugin-editor"); + renderer.manifestGui?.classList.add("rendered"); +} diff --git a/static/scripts/rendering/control-buttons.ts b/static/scripts/rendering/control-buttons.ts new file mode 100644 index 0000000..e95e630 --- /dev/null +++ b/static/scripts/rendering/control-buttons.ts @@ -0,0 +1,24 @@ +import { manifestGuiBody } from "../../utils/element-helpers"; + +export function controlButtons({ hide }: { hide: boolean }): void { + const addButton = document.getElementById("add"); + const removeButton = document.getElementById("remove"); + const resetToDefaultButton = document.getElementById("reset-to-default"); + const hideOrDisplay = hide ? "none" : "inline-block"; + if (addButton) { + addButton.style.display = hideOrDisplay; + } + if (removeButton) { + removeButton.style.display = hideOrDisplay; + } + + if (resetToDefaultButton) { + resetToDefaultButton.style.display = hideOrDisplay; + } + + if (!manifestGuiBody) { + return; + } + + manifestGuiBody.classList.add("rendered"); +} diff --git a/static/scripts/rendering/input-parsing.ts b/static/scripts/rendering/input-parsing.ts new file mode 100644 index 0000000..c9f65a5 --- /dev/null +++ b/static/scripts/rendering/input-parsing.ts @@ -0,0 +1,76 @@ +import AJV, { AnySchemaObject } from "ajv"; +import { createInputRow } from "../../utils/element-helpers"; +import { ManifestRenderer } from "../render-manifest"; +import { Manifest } from "../../types/plugins"; +const ajv = new AJV({ allErrors: true, coerceTypes: true, strict: true }); + +export function processProperties(renderer: ManifestRenderer, props: Record, prefix: string | null = null) { + Object.keys(props).forEach((key) => { + const fullKey = prefix ? `${prefix}.${key}` : key; + const prop = props[key] as Manifest["configuration"]; + if (!prop) { + return; + } + + if (prop.type === "object" && prop.properties) { + processProperties(renderer, prop.properties, fullKey); + } else { + createInputRow(fullKey, prop, renderer.configDefaults); + } + }); +} + +export function parseConfigInputs( + renderer: ManifestRenderer, + configInputs: NodeListOf, + manifest: Manifest +): { [key: string]: unknown } { + const config: Record = {}; + const schema = manifest.configuration; + if (!schema) { + throw new Error("No schema found in manifest"); + } + const validate = ajv.compile(schema as AnySchemaObject); + + configInputs.forEach((input) => { + const key = input.getAttribute("data-config-key"); + if (!key) { + throw new Error("Input key is required"); + } + + const keys = key.split("."); + + let currentObj = config; + for (let i = 0; i < keys.length - 1; i++) { + const part = keys[i]; + if (!currentObj[part] || typeof currentObj[part] !== "object") { + currentObj[part] = {}; + } + currentObj = currentObj[part] as Record; + } + + let value: unknown; + const expectedType = input.getAttribute("data-type"); + + if (expectedType === "boolean") { + value = (input as HTMLInputElement).checked; + } else if (expectedType === "object" || expectedType === "array") { + try { + value = JSON.parse((input as HTMLTextAreaElement).value); + } catch (e) { + console.error(e); + throw new Error(`Invalid JSON input for ${expectedType} at key "${key}": ${input.value}`); + } + } else { + value = (input as HTMLInputElement).value; + } + + currentObj[keys[keys.length - 1]] = value; + }); + + if (validate(config)) { + return config; + } else { + throw new Error("Invalid configuration: " + JSON.stringify(validate.errors, null, 2)); + } +} diff --git a/static/scripts/rendering/navigation.ts b/static/scripts/rendering/navigation.ts new file mode 100644 index 0000000..102a1ad --- /dev/null +++ b/static/scripts/rendering/navigation.ts @@ -0,0 +1,28 @@ +import { createElement, manifestGuiBody } from "../../utils/element-helpers"; +import { ManifestRenderer } from "../render-manifest"; + +export function createBackButton(renderer: ManifestRenderer, step: string): HTMLButtonElement { + const backButton = createElement("button", { + id: "back-button", + class: "button", + textContent: "Back", + }) as HTMLButtonElement; + + backButton.style.display = "none"; + backButton.addEventListener("click", handleBackButtonClick.bind(null, renderer, step)); + return backButton; +} + +function handleBackButtonClick(renderer: ManifestRenderer, step: string): void { + if (step === "pluginSelector") { + renderer.renderOrgPicker(renderer.orgs); + } else if (step === "configEditor") { + renderer.renderPluginSelector(); + } + + const readmeContainer = document.querySelector(".readme-container"); + if (readmeContainer) { + readmeContainer.remove(); + manifestGuiBody?.classList.remove("plugin-editor"); + } +} diff --git a/static/scripts/rendering/org-select.ts b/static/scripts/rendering/org-select.ts new file mode 100644 index 0000000..3e8f12c --- /dev/null +++ b/static/scripts/rendering/org-select.ts @@ -0,0 +1,111 @@ +import { ManifestPreDecode } from "../../types/plugins"; +import { createElement } from "../../utils/element-helpers"; +import { STRINGS } from "../../utils/strings"; +import { toastNotification } from "../../utils/toaster"; +import { ManifestRenderer } from "../render-manifest"; +import { controlButtons } from "./control-buttons"; +import { renderPluginSelector } from "./plugin-select"; +import { closeAllSelect, updateGuiTitle } from "./utils"; + +export function renderOrgPicker(renderer: ManifestRenderer, orgs: string[], fetchPromise?: Promise>) { + renderer.currentStep = "orgPicker"; + controlButtons({ hide: true }); + renderer.backButton.style.display = "none"; + renderer.manifestGui?.classList.add("rendering"); + renderer.manifestGuiBody.innerHTML = null; + + const pickerRow = document.createElement("tr"); + const pickerCell = document.createElement("td"); + pickerCell.colSpan = 4; + pickerCell.className = STRINGS.TDV_CENTERED; + + const customSelect = createElement("div", { class: "custom-select" }); + + const selectSelected = createElement("div", { + class: "select-selected", + textContent: "Select an organization", + }); + + const selectItems = createElement("div", { + class: "select-items select-hide", + }); + + customSelect.appendChild(selectSelected); + customSelect.appendChild(selectItems); + + pickerCell.appendChild(customSelect); + pickerRow.appendChild(pickerCell); + + renderer.manifestGuiBody.appendChild(pickerRow); + renderer.manifestGui?.classList.add("rendered"); + + if (!orgs.length) { + const hasSession = renderer.auth.isActiveSession(); + if (hasSession) { + updateGuiTitle("No organizations found"); + } else { + updateGuiTitle("Please sign in to GitHub"); + } + return; + } + + updateGuiTitle("Select an Organization"); + + orgs.forEach((org) => { + const optionDiv = createElement("div", { class: "select-option" }); + const textSpan = createElement("span", { textContent: org }); + + optionDiv.appendChild(textSpan); + + optionDiv.addEventListener("click", () => { + handleOrgSelection(renderer, org, fetchPromise); + selectSelected.textContent = org; + localStorage.setItem("selectedOrg", org); + }); + + selectItems.appendChild(optionDiv); + }); + + selectSelected.addEventListener("click", (e) => { + e.stopPropagation(); + closeAllSelect(); + selectItems.classList.toggle(STRINGS.SELECT_HIDE); + selectSelected.classList.toggle(STRINGS.SELECT_ARROW_ACTIVE); + }); + + document.addEventListener("click", closeAllSelect); +} + +function handleOrgSelection(renderer: ManifestRenderer, org: string, fetchPromise?: Promise>): void { + if (!org) { + throw new Error("No org selected"); + } + + localStorage.setItem("selectedOrg", org); + + if (fetchPromise) { + fetchPromise + .then((manifestCache) => { + localStorage.setItem("manifestCache", JSON.stringify(manifestCache)); + }) + .catch((error) => { + console.error("Error fetching manifest cache:", error); + toastNotification(`An error occurred while fetching the manifest cache: ${String(error)}`, { + type: "error", + shouldAutoDismiss: true, + }); + }); + + const fetchOrgConfig = async () => { + const octokit = renderer.auth.octokit; + if (!octokit) { + throw new Error("No org or octokit found"); + } + await renderer.configParser.fetchUserInstalledConfig(org, octokit); + renderPluginSelector(renderer); + }; + fetchOrgConfig().catch(console.error); + } else { + renderPluginSelector(renderer); + } +} diff --git a/static/scripts/rendering/plugin-select.ts b/static/scripts/rendering/plugin-select.ts new file mode 100644 index 0000000..80131b7 --- /dev/null +++ b/static/scripts/rendering/plugin-select.ts @@ -0,0 +1,92 @@ +import { ManifestCache, ManifestPreDecode, Plugin } from "../../types/plugins"; +import { createElement } from "../../utils/element-helpers"; +import { STRINGS } from "../../utils/strings"; +import { ManifestRenderer } from "../render-manifest"; +import { controlButtons } from "./control-buttons"; +import { closeAllSelect, updateGuiTitle } from "./utils"; + +export function renderPluginSelector(renderer: ManifestRenderer): void { + renderer.currentStep = "pluginSelector"; + renderer.backButton.style.display = "block"; + renderer.manifestGuiBody.innerHTML = null; + controlButtons({ hide: true }); + + const manifestCache = JSON.parse(localStorage.getItem("manifestCache") || "{}") as ManifestCache; + const pluginUrls = Object.keys(manifestCache); + + const pickerRow = document.createElement("tr"); + const pickerCell = document.createElement("td"); + pickerCell.colSpan = 2; + pickerCell.className = STRINGS.TDV_CENTERED; + + const userConfig = renderer.configParser.repoConfig; + let installedPlugins: Plugin[] = []; + + if (userConfig) { + installedPlugins = renderer.configParser.parseConfig(userConfig).plugins; + } + + const cleanManifestCache = Object.keys(manifestCache).reduce((acc, key) => { + if (manifestCache[key]?.name) { + acc[key] = manifestCache[key]; + } + return acc; + }, {} as ManifestCache); + + const customSelect = createElement("div", { class: "custom-select" }); + + const selectSelected = createElement("div", { + class: "select-selected", + textContent: "Select a plugin", + }); + + const selectItems = createElement("div", { + class: "select-items select-hide", + }); + + customSelect.appendChild(selectSelected); + customSelect.appendChild(selectItems); + + pickerCell.appendChild(customSelect); + pickerRow.appendChild(pickerCell); + + renderer.manifestGuiBody.appendChild(pickerRow); + + pluginUrls.forEach((url) => { + if (!cleanManifestCache[url]?.name) { + return; + } + + const [, repo] = url.replace("https://raw.githubusercontent.com/", "").split("/"); + const reg = new RegExp(`${repo}`, "gi"); + const installedPlugin: Plugin | undefined = installedPlugins.find((plugin) => plugin.uses[0].plugin.match(reg)); + const defaultForInstalled: ManifestPreDecode | null = cleanManifestCache[url]; + const optionText = defaultForInstalled.name; + const indicator = installedPlugin ? "🟢" : "🔴"; + + const optionDiv = createElement("div", { class: "select-option" }); + const textSpan = createElement("span", { textContent: optionText }); + const indicatorSpan = createElement("span", { textContent: indicator }); + + optionDiv.appendChild(textSpan); + optionDiv.appendChild(indicatorSpan); + + optionDiv.addEventListener("click", () => { + selectSelected.textContent = optionText; + closeAllSelect(); + localStorage.setItem("selectedPluginManifest", JSON.stringify(defaultForInstalled)); + renderer.renderConfigEditor(defaultForInstalled, installedPlugin?.uses[0].with); + }); + + selectItems.appendChild(optionDiv); + }); + + selectSelected.addEventListener("click", (e) => { + e.stopPropagation(); + closeAllSelect(); + selectItems.classList.toggle(STRINGS.SELECT_HIDE); + selectSelected.classList.toggle(STRINGS.SELECT_ARROW_ACTIVE); + }); + + updateGuiTitle(`Select a Plugin`); +} diff --git a/static/scripts/rendering/utils.ts b/static/scripts/rendering/utils.ts new file mode 100644 index 0000000..f93ec78 --- /dev/null +++ b/static/scripts/rendering/utils.ts @@ -0,0 +1,20 @@ +import { STRINGS } from "../../utils/strings"; + +export function updateGuiTitle(title: string): void { + const guiTitle = document.querySelector("#manifest-gui-title"); + if (!guiTitle) { + throw new Error("GUI Title not found"); + } + guiTitle.textContent = title; +} + +export function closeAllSelect() { + const selectItemsList = document.querySelectorAll(STRINGS.SELECT_ITEMS); + const selectSelectedList = document.querySelectorAll(STRINGS.SELECT_SELECTED); + selectItemsList.forEach((item) => { + item.classList.add(STRINGS.SELECT_HIDE); + }); + selectSelectedList.forEach((item) => { + item.classList.remove(STRINGS.SELECT_ARROW_ACTIVE); + }); +} diff --git a/static/scripts/rendering/write-add-remove.ts b/static/scripts/rendering/write-add-remove.ts new file mode 100644 index 0000000..5b9ec8f --- /dev/null +++ b/static/scripts/rendering/write-add-remove.ts @@ -0,0 +1,131 @@ +import { toastNotification } from "../../utils/toaster"; +import { ManifestRenderer } from "../render-manifest"; +import { Manifest, Plugin } from "../../types/plugins"; +import { parseConfigInputs } from "./input-parsing"; + +export function writeNewConfig(renderer: ManifestRenderer, option: "add" | "remove"): void { + const selectedManifest = localStorage.getItem("selectedPluginManifest"); + if (!selectedManifest) { + toastNotification("No selected plugin manifest found.", { + type: "error", + shouldAutoDismiss: true, + }); + throw new Error("No selected plugin manifest found"); + } + const pluginManifest = JSON.parse(selectedManifest) as Manifest; + const configInputs = document.querySelectorAll(".config-input"); + + const newConfig = parseConfigInputs(renderer, configInputs, pluginManifest); + + renderer.configParser.loadConfig(); + + const officialPluginConfig: Record = JSON.parse(localStorage.getItem("officialPluginConfig") || "{}"); + + const pluginName = pluginManifest.name; + + // this relies on the manifest matching the repo name + const normalizedPluginName = pluginName + .toLowerCase() + .replace(/ /g, "-") + .replace(/[^a-z0-9-]/g, "") + .replace(/-+/g, "-"); + + const pluginUrl = Object.keys(officialPluginConfig).find((url) => { + return url.includes(normalizedPluginName); + }); + + if (!pluginUrl) { + toastNotification(`No plugin URL found for ${pluginName}.`, { + type: "error", + shouldAutoDismiss: true, + }); + throw new Error("No plugin URL found"); + } + + const plugin: Plugin = { + uses: [ + { + plugin: pluginUrl, + with: newConfig, + }, + ], + }; + + if (option === "add") { + handleAddPlugin(renderer, plugin, pluginManifest); + } else { + handleRemovePlugin(renderer, plugin, pluginManifest); + } +} + +function handleAddPlugin(renderer: ManifestRenderer, plugin: Plugin, pluginManifest: Manifest): void { + renderer.configParser.addPlugin(plugin); + toastNotification(`Configuration for ${pluginManifest.name} saved successfully.Do you want to push to GitHub ? `, { + type: "success", + actionText: "Push to GitHub", + action: async () => { + const octokit = renderer.auth.octokit; + if (!octokit) { + throw new Error("Octokit not found"); + } + + const org = localStorage.getItem("selectedOrg"); + + if (!org) { + throw new Error("No selected org found"); + } + + try { + await renderer.configParser.updateConfig(org, octokit, "add"); + } catch (error) { + console.error("Error pushing config to GitHub:", error); + toastNotification("An error occurred while pushing the configuration to GitHub.", { + type: "error", + shouldAutoDismiss: true, + }); + return; + } + + toastNotification("Configuration pushed to GitHub successfully.", { + type: "success", + shouldAutoDismiss: true, + }); + }, + }); +} + +function handleRemovePlugin(renderer: ManifestRenderer, plugin: Plugin, pluginManifest: Manifest): void { + renderer.configParser.removePlugin(plugin); + toastNotification(`Configuration for ${pluginManifest.name} removed successfully.Do you want to push to GitHub ? `, { + type: "success", + actionText: "Push to GitHub", + action: async () => { + const octokit = renderer.auth.octokit; + if (!octokit) { + throw new Error("Octokit not found"); + } + + const org = localStorage.getItem("selectedOrg"); + + if (!org) { + throw new Error("No selected org found"); + } + + try { + await renderer.configParser.updateConfig(org, octokit, "remove"); + } catch (error) { + console.error("Error pushing config to GitHub:", error); + toastNotification("An error occurred while pushing the configuration to GitHub.", { + type: "error", + shouldAutoDismiss: true, + }); + return; + } + + toastNotification("Configuration pushed to GitHub successfully.", { + type: "success", + shouldAutoDismiss: true, + }); + }, + }); +} From 2a2a83faf270aad3ed1bedd957731b4b2bc26db1 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Thu, 21 Nov 2024 21:40:11 +0000 Subject: [PATCH 10/33] chore: minor fixes --- static/main.ts | 5 +++-- static/scripts/rendering/config-editor.ts | 3 +-- static/scripts/rendering/input-parsing.ts | 1 - static/scripts/rendering/navigation.ts | 6 ++++-- static/scripts/rendering/org-select.ts | 5 ++++- static/scripts/rendering/plugin-select.ts | 3 ++- static/scripts/rendering/write-add-remove.ts | 2 +- 7 files changed, 15 insertions(+), 10 deletions(-) diff --git a/static/main.ts b/static/main.ts index 40e6e42..8f6da77 100644 --- a/static/main.ts +++ b/static/main.ts @@ -1,6 +1,7 @@ import { AuthService } from "./scripts/authentication"; import { ManifestFetcher } from "./scripts/fetch-manifest"; import { ManifestRenderer } from "./scripts/render-manifest"; +import { renderOrgPicker } from "./scripts/rendering/org-select"; import { ManifestPreDecode } from "./types/plugins"; import { manifestGuiBody } from "./utils/element-helpers"; import { toastNotification } from "./utils/toaster"; @@ -45,9 +46,9 @@ export async function mainModule() { }); } - renderer.renderOrgPicker(userOrgs, fetchPromise); + renderOrgPicker(renderer, userOrgs, fetchPromise); } else { - renderer.renderOrgPicker([]); + renderOrgPicker(renderer, []); } } catch (error) { if (error instanceof Error) { diff --git a/static/scripts/rendering/config-editor.ts b/static/scripts/rendering/config-editor.ts index c85dd40..2e37722 100644 --- a/static/scripts/rendering/config-editor.ts +++ b/static/scripts/rendering/config-editor.ts @@ -1,5 +1,4 @@ -import { Manifest } from "@ubiquity-os/ubiquity-os-kernel"; -import { ManifestCache, Plugin } from "../../types/plugins"; +import { Manifest, ManifestCache, Plugin } from "../../types/plugins"; import { controlButtons } from "./control-buttons"; import { ManifestRenderer } from "../render-manifest"; import { processProperties } from "./input-parsing"; diff --git a/static/scripts/rendering/input-parsing.ts b/static/scripts/rendering/input-parsing.ts index c9f65a5..ca2b03b 100644 --- a/static/scripts/rendering/input-parsing.ts +++ b/static/scripts/rendering/input-parsing.ts @@ -21,7 +21,6 @@ export function processProperties(renderer: ManifestRenderer, props: Record, manifest: Manifest ): { [key: string]: unknown } { diff --git a/static/scripts/rendering/navigation.ts b/static/scripts/rendering/navigation.ts index 102a1ad..cff5d2d 100644 --- a/static/scripts/rendering/navigation.ts +++ b/static/scripts/rendering/navigation.ts @@ -1,5 +1,7 @@ import { createElement, manifestGuiBody } from "../../utils/element-helpers"; import { ManifestRenderer } from "../render-manifest"; +import { renderOrgPicker } from "./org-select"; +import { renderPluginSelector } from "./plugin-select"; export function createBackButton(renderer: ManifestRenderer, step: string): HTMLButtonElement { const backButton = createElement("button", { @@ -15,9 +17,9 @@ export function createBackButton(renderer: ManifestRenderer, step: string): HTML function handleBackButtonClick(renderer: ManifestRenderer, step: string): void { if (step === "pluginSelector") { - renderer.renderOrgPicker(renderer.orgs); + renderOrgPicker(renderer, renderer.orgs); } else if (step === "configEditor") { - renderer.renderPluginSelector(); + renderPluginSelector(renderer); } const readmeContainer = document.querySelector(".readme-container"); diff --git a/static/scripts/rendering/org-select.ts b/static/scripts/rendering/org-select.ts index 3e8f12c..a7d0654 100644 --- a/static/scripts/rendering/org-select.ts +++ b/static/scripts/rendering/org-select.ts @@ -14,12 +14,13 @@ export function renderOrgPicker(renderer: ManifestRenderer, orgs: string[], fetc renderer.manifestGui?.classList.add("rendering"); renderer.manifestGuiBody.innerHTML = null; + const pickerRow = document.createElement("tr"); const pickerCell = document.createElement("td"); pickerCell.colSpan = 4; pickerCell.className = STRINGS.TDV_CENTERED; - const customSelect = createElement("div", { class: "custom-select" }); + const customSelect = createElement("div", { class: "custom-select", style: `display: ${orgs.length ? "block" : "none"}` }); const selectSelected = createElement("div", { class: "select-selected", @@ -39,6 +40,8 @@ export function renderOrgPicker(renderer: ManifestRenderer, orgs: string[], fetc renderer.manifestGuiBody.appendChild(pickerRow); renderer.manifestGui?.classList.add("rendered"); + + if (!orgs.length) { const hasSession = renderer.auth.isActiveSession(); if (hasSession) { diff --git a/static/scripts/rendering/plugin-select.ts b/static/scripts/rendering/plugin-select.ts index 80131b7..5e20a5d 100644 --- a/static/scripts/rendering/plugin-select.ts +++ b/static/scripts/rendering/plugin-select.ts @@ -2,6 +2,7 @@ import { ManifestCache, ManifestPreDecode, Plugin } from "../../types/plugins"; import { createElement } from "../../utils/element-helpers"; import { STRINGS } from "../../utils/strings"; import { ManifestRenderer } from "../render-manifest"; +import { renderConfigEditor } from "./config-editor"; import { controlButtons } from "./control-buttons"; import { closeAllSelect, updateGuiTitle } from "./utils"; @@ -75,7 +76,7 @@ export function renderPluginSelector(renderer: ManifestRenderer): void { selectSelected.textContent = optionText; closeAllSelect(); localStorage.setItem("selectedPluginManifest", JSON.stringify(defaultForInstalled)); - renderer.renderConfigEditor(defaultForInstalled, installedPlugin?.uses[0].with); + renderConfigEditor(renderer, defaultForInstalled, installedPlugin?.uses[0].with); }); selectItems.appendChild(optionDiv); diff --git a/static/scripts/rendering/write-add-remove.ts b/static/scripts/rendering/write-add-remove.ts index 5b9ec8f..2788e7b 100644 --- a/static/scripts/rendering/write-add-remove.ts +++ b/static/scripts/rendering/write-add-remove.ts @@ -15,7 +15,7 @@ export function writeNewConfig(renderer: ManifestRenderer, option: "add" | "remo const pluginManifest = JSON.parse(selectedManifest) as Manifest; const configInputs = document.querySelectorAll(".config-input"); - const newConfig = parseConfigInputs(renderer, configInputs, pluginManifest); + const newConfig = parseConfigInputs(configInputs, pluginManifest); renderer.configParser.loadConfig(); From 359e0951648b74a139f550e294c4c1a662a197e5 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Thu, 21 Nov 2024 23:10:09 +0000 Subject: [PATCH 11/33] chore: markdown-it readme rendering, minor fixes --- package.json | 2 + static/manifest-gui.css | 113 +++++++++++----------- static/scripts/render-manifest.ts | 3 +- static/scripts/rendering/config-editor.ts | 6 +- static/scripts/rendering/input-parsing.ts | 5 +- static/scripts/rendering/navigation.ts | 24 +++-- static/scripts/rendering/org-select.ts | 23 +++-- static/utils/element-helpers.ts | 2 +- yarn.lock | 57 +++++++++++ 9 files changed, 147 insertions(+), 88 deletions(-) diff --git a/package.json b/package.json index 90f9c4e..c57f2d1 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@ubiquity-os/ubiquity-os-kernel": "^2.5.3", "ajv": "^8.17.1", "dotenv": "^16.4.4", + "markdown-it": "^14.1.0", "yaml": "^2.6.0" }, "devDependencies": { @@ -51,6 +52,7 @@ "@jest/globals": "29.7.0", "@mswjs/data": "0.16.1", "@types/jest": "^29.5.12", + "@types/markdown-it": "^14.1.2", "@types/node": "20.14.5", "cspell": "8.14.4", "cypress": "13.6.6", diff --git a/static/manifest-gui.css b/static/manifest-gui.css index e472432..37bc125 100644 --- a/static/manifest-gui.css +++ b/static/manifest-gui.css @@ -66,7 +66,7 @@ header #uos-logo path { #viewport-cell { display: flex; - flex-direction: column; + flex-direction: row; align-items: center; width: 100%; } @@ -77,15 +77,47 @@ header #uos-logo path { user-select: text; background-image: linear-gradient(0deg, #101010, #202020); border-radius: 4px; - margin: 16px auto; + margin: 0 auto; box-shadow: 0 24px 48px #000000; overflow: auto; - padding: 16px; + padding: 16px 24px; color: #fff; font-family: "Proxima Nova", sans-serif; line-height: 1.6; opacity: 0.85; - overflow-y: auto; + max-height: calc(100vh - 192px); + scrollbar-width: thin; + scrollbar-color: #303030 #101010; +} + +.readme-container p { + color: #ccc; + margin-bottom: 16px; +} + +.readme-container a { + color: #1e90ff; + text-decoration: none; +} + +.readme-container a:hover { + text-decoration: underline; +} + +.readme-container code { + background-color: #202020; + padding: 2px 4px; + border-radius: 4px; + color: #1e90ff; +} + +.readme-container pre { + background-color: #202020; + padding: 16px; + border-radius: 4px; + overflow: auto; + color: #ccc; + margin-bottom: 16px; } #manifest-gui { @@ -140,52 +172,36 @@ header #uos-logo path { margin: 0 0 16px; } -.readme-container h1, -.readme-container h2, -.readme-container h3 { +.config-input { + width: calc(100% - 32px); + padding: 8px 16px; + background-color: #101010; color: #fff; - font-weight: bold; - margin-top: 24px; -} - -.readme-container p { - color: #ccc; - margin-bottom: 16px; -} - -.readme-container a { - color: #1e90ff; - text-decoration: none; -} - -.readme-container a:hover { - text-decoration: underline; + border-radius: 4px; + font-size: 16px; + font-family: "Proxima Nova", sans-serif; + border-bottom: 1px solid #303030; } -.readme-container code { - background-color: #202020; - padding: 2px 4px; - border-radius: 4px; - color: #1e90ff; +.config-input:hover { + border-color: #606060; } -.readme-container pre { - background-color: #202020; - padding: 16px; - border-radius: 4px; - overflow: auto; - color: #ccc; - margin-bottom: 16px; +.config-input:focus { + outline: none; + border-color: #808080; } .table-data-header { - text-align: right; color: grey; text-transform: capitalize; text-rendering: geometricPrecision; + display: flex; + justify-content: right; + border-top: 1px solid #303030; } -.table-data-header > div { +.table-data-header { padding: 8px 16px; } @@ -193,7 +209,8 @@ header #uos-logo path { user-select: text; } -.table-data-value > div { +.table-data-value > input, +textarea { padding: 8px 16px; } @@ -369,26 +386,6 @@ select option { display: none; } -.config-input { - width: calc(100% - 32px); - padding: 8px 16px; - background-color: #101010; - color: #fff; - border: 1px solid #303030; - border-radius: 4px; - font-size: 16px; - font-family: "Proxima Nova", sans-serif; -} - -.config-input:hover { - border-color: #606060; -} - -.config-input:focus { - outline: none; - border-color: #808080; -} - .picker-container, .editor-container { background-image: linear-gradient(0deg, #101010, #202020); diff --git a/static/scripts/render-manifest.ts b/static/scripts/render-manifest.ts index 50dc897..4ed20c4 100644 --- a/static/scripts/render-manifest.ts +++ b/static/scripts/render-manifest.ts @@ -27,8 +27,9 @@ export class ManifestRenderer { this._manifestGuiBody = manifestGuiBody as HTMLElement; controlButtons({ hide: true }); + this.currentStep = "orgPicker"; const title = manifestGui.querySelector("#manifest-gui-title"); - this._backButton = createBackButton(this, this._currentStep); + this._backButton = createBackButton(this); title?.previousSibling?.appendChild(this._backButton); } diff --git a/static/scripts/rendering/config-editor.ts b/static/scripts/rendering/config-editor.ts index 2e37722..9b85b49 100644 --- a/static/scripts/rendering/config-editor.ts +++ b/static/scripts/rendering/config-editor.ts @@ -4,6 +4,8 @@ import { ManifestRenderer } from "../render-manifest"; import { processProperties } from "./input-parsing"; import { updateGuiTitle } from "./utils"; import { writeNewConfig } from "./write-add-remove"; +import MarkdownIt from "markdown-it"; +const md = new MarkdownIt(); export function renderConfigEditor(renderer: ManifestRenderer, pluginManifest: Manifest | null, plugin?: Plugin["uses"][0]["with"]): void { renderer.currentStep = "configEditor"; @@ -86,8 +88,8 @@ export function renderConfigEditor(renderer: ManifestRenderer, pluginManifest: M } const readmeContainer = document.createElement("div"); readmeContainer.className = "readme-container"; - readmeContainer.innerHTML = readme; - viewportCell.insertAdjacentElement("afterend", readmeContainer); + readmeContainer.innerHTML = md.render(readme); + viewportCell.appendChild(readmeContainer); } updateGuiTitle(`Editing Configuration for ${pluginManifest?.name}`); diff --git a/static/scripts/rendering/input-parsing.ts b/static/scripts/rendering/input-parsing.ts index ca2b03b..fd56ba0 100644 --- a/static/scripts/rendering/input-parsing.ts +++ b/static/scripts/rendering/input-parsing.ts @@ -20,10 +20,7 @@ export function processProperties(renderer: ManifestRenderer, props: Record, - manifest: Manifest -): { [key: string]: unknown } { +export function parseConfigInputs(configInputs: NodeListOf, manifest: Manifest): { [key: string]: unknown } { const config: Record = {}; const schema = manifest.configuration; if (!schema) { diff --git a/static/scripts/rendering/navigation.ts b/static/scripts/rendering/navigation.ts index cff5d2d..3fd8b77 100644 --- a/static/scripts/rendering/navigation.ts +++ b/static/scripts/rendering/navigation.ts @@ -1,9 +1,9 @@ -import { createElement, manifestGuiBody } from "../../utils/element-helpers"; +import { createElement } from "../../utils/element-helpers"; import { ManifestRenderer } from "../render-manifest"; import { renderOrgPicker } from "./org-select"; import { renderPluginSelector } from "./plugin-select"; -export function createBackButton(renderer: ManifestRenderer, step: string): HTMLButtonElement { +export function createBackButton(renderer: ManifestRenderer): HTMLButtonElement { const backButton = createElement("button", { id: "back-button", class: "button", @@ -11,20 +11,24 @@ export function createBackButton(renderer: ManifestRenderer, step: string): HTML }) as HTMLButtonElement; backButton.style.display = "none"; - backButton.addEventListener("click", handleBackButtonClick.bind(null, renderer, step)); + backButton.addEventListener("click", () => handleBackButtonClick(renderer)); return backButton; } -function handleBackButtonClick(renderer: ManifestRenderer, step: string): void { +function handleBackButtonClick(renderer: ManifestRenderer): void { + const readmeContainer = document.querySelector(".readme-container"); + if (readmeContainer) { + readmeContainer.remove(); + renderer.manifestGuiBody?.classList.remove("plugin-editor"); + } + + const step = renderer.currentStep; + if (step === "pluginSelector") { renderOrgPicker(renderer, renderer.orgs); } else if (step === "configEditor") { renderPluginSelector(renderer); - } - - const readmeContainer = document.querySelector(".readme-container"); - if (readmeContainer) { - readmeContainer.remove(); - manifestGuiBody?.classList.remove("plugin-editor"); + } else if (step === "orgPicker") { + renderOrgPicker(renderer, renderer.orgs); } } diff --git a/static/scripts/rendering/org-select.ts b/static/scripts/rendering/org-select.ts index a7d0654..8d374b2 100644 --- a/static/scripts/rendering/org-select.ts +++ b/static/scripts/rendering/org-select.ts @@ -13,7 +13,7 @@ export function renderOrgPicker(renderer: ManifestRenderer, orgs: string[], fetc renderer.backButton.style.display = "none"; renderer.manifestGui?.classList.add("rendering"); renderer.manifestGuiBody.innerHTML = null; - + renderer.orgs = orgs; const pickerRow = document.createElement("tr"); const pickerCell = document.createElement("td"); @@ -40,8 +40,6 @@ export function renderOrgPicker(renderer: ManifestRenderer, orgs: string[], fetc renderer.manifestGuiBody.appendChild(pickerRow); renderer.manifestGui?.classList.add("rendered"); - - if (!orgs.length) { const hasSession = renderer.auth.isActiveSession(); if (hasSession) { @@ -99,16 +97,17 @@ function handleOrgSelection(renderer: ManifestRenderer, org: string, fetchPromis }); }); - const fetchOrgConfig = async () => { - const octokit = renderer.auth.octokit; - if (!octokit) { - throw new Error("No org or octokit found"); - } - await renderer.configParser.fetchUserInstalledConfig(org, octokit); - renderPluginSelector(renderer); - }; - fetchOrgConfig().catch(console.error); + fetchOrgConfig(renderer, org).catch(console.error); } else { renderPluginSelector(renderer); } } + +async function fetchOrgConfig(renderer: ManifestRenderer, org: string): Promise { + const octokit = renderer.auth.octokit; + if (!octokit) { + throw new Error("No org or octokit found"); + } + await renderer.configParser.fetchUserInstalledConfig(org, octokit); + renderPluginSelector(renderer); +} diff --git a/static/utils/element-helpers.ts b/static/utils/element-helpers.ts index 9b4011a..73d3afd 100644 --- a/static/utils/element-helpers.ts +++ b/static/utils/element-helpers.ts @@ -29,7 +29,7 @@ export function createInputRow( const headerCell = document.createElement("td"); headerCell.className = "table-data-header"; - headerCell.textContent = key; + headerCell.textContent = key.replace(/([A-Z])/g, " $1"); row.appendChild(headerCell); const valueCell = document.createElement("td"); diff --git a/yarn.lock b/yarn.lock index 746248a..34babbc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2085,16 +2085,34 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== +"@types/linkify-it@^5": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-5.0.0.tgz#21413001973106cda1c3a9b91eedd4ccd5469d76" + integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q== + "@types/lodash@^4.14.172": version "4.14.202" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.202.tgz#f09dbd2fb082d507178b2f2a5c7e74bd72ff98f8" integrity sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ== +"@types/markdown-it@^14.1.2": + version "14.1.2" + resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-14.1.2.tgz#57f2532a0800067d9b934f3521429a2e8bfb4c61" + integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog== + dependencies: + "@types/linkify-it" "^5" + "@types/mdurl" "^2" + "@types/md5@^2.3.0": version "2.3.5" resolved "https://registry.yarnpkg.com/@types/md5/-/md5-2.3.5.tgz#481cef0a896e3a5dcbfc5a8a8b02c05958af48a5" integrity sha512-/i42wjYNgE6wf0j2bcTX6kuowmdL/6PE4IVitMpm2eYKBUuYCprdcWVK+xEF0gcV6ufMCRhtxmReGfc6hIK7Jw== +"@types/mdurl@^2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd" + integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg== + "@types/mute-stream@^0.0.4": version "0.0.4" resolved "https://registry.yarnpkg.com/@types/mute-stream/-/mute-stream-0.0.4.tgz#77208e56a08767af6c5e1237be8888e2f255c478" @@ -3474,6 +3492,11 @@ enquirer@^2.3.6: ansi-colors "^4.1.1" strip-ansi "^6.0.1" +entities@^4.4.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + env-paths@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" @@ -5277,6 +5300,13 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== +linkify-it@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421" + integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ== + dependencies: + uc.micro "^2.0.0" + lint-staged@15.2.7: version "15.2.7" resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.2.7.tgz#97867e29ed632820c0fb90be06cd9ed384025649" @@ -5482,6 +5512,18 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" +markdown-it@^14.1.0: + version "14.1.0" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.1.0.tgz#3c3c5992883c633db4714ccb4d7b5935d98b7d45" + integrity sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg== + dependencies: + argparse "^2.0.1" + entities "^4.4.0" + linkify-it "^5.0.0" + mdurl "^2.0.0" + punycode.js "^2.3.1" + uc.micro "^2.1.0" + md5@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f" @@ -5491,6 +5533,11 @@ md5@^2.3.0: crypt "0.0.2" is-buffer "~1.1.6" +mdurl@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0" + integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w== + memorystream@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" @@ -6049,6 +6096,11 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" +punycode.js@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7" + integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA== + punycode@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" @@ -6920,6 +6972,11 @@ typescript@5.6.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.2.tgz#d1de67b6bef77c41823f822df8f0b3bcff60a5a0" integrity sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw== +uc.micro@^2.0.0, uc.micro@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee" + integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A== + unbox-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" From d8b6470e98840c6bc3dd5d737b01a090cc4de422 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Thu, 21 Nov 2024 23:37:46 +0000 Subject: [PATCH 12/33] chore: feedback and await early --- static/main.ts | 40 ++++++++------------------ static/scripts/rendering/navigation.ts | 4 +-- static/scripts/rendering/org-select.ts | 30 ++++--------------- 3 files changed, 19 insertions(+), 55 deletions(-) diff --git a/static/main.ts b/static/main.ts index 8f6da77..216aae3 100644 --- a/static/main.ts +++ b/static/main.ts @@ -2,8 +2,6 @@ import { AuthService } from "./scripts/authentication"; import { ManifestFetcher } from "./scripts/fetch-manifest"; import { ManifestRenderer } from "./scripts/render-manifest"; import { renderOrgPicker } from "./scripts/rendering/org-select"; -import { ManifestPreDecode } from "./types/plugins"; -import { manifestGuiBody } from "./utils/element-helpers"; import { toastNotification } from "./utils/toaster"; async function handleAuth() { @@ -15,38 +13,30 @@ async function handleAuth() { export async function mainModule() { const auth = await handleAuth(); const renderer = new ManifestRenderer(auth); + renderer.manifestGuiBody.dataset.loading = "false"; try { const ubiquityOrgsToFetchOfficialConfigFrom = ["ubiquity-os"]; const fetcher = new ManifestFetcher(ubiquityOrgsToFetchOfficialConfigFrom, auth.octokit); const cache = fetcher.checkManifestCache(); - if (!manifestGuiBody) { - throw new Error("Manifest GUI body not found"); - } - manifestGuiBody.dataset.loading = "false"; if (auth.isActiveSession()) { const userOrgs = await auth.getGitHubUserOrgs(); - let fetchPromise: Promise> = Promise.resolve(cache); + if (Object.keys(cache).length === 0) { + renderer.manifestGuiBody.dataset.loading = "true"; const killNotification = toastNotification("Fetching manifest data...", { type: "info", shouldAutoDismiss: true }); - manifestGuiBody.dataset.loading = "true"; + renderOrgPicker(renderer, []); + + const manifestCache = await fetcher.fetchMarketplaceManifests(); + localStorage.setItem("manifestCache", JSON.stringify(manifestCache)); - // eslint-disable-next-line no-async-promise-executor - fetchPromise = new Promise(async (resolve) => { - if (!manifestGuiBody) { - throw new Error("Manifest GUI body not found"); - } - const manifestCache = await fetcher.fetchMarketplaceManifests(); - localStorage.setItem("manifestCache", JSON.stringify(manifestCache)); - await fetcher.fetchOfficialPluginConfig(); - manifestGuiBody.dataset.loading = "false"; - resolve(manifestCache); - killNotification(); - }); + await fetcher.fetchOfficialPluginConfig(); + killNotification(); + renderer.manifestGuiBody.dataset.loading = "false"; } - renderOrgPicker(renderer, userOrgs, fetchPromise); + renderOrgPicker(renderer, userOrgs); } else { renderOrgPicker(renderer, []); } @@ -59,10 +49,4 @@ export async function mainModule() { } } -mainModule() - .then(() => { - console.log("mainModule loaded"); - }) - .catch((error) => { - console.error(error); - }); +mainModule().catch(console.error); \ No newline at end of file diff --git a/static/scripts/rendering/navigation.ts b/static/scripts/rendering/navigation.ts index 3fd8b77..de9e51d 100644 --- a/static/scripts/rendering/navigation.ts +++ b/static/scripts/rendering/navigation.ts @@ -24,11 +24,9 @@ function handleBackButtonClick(renderer: ManifestRenderer): void { const step = renderer.currentStep; - if (step === "pluginSelector") { + if (step === "pluginSelector" || step === "orgPicker") { renderOrgPicker(renderer, renderer.orgs); } else if (step === "configEditor") { renderPluginSelector(renderer); - } else if (step === "orgPicker") { - renderOrgPicker(renderer, renderer.orgs); } } diff --git a/static/scripts/rendering/org-select.ts b/static/scripts/rendering/org-select.ts index 8d374b2..30f5bb0 100644 --- a/static/scripts/rendering/org-select.ts +++ b/static/scripts/rendering/org-select.ts @@ -1,4 +1,3 @@ -import { ManifestPreDecode } from "../../types/plugins"; import { createElement } from "../../utils/element-helpers"; import { STRINGS } from "../../utils/strings"; import { toastNotification } from "../../utils/toaster"; @@ -7,7 +6,7 @@ import { controlButtons } from "./control-buttons"; import { renderPluginSelector } from "./plugin-select"; import { closeAllSelect, updateGuiTitle } from "./utils"; -export function renderOrgPicker(renderer: ManifestRenderer, orgs: string[], fetchPromise?: Promise>) { +export function renderOrgPicker(renderer: ManifestRenderer, orgs: string[]) { renderer.currentStep = "orgPicker"; controlButtons({ hide: true }); renderer.backButton.style.display = "none"; @@ -59,7 +58,7 @@ export function renderOrgPicker(renderer: ManifestRenderer, orgs: string[], fetc optionDiv.appendChild(textSpan); optionDiv.addEventListener("click", () => { - handleOrgSelection(renderer, org, fetchPromise); + handleOrgSelection(renderer, org); selectSelected.textContent = org; localStorage.setItem("selectedOrg", org); }); @@ -69,7 +68,6 @@ export function renderOrgPicker(renderer: ManifestRenderer, orgs: string[], fetc selectSelected.addEventListener("click", (e) => { e.stopPropagation(); - closeAllSelect(); selectItems.classList.toggle(STRINGS.SELECT_HIDE); selectSelected.classList.toggle(STRINGS.SELECT_ARROW_ACTIVE); }); @@ -77,37 +75,21 @@ export function renderOrgPicker(renderer: ManifestRenderer, orgs: string[], fetc document.addEventListener("click", closeAllSelect); } -function handleOrgSelection(renderer: ManifestRenderer, org: string, fetchPromise?: Promise>): void { +function handleOrgSelection(renderer: ManifestRenderer, org: string): void { if (!org) { throw new Error("No org selected"); } - localStorage.setItem("selectedOrg", org); - - if (fetchPromise) { - fetchPromise - .then((manifestCache) => { - localStorage.setItem("manifestCache", JSON.stringify(manifestCache)); - }) - .catch((error) => { - console.error("Error fetching manifest cache:", error); - toastNotification(`An error occurred while fetching the manifest cache: ${String(error)}`, { - type: "error", - shouldAutoDismiss: true, - }); - }); - - fetchOrgConfig(renderer, org).catch(console.error); - } else { - renderPluginSelector(renderer); - } + fetchOrgConfig(renderer, org).catch(console.error); } async function fetchOrgConfig(renderer: ManifestRenderer, org: string): Promise { + const kill = toastNotification("Fetching organization config...", { type: "info", shouldAutoDismiss: true }); const octokit = renderer.auth.octokit; if (!octokit) { throw new Error("No org or octokit found"); } await renderer.configParser.fetchUserInstalledConfig(org, octokit); renderPluginSelector(renderer); + kill(); } From 67dad9366963f82aa49a9a78f82261a84e028b76 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Fri, 22 Nov 2024 14:57:45 +0000 Subject: [PATCH 13/33] chore: responsive padding, resize after nav --- static/manifest-gui.css | 2 ++ static/scripts/rendering/navigation.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/static/manifest-gui.css b/static/manifest-gui.css index 37bc125..9f60420 100644 --- a/static/manifest-gui.css +++ b/static/manifest-gui.css @@ -69,6 +69,8 @@ header #uos-logo path { flex-direction: row; align-items: center; width: 100%; + padding: clamp(16px, 2vw, 32px); + gap: 24px; } .readme-container { diff --git a/static/scripts/rendering/navigation.ts b/static/scripts/rendering/navigation.ts index de9e51d..d57b96a 100644 --- a/static/scripts/rendering/navigation.ts +++ b/static/scripts/rendering/navigation.ts @@ -16,10 +16,10 @@ export function createBackButton(renderer: ManifestRenderer): HTMLButtonElement } function handleBackButtonClick(renderer: ManifestRenderer): void { + renderer.manifestGui?.classList.remove("plugin-editor"); const readmeContainer = document.querySelector(".readme-container"); if (readmeContainer) { readmeContainer.remove(); - renderer.manifestGuiBody?.classList.remove("plugin-editor"); } const step = renderer.currentStep; From eadc53bf668d85662d764a6d3ebfe139209c7f80 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Fri, 22 Nov 2024 15:03:20 +0000 Subject: [PATCH 14/33] chore: disable remove if not installed --- static/manifest-gui.css | 8 ++++++++ static/scripts/rendering/config-editor.ts | 16 ++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/static/manifest-gui.css b/static/manifest-gui.css index 9f60420..1f88ee5 100644 --- a/static/manifest-gui.css +++ b/static/manifest-gui.css @@ -261,6 +261,14 @@ button#reset-to-default:active::before { content: "♻️♻️♻️♻️♻️"; } +button#remove.disabled { + background-color: #303030; + color: #808080; + cursor: not-allowed; + opacity: 0.5; + pointer-events: none; +} + button#remove::before { content: "−"; } diff --git a/static/scripts/rendering/config-editor.ts b/static/scripts/rendering/config-editor.ts index 9b85b49..1561070 100644 --- a/static/scripts/rendering/config-editor.ts +++ b/static/scripts/rendering/config-editor.ts @@ -47,15 +47,23 @@ export function renderConfigEditor(renderer: ManifestRenderer, pluginManifest: M }); } - const add = document.getElementById("add"); - const remove = document.getElementById("remove"); + const add = document.getElementById("add") as HTMLButtonElement; + const remove = document.getElementById("remove") as HTMLButtonElement; if (!add || !remove) { throw new Error("Add or remove button not found"); } add.addEventListener("click", writeNewConfig.bind(null, renderer, "add")); - remove.addEventListener("click", () => writeNewConfig.bind(null, renderer, "remove")); - const resetToDefaultButton = document.getElementById("reset-to-default"); + if (plugin) { + remove.disabled = false; + remove.classList.remove("disabled"); + remove.addEventListener("click", () => writeNewConfig.bind(null, renderer, "remove")); + } else { + remove.disabled = true; + remove.classList.add("disabled"); + } + + const resetToDefaultButton = document.getElementById("reset-to-default") as HTMLButtonElement; if (!resetToDefaultButton) { throw new Error("Reset to default button not found"); } From 87076db76aee08f2ceef0aafce79327cbb57730e Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Fri, 22 Nov 2024 15:06:52 +0000 Subject: [PATCH 15/33] chore: knip ignore --- .github/knip.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/knip.ts b/.github/knip.ts index b3f8e17..c5e4622 100644 --- a/.github/knip.ts +++ b/.github/knip.ts @@ -16,6 +16,8 @@ const config: KnipConfig = { "@actions/core", "esbuild", "@ubiquity-os/plugin-sdk", + "markdown-it", + "@types/markdown-it" ], eslint: true, }; From 2921cb9132873ae00229fec4d19b7a617e7230d2 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Fri, 22 Nov 2024 18:58:24 +0000 Subject: [PATCH 16/33] chore: loading title on fetch --- static/main.ts | 1 + static/scripts/rendering/org-select.ts | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/static/main.ts b/static/main.ts index 216aae3..2cb59d6 100644 --- a/static/main.ts +++ b/static/main.ts @@ -2,6 +2,7 @@ import { AuthService } from "./scripts/authentication"; import { ManifestFetcher } from "./scripts/fetch-manifest"; import { ManifestRenderer } from "./scripts/render-manifest"; import { renderOrgPicker } from "./scripts/rendering/org-select"; +import { updateGuiTitle } from "./scripts/rendering/utils"; import { toastNotification } from "./utils/toaster"; async function handleAuth() { diff --git a/static/scripts/rendering/org-select.ts b/static/scripts/rendering/org-select.ts index 30f5bb0..ee4f762 100644 --- a/static/scripts/rendering/org-select.ts +++ b/static/scripts/rendering/org-select.ts @@ -41,8 +41,12 @@ export function renderOrgPicker(renderer: ManifestRenderer, orgs: string[]) { if (!orgs.length) { const hasSession = renderer.auth.isActiveSession(); - if (hasSession) { + const isLoading = renderer.manifestGuiBody.dataset.loading === "true"; + + if (hasSession && !isLoading) { updateGuiTitle("No organizations found"); + } else if (hasSession && isLoading) { + updateGuiTitle("Fetching organization data..."); } else { updateGuiTitle("Please sign in to GitHub"); } From 1778a64a014baa8e9f7cfff7e56175d9a82881b3 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Fri, 22 Nov 2024 18:59:56 +0000 Subject: [PATCH 17/33] chore: inject org into editor title --- static/scripts/rendering/config-editor.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/static/scripts/rendering/config-editor.ts b/static/scripts/rendering/config-editor.ts index 1561070..604c256 100644 --- a/static/scripts/rendering/config-editor.ts +++ b/static/scripts/rendering/config-editor.ts @@ -100,7 +100,9 @@ export function renderConfigEditor(renderer: ManifestRenderer, pluginManifest: M viewportCell.appendChild(readmeContainer); } - updateGuiTitle(`Editing Configuration for ${pluginManifest?.name}`); + const org = localStorage.getItem("selectedOrg"); + + updateGuiTitle(`Editing Configuration for ${pluginManifest?.name} in ${org}`); renderer.manifestGui?.classList.add("plugin-editor"); renderer.manifestGui?.classList.add("rendered"); } From d3de5815b1aec21c126c5df28d41e549f2c27823 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Fri, 22 Nov 2024 19:22:28 +0000 Subject: [PATCH 18/33] chore: storage getters --- static/main.ts | 4 +--- static/scripts/fetch-manifest.ts | 3 ++- static/scripts/rendering/config-editor.ts | 3 ++- static/scripts/rendering/plugin-select.ts | 3 ++- static/scripts/rendering/write-add-remove.ts | 3 ++- static/utils/storage.ts | 9 +++++++++ 6 files changed, 18 insertions(+), 7 deletions(-) create mode 100644 static/utils/storage.ts diff --git a/static/main.ts b/static/main.ts index 2cb59d6..da91f04 100644 --- a/static/main.ts +++ b/static/main.ts @@ -29,9 +29,7 @@ export async function mainModule() { const killNotification = toastNotification("Fetching manifest data...", { type: "info", shouldAutoDismiss: true }); renderOrgPicker(renderer, []); - const manifestCache = await fetcher.fetchMarketplaceManifests(); - localStorage.setItem("manifestCache", JSON.stringify(manifestCache)); - + await fetcher.fetchMarketplaceManifests(); await fetcher.fetchOfficialPluginConfig(); killNotification(); renderer.manifestGuiBody.dataset.loading = "false"; diff --git a/static/scripts/fetch-manifest.ts b/static/scripts/fetch-manifest.ts index ca09647..3b70545 100644 --- a/static/scripts/fetch-manifest.ts +++ b/static/scripts/fetch-manifest.ts @@ -1,6 +1,7 @@ import { Octokit } from "@octokit/rest"; import { Manifest, ManifestPreDecode } from "../types/plugins"; import { DEV_CONFIG_FULL_PATH, CONFIG_FULL_PATH, CONFIG_ORG_REPO } from "@ubiquity-os/plugin-sdk/constants"; +import { getOfficialPluginConfig } from "../utils/storage"; export class ManifestFetcher { private _orgs: string[]; @@ -74,7 +75,7 @@ export class ManifestFetcher { async fetchOfficialPluginConfig() { await this.fetchOrgsUbiquityOsConfigs(); - const officialPluginConfig = JSON.parse(localStorage.getItem("officialPluginConfig") || "{}") || {}; + const officialPluginConfig = getOfficialPluginConfig() this.workerUrls.forEach((url) => { officialPluginConfig[url] = { workerUrl: url }; diff --git a/static/scripts/rendering/config-editor.ts b/static/scripts/rendering/config-editor.ts index 604c256..3f68fcb 100644 --- a/static/scripts/rendering/config-editor.ts +++ b/static/scripts/rendering/config-editor.ts @@ -5,6 +5,7 @@ import { processProperties } from "./input-parsing"; import { updateGuiTitle } from "./utils"; import { writeNewConfig } from "./write-add-remove"; import MarkdownIt from "markdown-it"; +import { getManifestCache } from "../../utils/storage"; const md = new MarkdownIt(); export function renderConfigEditor(renderer: ManifestRenderer, pluginManifest: Manifest | null, plugin?: Plugin["uses"][0]["with"]): void { @@ -78,7 +79,7 @@ export function renderConfigEditor(renderer: ManifestRenderer, pluginManifest: M resetToDefaultButton.hidden = !!plugin; - const manifestCache = JSON.parse(localStorage.getItem("manifestCache") || "{}") as ManifestCache; + const manifestCache = getManifestCache() const pluginUrls = Object.keys(manifestCache); const pluginUrl = pluginUrls.find((url) => { return manifestCache[url].name === pluginManifest?.name; diff --git a/static/scripts/rendering/plugin-select.ts b/static/scripts/rendering/plugin-select.ts index 5e20a5d..9805145 100644 --- a/static/scripts/rendering/plugin-select.ts +++ b/static/scripts/rendering/plugin-select.ts @@ -1,5 +1,6 @@ import { ManifestCache, ManifestPreDecode, Plugin } from "../../types/plugins"; import { createElement } from "../../utils/element-helpers"; +import { getManifestCache } from "../../utils/storage"; import { STRINGS } from "../../utils/strings"; import { ManifestRenderer } from "../render-manifest"; import { renderConfigEditor } from "./config-editor"; @@ -12,7 +13,7 @@ export function renderPluginSelector(renderer: ManifestRenderer): void { renderer.manifestGuiBody.innerHTML = null; controlButtons({ hide: true }); - const manifestCache = JSON.parse(localStorage.getItem("manifestCache") || "{}") as ManifestCache; + const manifestCache = getManifestCache() const pluginUrls = Object.keys(manifestCache); const pickerRow = document.createElement("tr"); diff --git a/static/scripts/rendering/write-add-remove.ts b/static/scripts/rendering/write-add-remove.ts index 2788e7b..9d743bd 100644 --- a/static/scripts/rendering/write-add-remove.ts +++ b/static/scripts/rendering/write-add-remove.ts @@ -2,6 +2,7 @@ import { toastNotification } from "../../utils/toaster"; import { ManifestRenderer } from "../render-manifest"; import { Manifest, Plugin } from "../../types/plugins"; import { parseConfigInputs } from "./input-parsing"; +import { getOfficialPluginConfig } from "../../utils/storage"; export function writeNewConfig(renderer: ManifestRenderer, option: "add" | "remove"): void { const selectedManifest = localStorage.getItem("selectedPluginManifest"); @@ -19,7 +20,7 @@ export function writeNewConfig(renderer: ManifestRenderer, option: "add" | "remo renderer.configParser.loadConfig(); - const officialPluginConfig: Record = JSON.parse(localStorage.getItem("officialPluginConfig") || "{}"); + const officialPluginConfig: Record = getOfficialPluginConfig(); const pluginName = pluginManifest.name; diff --git a/static/utils/storage.ts b/static/utils/storage.ts new file mode 100644 index 0000000..f4f113c --- /dev/null +++ b/static/utils/storage.ts @@ -0,0 +1,9 @@ +import { ManifestCache } from "../types/plugins"; + +export function getManifestCache(): ManifestCache { + return JSON.parse(localStorage.getItem("manifestCache") || "{}"); +} + +export function getOfficialPluginConfig() { + return JSON.parse(localStorage.getItem("officialPluginConfig") || "{}"); +} \ No newline at end of file From 7bd56acec90ae8d1a3bf12f608094dc778ed08f9 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Fri, 22 Nov 2024 19:46:19 +0000 Subject: [PATCH 19/33] chore: indicator after push without reload --- static/scripts/config-parser.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/static/scripts/config-parser.ts b/static/scripts/config-parser.ts index ff43695..53948b4 100644 --- a/static/scripts/config-parser.ts +++ b/static/scripts/config-parser.ts @@ -160,6 +160,8 @@ export class ConfigParser { throw new Error("No content to push"); } + this.repoConfig = this.newConfigYml; + return octokit.repos.createOrUpdateFileContents({ owner: org, repo: repo, From f0f480857e3530436c44ba7cb115e6b52c989aa7 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Fri, 22 Nov 2024 19:49:43 +0000 Subject: [PATCH 20/33] chore: format --- .github/knip.ts | 2 +- static/main.ts | 3 +-- static/scripts/fetch-manifest.ts | 2 +- static/scripts/rendering/config-editor.ts | 4 ++-- static/scripts/rendering/plugin-select.ts | 2 +- static/utils/storage.ts | 6 +++--- 6 files changed, 9 insertions(+), 10 deletions(-) diff --git a/.github/knip.ts b/.github/knip.ts index c5e4622..1db99c4 100644 --- a/.github/knip.ts +++ b/.github/knip.ts @@ -17,7 +17,7 @@ const config: KnipConfig = { "esbuild", "@ubiquity-os/plugin-sdk", "markdown-it", - "@types/markdown-it" + "@types/markdown-it", ], eslint: true, }; diff --git a/static/main.ts b/static/main.ts index da91f04..eb5cc83 100644 --- a/static/main.ts +++ b/static/main.ts @@ -2,7 +2,6 @@ import { AuthService } from "./scripts/authentication"; import { ManifestFetcher } from "./scripts/fetch-manifest"; import { ManifestRenderer } from "./scripts/render-manifest"; import { renderOrgPicker } from "./scripts/rendering/org-select"; -import { updateGuiTitle } from "./scripts/rendering/utils"; import { toastNotification } from "./utils/toaster"; async function handleAuth() { @@ -48,4 +47,4 @@ export async function mainModule() { } } -mainModule().catch(console.error); \ No newline at end of file +mainModule().catch(console.error); diff --git a/static/scripts/fetch-manifest.ts b/static/scripts/fetch-manifest.ts index 3b70545..e39bc08 100644 --- a/static/scripts/fetch-manifest.ts +++ b/static/scripts/fetch-manifest.ts @@ -75,7 +75,7 @@ export class ManifestFetcher { async fetchOfficialPluginConfig() { await this.fetchOrgsUbiquityOsConfigs(); - const officialPluginConfig = getOfficialPluginConfig() + const officialPluginConfig = getOfficialPluginConfig(); this.workerUrls.forEach((url) => { officialPluginConfig[url] = { workerUrl: url }; diff --git a/static/scripts/rendering/config-editor.ts b/static/scripts/rendering/config-editor.ts index 3f68fcb..9bfe10b 100644 --- a/static/scripts/rendering/config-editor.ts +++ b/static/scripts/rendering/config-editor.ts @@ -1,4 +1,4 @@ -import { Manifest, ManifestCache, Plugin } from "../../types/plugins"; +import { Manifest, Plugin } from "../../types/plugins"; import { controlButtons } from "./control-buttons"; import { ManifestRenderer } from "../render-manifest"; import { processProperties } from "./input-parsing"; @@ -79,7 +79,7 @@ export function renderConfigEditor(renderer: ManifestRenderer, pluginManifest: M resetToDefaultButton.hidden = !!plugin; - const manifestCache = getManifestCache() + const manifestCache = getManifestCache(); const pluginUrls = Object.keys(manifestCache); const pluginUrl = pluginUrls.find((url) => { return manifestCache[url].name === pluginManifest?.name; diff --git a/static/scripts/rendering/plugin-select.ts b/static/scripts/rendering/plugin-select.ts index 9805145..a67338d 100644 --- a/static/scripts/rendering/plugin-select.ts +++ b/static/scripts/rendering/plugin-select.ts @@ -13,7 +13,7 @@ export function renderPluginSelector(renderer: ManifestRenderer): void { renderer.manifestGuiBody.innerHTML = null; controlButtons({ hide: true }); - const manifestCache = getManifestCache() + const manifestCache = getManifestCache(); const pluginUrls = Object.keys(manifestCache); const pickerRow = document.createElement("tr"); diff --git a/static/utils/storage.ts b/static/utils/storage.ts index f4f113c..71d4cdc 100644 --- a/static/utils/storage.ts +++ b/static/utils/storage.ts @@ -1,9 +1,9 @@ import { ManifestCache } from "../types/plugins"; export function getManifestCache(): ManifestCache { - return JSON.parse(localStorage.getItem("manifestCache") || "{}"); + return JSON.parse(localStorage.getItem("manifestCache") || "{}"); } export function getOfficialPluginConfig() { - return JSON.parse(localStorage.getItem("officialPluginConfig") || "{}"); -} \ No newline at end of file + return JSON.parse(localStorage.getItem("officialPluginConfig") || "{}"); +} From 5c220311bd9fad2aa01e1722fe69bf766905981c Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Tue, 26 Nov 2024 08:04:46 +0000 Subject: [PATCH 21/33] chore: add/remove shift --- static/scripts/config-parser.ts | 40 +++++++++++++-------------------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/static/scripts/config-parser.ts b/static/scripts/config-parser.ts index 53948b4..147bede 100644 --- a/static/scripts/config-parser.ts +++ b/static/scripts/config-parser.ts @@ -109,7 +109,7 @@ export class ConfigParser { } async updateConfig(org: string, octokit: Octokit, option: "add" | "remove", path = CONFIG_FULL_PATH, repo = CONFIG_ORG_REPO) { - let repoPlugins = this.parseConfig(this.repoConfig).plugins; + const repoPlugins = this.parseConfig(this.repoConfig).plugins; const newPlugins = this.parseConfig().plugins; if (!newPlugins?.length && option === "add") { @@ -117,25 +117,8 @@ export class ConfigParser { } if (option === "add") { - // update if it exists, add if it doesn't - newPlugins.forEach((newPlugin) => { - const existingPlugin = repoPlugins.find((p) => p.uses[0].plugin === newPlugin.uses[0].plugin); - if (existingPlugin) { - existingPlugin.uses[0].with = newPlugin.uses[0].with; - } else { - repoPlugins.push(newPlugin); - } - }); - this.newConfigYml = YAML.stringify({ plugins: repoPlugins }); } else if (option === "remove") { - // remove only this plugin, keep all others - newPlugins.forEach((newPlugin) => { - const existingPlugin = repoPlugins.find((p) => p.uses[0].plugin === newPlugin.uses[0].plugin); - if (existingPlugin) { - repoPlugins = repoPlugins.filter((p) => p.uses[0].plugin !== newPlugin.uses[0].plugin); - } - }); this.newConfigYml = YAML.stringify({ plugins: newPlugins }); } @@ -174,24 +157,33 @@ export class ConfigParser { addPlugin(plugin: Plugin) { const config = this.loadConfig(); - const parsedConfig = YAML.parse(config); - if (!parsedConfig.plugins) { - parsedConfig.plugins = []; + const parsedConfig = this.parseConfig(config); + parsedConfig.plugins ??= []; + + const existingPlugin = parsedConfig.plugins.find((p) => p.uses[0].plugin === plugin.uses[0].plugin); + if (existingPlugin) { + existingPlugin.uses[0].with = plugin.uses[0].with; + } else { + parsedConfig.plugins.push(plugin); } + parsedConfig.plugins.push(plugin); - this.newConfigYml = YAML.stringify(parsedConfig); + const newConfig = YAML.stringify(parsedConfig); + this.newConfigYml = newConfig; this.saveConfig(); } removePlugin(plugin: Plugin) { const config = this.loadConfig(); - const parsedConfig = YAML.parse(config); + const parsedConfig = this.parseConfig(config); if (!parsedConfig.plugins) { console.log("No plugins to remove"); return; } + parsedConfig.plugins = parsedConfig.plugins.filter((p: Plugin) => p.uses[0].plugin !== plugin.uses[0].plugin); - this.newConfigYml = YAML.stringify(parsedConfig); + const newConfig = YAML.stringify(parsedConfig); + this.newConfigYml = newConfig; this.saveConfig(); } From cfef7becf8d5d072b8d75af391b22e49cd03eb04 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Tue, 26 Nov 2024 08:05:44 +0000 Subject: [PATCH 22/33] chore: editor listeners, active remove for installed --- static/scripts/rendering/config-editor.ts | 85 +++++++++++++++++------ 1 file changed, 63 insertions(+), 22 deletions(-) diff --git a/static/scripts/rendering/config-editor.ts b/static/scripts/rendering/config-editor.ts index 9bfe10b..7c75f73 100644 --- a/static/scripts/rendering/config-editor.ts +++ b/static/scripts/rendering/config-editor.ts @@ -2,8 +2,8 @@ import { Manifest, Plugin } from "../../types/plugins"; import { controlButtons } from "./control-buttons"; import { ManifestRenderer } from "../render-manifest"; import { processProperties } from "./input-parsing"; -import { updateGuiTitle } from "./utils"; -import { writeNewConfig } from "./write-add-remove"; +import { addTrackedEventListener, getTrackedEventListeners, normalizePluginName, removeTrackedEventListener, updateGuiTitle } from "./utils"; +import { handleResetToDefault, writeNewConfig } from "./write-add-remove"; import MarkdownIt from "markdown-it"; import { getManifestCache } from "../../utils/storage"; const md = new MarkdownIt(); @@ -13,7 +13,7 @@ export function renderConfigEditor(renderer: ManifestRenderer, pluginManifest: M renderer.backButton.style.display = "block"; renderer.manifestGuiBody.innerHTML = null; controlButtons({ hide: false }); - processProperties(renderer, pluginManifest?.configuration?.properties || {}); + processProperties(renderer, pluginManifest?.configuration.properties || {}); const configInputs = document.querySelectorAll(".config-input"); if (plugin) { @@ -50,35 +50,32 @@ export function renderConfigEditor(renderer: ManifestRenderer, pluginManifest: M const add = document.getElementById("add") as HTMLButtonElement; const remove = document.getElementById("remove") as HTMLButtonElement; - if (!add || !remove) { - throw new Error("Add or remove button not found"); + const resetToDefaultButton = document.getElementById("reset-to-default") as HTMLButtonElement; + if (!add || !remove || !resetToDefaultButton) { + throw new Error("Buttons not found"); } - add.addEventListener("click", writeNewConfig.bind(null, renderer, "add")); - if (plugin) { + const parsedConfig = renderer.configParser.parseConfig(renderer.configParser.repoConfig || localStorage.getItem("config")); + const isInstalled = parsedConfig.plugins?.find((p) => p.uses[0].plugin.includes(normalizePluginName(pluginManifest?.name || ""))); + + loadListeners({ + renderer, + pluginManifest, + withPluginOrInstalled: !!(plugin || isInstalled), + add, + remove, + resetToDefaultButton, + }).catch(console.error); + + if (plugin || isInstalled) { remove.disabled = false; remove.classList.remove("disabled"); - remove.addEventListener("click", () => writeNewConfig.bind(null, renderer, "remove")); } else { remove.disabled = true; remove.classList.add("disabled"); } - const resetToDefaultButton = document.getElementById("reset-to-default") as HTMLButtonElement; - if (!resetToDefaultButton) { - throw new Error("Reset to default button not found"); - } - - resetToDefaultButton.addEventListener("click", () => { - renderConfigEditor(renderer, pluginManifest); - const readmeContainer = document.querySelector(".readme-container"); - if (readmeContainer) { - readmeContainer.remove(); - } - }); - resetToDefaultButton.hidden = !!plugin; - const manifestCache = getManifestCache(); const pluginUrls = Object.keys(manifestCache); const pluginUrl = pluginUrls.find((url) => { @@ -107,3 +104,47 @@ export function renderConfigEditor(renderer: ManifestRenderer, pluginManifest: M renderer.manifestGui?.classList.add("plugin-editor"); renderer.manifestGui?.classList.add("rendered"); } + +async function loadListeners({ + renderer, + pluginManifest, + withPluginOrInstalled, + add, + remove, + resetToDefaultButton, +}: { + renderer: ManifestRenderer; + pluginManifest: Manifest | null; + withPluginOrInstalled: boolean; + add: HTMLButtonElement; + remove: HTMLButtonElement; + resetToDefaultButton: HTMLButtonElement; +}) { + function addHandler() { + writeNewConfig(renderer, "add"); + } + function removeHandler() { + writeNewConfig(renderer, "remove"); + } + function resetToDefaultHandler() { + handleResetToDefault(renderer, pluginManifest); + } + + await (async () => { + getTrackedEventListeners(remove, "click")?.forEach((listener) => { + removeTrackedEventListener(remove, "click", listener); + }); + getTrackedEventListeners(add, "click")?.forEach((listener) => { + removeTrackedEventListener(add, "click", listener); + }); + getTrackedEventListeners(resetToDefaultButton, "click")?.forEach((listener) => { + removeTrackedEventListener(resetToDefaultButton, "click", listener); + }); + })(); + + addTrackedEventListener(resetToDefaultButton, "click", resetToDefaultHandler); + addTrackedEventListener(add, "click", addHandler); + if (withPluginOrInstalled) { + addTrackedEventListener(remove, "click", removeHandler); + } +} From 728dd9f9db7ca959ff47c2c1ca2f8c8b05d036f6 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Tue, 26 Nov 2024 08:06:17 +0000 Subject: [PATCH 23/33] chore: process typebox unions for convo-rewards --- static/scripts/rendering/input-parsing.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/static/scripts/rendering/input-parsing.ts b/static/scripts/rendering/input-parsing.ts index fd56ba0..ef4f1e1 100644 --- a/static/scripts/rendering/input-parsing.ts +++ b/static/scripts/rendering/input-parsing.ts @@ -14,6 +14,14 @@ export function processProperties(renderer: ManifestRenderer, props: Record { + processProperties(renderer, subProp.properties || {}, fullKey); + }); + } } else { createInputRow(fullKey, prop, renderer.configDefaults); } From 4344b97ccc03a75d3d46b9a48b5f78ebf4832924 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Tue, 26 Nov 2024 08:06:44 +0000 Subject: [PATCH 24/33] chore: remove dupe storage set --- static/scripts/rendering/org-select.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/static/scripts/rendering/org-select.ts b/static/scripts/rendering/org-select.ts index ee4f762..2599210 100644 --- a/static/scripts/rendering/org-select.ts +++ b/static/scripts/rendering/org-select.ts @@ -60,11 +60,9 @@ export function renderOrgPicker(renderer: ManifestRenderer, orgs: string[]) { const textSpan = createElement("span", { textContent: org }); optionDiv.appendChild(textSpan); - optionDiv.addEventListener("click", () => { - handleOrgSelection(renderer, org); selectSelected.textContent = org; - localStorage.setItem("selectedOrg", org); + handleOrgSelection(renderer, org); }); selectItems.appendChild(optionDiv); From 0897172f583d0746d722dcb8d0d7aff0fef0461f Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Tue, 26 Nov 2024 08:07:08 +0000 Subject: [PATCH 25/33] chore: toast typos --- static/scripts/rendering/write-add-remove.ts | 27 +++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/static/scripts/rendering/write-add-remove.ts b/static/scripts/rendering/write-add-remove.ts index 9d743bd..277349c 100644 --- a/static/scripts/rendering/write-add-remove.ts +++ b/static/scripts/rendering/write-add-remove.ts @@ -3,8 +3,10 @@ import { ManifestRenderer } from "../render-manifest"; import { Manifest, Plugin } from "../../types/plugins"; import { parseConfigInputs } from "./input-parsing"; import { getOfficialPluginConfig } from "../../utils/storage"; +import { renderConfigEditor } from "./config-editor"; +import { normalizePluginName } from "./utils"; -export function writeNewConfig(renderer: ManifestRenderer, option: "add" | "remove"): void { +export function writeNewConfig(renderer: ManifestRenderer, option: "add" | "remove") { const selectedManifest = localStorage.getItem("selectedPluginManifest"); if (!selectedManifest) { toastNotification("No selected plugin manifest found.", { @@ -25,11 +27,7 @@ export function writeNewConfig(renderer: ManifestRenderer, option: "add" | "remo const pluginName = pluginManifest.name; // this relies on the manifest matching the repo name - const normalizedPluginName = pluginName - .toLowerCase() - .replace(/ /g, "-") - .replace(/[^a-z0-9-]/g, "") - .replace(/-+/g, "-"); + const normalizedPluginName = normalizePluginName(pluginName); const pluginUrl = Object.keys(officialPluginConfig).find((url) => { return url.includes(normalizedPluginName); @@ -54,14 +52,14 @@ export function writeNewConfig(renderer: ManifestRenderer, option: "add" | "remo if (option === "add") { handleAddPlugin(renderer, plugin, pluginManifest); - } else { + } else if (option === "remove") { handleRemovePlugin(renderer, plugin, pluginManifest); } } function handleAddPlugin(renderer: ManifestRenderer, plugin: Plugin, pluginManifest: Manifest): void { renderer.configParser.addPlugin(plugin); - toastNotification(`Configuration for ${pluginManifest.name} saved successfully.Do you want to push to GitHub ? `, { + toastNotification(`Configuration for ${pluginManifest.name} saved successfully. Do you want to push to GitHub?`, { type: "success", actionText: "Push to GitHub", action: async () => { @@ -97,7 +95,7 @@ function handleAddPlugin(renderer: ManifestRenderer, plugin: Plugin, pluginManif function handleRemovePlugin(renderer: ManifestRenderer, plugin: Plugin, pluginManifest: Manifest): void { renderer.configParser.removePlugin(plugin); - toastNotification(`Configuration for ${pluginManifest.name} removed successfully.Do you want to push to GitHub ? `, { + toastNotification(`Configuration for ${pluginManifest.name} removed successfully. Do you want to push to GitHub?`, { type: "success", actionText: "Push to GitHub", action: async () => { @@ -130,3 +128,14 @@ function handleRemovePlugin(renderer: ManifestRenderer, plugin: Plugin, pluginMa }, }); } + +export function handleResetToDefault(renderer: ManifestRenderer, pluginManifest: Manifest | null) { + if (!pluginManifest) { + throw new Error("No plugin manifest found"); + } + renderConfigEditor(renderer, pluginManifest); + const readmeContainer = document.querySelector(".readme-container"); + if (readmeContainer) { + readmeContainer.remove(); + } +} From 40c4fce8434f5757f628be0da0776353ac7b420a Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Tue, 26 Nov 2024 08:07:38 +0000 Subject: [PATCH 26/33] chore: tracked listeners, normalize util --- static/scripts/rendering/utils.ts | 39 +++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/static/scripts/rendering/utils.ts b/static/scripts/rendering/utils.ts index f93ec78..9789aa9 100644 --- a/static/scripts/rendering/utils.ts +++ b/static/scripts/rendering/utils.ts @@ -1,5 +1,14 @@ import { STRINGS } from "../../utils/strings"; +// this relies on the manifest matching the repo name +export function normalizePluginName(pluginName: string): string { + return pluginName + .toLowerCase() + .replace(/ /g, "-") + .replace(/[^a-z0-9-]/g, "") + .replace(/-+/g, "-"); +} + export function updateGuiTitle(title: string): void { const guiTitle = document.querySelector("#manifest-gui-title"); if (!guiTitle) { @@ -18,3 +27,33 @@ export function closeAllSelect() { item.classList.remove(STRINGS.SELECT_ARROW_ACTIVE); }); } + +const eventListenersMap = new WeakMap>(); +export function addTrackedEventListener(target: EventTarget, type: string, listener: EventListener) { + if (!eventListenersMap.has(target)) { + eventListenersMap.set(target, new Map()); + } + const listeners = eventListenersMap.get(target)?.get(type) || []; + if (!listeners.map((l) => l.name).includes(listener.name)) { + listeners.push(listener); + eventListenersMap.get(target)?.set(type, listeners); + target.addEventListener(type, listener); + } +} + +export function removeTrackedEventListener(target: EventTarget, type: string, listener: EventListener) { + const listeners = eventListenersMap.get(target)?.get(type) || []; + const index = listeners.findIndex((l) => l.name === listener.name); + if (index !== -1) { + listeners.splice(index, 1); + eventListenersMap?.get(target)?.set(type, listeners); + target.removeEventListener(type, listener); + } +} + +export function getTrackedEventListeners(target: EventTarget, type: string): EventListener[] { + if (eventListenersMap.has(target)) { + return eventListenersMap.get(target)?.get(type) || []; + } + return []; +} From b2addf731eae36438fcddb5fd5dda62dfb574b54 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Tue, 26 Nov 2024 09:00:10 +0000 Subject: [PATCH 27/33] chore: required fields input assert and display --- static/scripts/config-parser.ts | 23 +++----------- static/scripts/rendering/config-editor.ts | 2 +- static/scripts/rendering/input-parsing.ts | 32 +++++++++++++++----- static/scripts/rendering/write-add-remove.ts | 19 +++++++++++- static/types/plugins.ts | 1 + static/utils/element-helpers.ts | 4 ++- 6 files changed, 51 insertions(+), 30 deletions(-) diff --git a/static/scripts/config-parser.ts b/static/scripts/config-parser.ts index 147bede..28dd0ea 100644 --- a/static/scripts/config-parser.ts +++ b/static/scripts/config-parser.ts @@ -109,19 +109,7 @@ export class ConfigParser { } async updateConfig(org: string, octokit: Octokit, option: "add" | "remove", path = CONFIG_FULL_PATH, repo = CONFIG_ORG_REPO) { - const repoPlugins = this.parseConfig(this.repoConfig).plugins; - const newPlugins = this.parseConfig().plugins; - - if (!newPlugins?.length && option === "add") { - throw new Error("No plugins found in the config"); - } - - if (option === "add") { - this.newConfigYml = YAML.stringify({ plugins: repoPlugins }); - } else if (option === "remove") { - this.newConfigYml = YAML.stringify({ plugins: newPlugins }); - } - + this.repoConfig = this.newConfigYml; this.saveConfig(); return this.createOrUpdateFileContents(org, repo, path, octokit); } @@ -167,9 +155,7 @@ export class ConfigParser { parsedConfig.plugins.push(plugin); } - parsedConfig.plugins.push(plugin); - const newConfig = YAML.stringify(parsedConfig); - this.newConfigYml = newConfig; + this.newConfigYml = YAML.stringify(parsedConfig); this.saveConfig(); } @@ -177,13 +163,12 @@ export class ConfigParser { const config = this.loadConfig(); const parsedConfig = this.parseConfig(config); if (!parsedConfig.plugins) { - console.log("No plugins to remove"); + toastNotification("No plugins found in config", { type: "error" }); return; } parsedConfig.plugins = parsedConfig.plugins.filter((p: Plugin) => p.uses[0].plugin !== plugin.uses[0].plugin); - const newConfig = YAML.stringify(parsedConfig); - this.newConfigYml = newConfig; + this.newConfigYml = YAML.stringify(parsedConfig); this.saveConfig(); } diff --git a/static/scripts/rendering/config-editor.ts b/static/scripts/rendering/config-editor.ts index 7c75f73..47085ed 100644 --- a/static/scripts/rendering/config-editor.ts +++ b/static/scripts/rendering/config-editor.ts @@ -13,7 +13,7 @@ export function renderConfigEditor(renderer: ManifestRenderer, pluginManifest: M renderer.backButton.style.display = "block"; renderer.manifestGuiBody.innerHTML = null; controlButtons({ hide: false }); - processProperties(renderer, pluginManifest?.configuration.properties || {}); + processProperties(renderer, pluginManifest, pluginManifest?.configuration.properties || {}, null); const configInputs = document.querySelectorAll(".config-input"); if (plugin) { diff --git a/static/scripts/rendering/input-parsing.ts b/static/scripts/rendering/input-parsing.ts index ef4f1e1..5cc341c 100644 --- a/static/scripts/rendering/input-parsing.ts +++ b/static/scripts/rendering/input-parsing.ts @@ -4,22 +4,29 @@ import { ManifestRenderer } from "../render-manifest"; import { Manifest } from "../../types/plugins"; const ajv = new AJV({ allErrors: true, coerceTypes: true, strict: true }); -export function processProperties(renderer: ManifestRenderer, props: Record, prefix: string | null = null) { +export function processProperties( + renderer: ManifestRenderer, + manifest: Manifest | null | undefined, + props: Record, + prefix: string | null = null +) { + const required = manifest?.configuration?.required || []; + Object.keys(props).forEach((key) => { const fullKey = prefix ? `${prefix}.${key}` : key; - const prop = props[key] as Manifest["configuration"]; + const prop = props[key]; if (!prop) { return; } if (prop.type === "object" && prop.properties) { - processProperties(renderer, prop.properties, fullKey); + processProperties(renderer, manifest, prop.properties, fullKey); } else if ("anyOf" in prop && Array.isArray(prop.anyOf)) { if (prop.default) { - createInputRow(fullKey, prop, renderer.configDefaults); + createInputRow(fullKey, prop, renderer.configDefaults, required.includes(fullKey)); } else { prop.anyOf?.forEach((subProp) => { - processProperties(renderer, subProp.properties || {}, fullKey); + processProperties(renderer, manifest, subProp.properties || {}, fullKey); }); } } else { @@ -28,12 +35,16 @@ export function processProperties(renderer: ManifestRenderer, props: Record, manifest: Manifest): { [key: string]: unknown } { +export function parseConfigInputs( + configInputs: NodeListOf, + manifest: Manifest +): { config: Record; missing: string[] } { const config: Record = {}; const schema = manifest.configuration; if (!schema) { throw new Error("No schema found in manifest"); } + const required = schema.required || []; const validate = ajv.compile(schema as AnySchemaObject); configInputs.forEach((input) => { @@ -68,12 +79,17 @@ export function parseConfigInputs(configInputs: NodeListOf(".config-input"); - const newConfig = parseConfigInputs(configInputs, pluginManifest); + const { config: newConfig, missing } = parseConfigInputs(configInputs, pluginManifest); + + if (missing.length) { + toastNotification("Please fill out all required fields.", { + type: "error", + shouldAutoDismiss: true, + }); + missing.forEach((key) => { + const ele = document.querySelector(`[data-config-key="${key}"]`) as HTMLInputElement | HTMLTextAreaElement | null; + if (ele) { + ele.style.border = "1px solid red"; + ele.focus(); + } else { + console.log(`Input element with key ${key} not found`); + } + }); + return; + } renderer.configParser.loadConfig(); diff --git a/static/types/plugins.ts b/static/types/plugins.ts index 49f1914..7f3404a 100644 --- a/static/types/plugins.ts +++ b/static/types/plugins.ts @@ -36,6 +36,7 @@ export type Manifest = { type: string; }; properties?: Record; + required?: string[]; }; readme?: string; }; diff --git a/static/utils/element-helpers.ts b/static/utils/element-helpers.ts index 73d3afd..b7865ff 100644 --- a/static/utils/element-helpers.ts +++ b/static/utils/element-helpers.ts @@ -23,7 +23,8 @@ export function createElement( export function createInputRow( key: string, prop: Manifest["configuration"], - configDefaults: Record + configDefaults: Record, + required = false ): void { const row = document.createElement("tr"); @@ -34,6 +35,7 @@ export function createInputRow( const valueCell = document.createElement("td"); valueCell.className = "table-data-value"; + valueCell.ariaRequired = `${required}`; const input = createInput(key, prop.default, prop); valueCell.appendChild(input); From 588377a1b8c0fe9e30b4efe497da4ec810fb7626 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Tue, 26 Nov 2024 09:56:00 +0000 Subject: [PATCH 28/33] chore: more robust readme fetching, cleanup, input type assertion --- static/scripts/fetch-manifest.ts | 75 ++++++++------------ static/scripts/rendering/input-parsing.ts | 22 ++++-- static/scripts/rendering/write-add-remove.ts | 11 +-- 3 files changed, 47 insertions(+), 61 deletions(-) diff --git a/static/scripts/fetch-manifest.ts b/static/scripts/fetch-manifest.ts index e39bc08..aa5f467 100644 --- a/static/scripts/fetch-manifest.ts +++ b/static/scripts/fetch-manifest.ts @@ -24,15 +24,12 @@ export class ManifestFetcher { } const repos = await this._octokit.repos.listForOrg({ org }); const manifestCache = this.checkManifestCache(); - function makeUrl(org: string, repo: string, file: string) { - return `https://raw.githubusercontent.com/${org}/${repo}/development/${file}`; - } for (const repo of repos.data) { - const manifestUrl = makeUrl(org, repo.name, "manifest.json"); - const manifest = await this.fetchActionManifest(manifestUrl); + const manifestUrl = this.createGithubRawEndpoint(org, repo.name, "development", "manifest.json"); + const manifest = await this.fetchPluginManifest(manifestUrl); const decoded = this.decodeManifestFromFetch(manifest); - const readme = await this._fetchPluginReadme(makeUrl(org, repo.name, "README.md")); + const readme = await this.fetchPluginReadme(this.createGithubRawEndpoint(org, repo.name, "development", "README.md")); if (decoded) { manifestCache[manifestUrl] = { ...decoded, readme }; @@ -61,9 +58,9 @@ export class ManifestFetcher { } } - createActionEndpoint(owner: string, repo: string, branch: string) { + createGithubRawEndpoint(owner: string, repo: string, branch: string, path: string) { // no endpoint so we fetch the raw content from the owner/repo/branch - return `https://raw.githubusercontent.com/${owner}/${repo}/refs/heads/${branch}/manifest.json`; + return `https://raw.githubusercontent.com/${owner}/${repo}/refs/heads/${branch}/${path}`; } captureActionUrls(config: string) { @@ -92,33 +89,7 @@ export class ManifestFetcher { return officialPluginConfig; } - async fetchWorkerManifest(workerUrl: string) { - const url = workerUrl + "/manifest.json"; - try { - const response = await fetch(url, { - headers: { - "Content-Type": "application/json", - }, - method: "GET", - }); - return await response.json(); - } catch (e) { - let error = e; - try { - const res = await fetch(url.replace(/development/g, "main")); - return await res.json(); - } catch (e) { - error = e; - } - console.error(error); - if (error instanceof Error) { - return { workerUrl, error: error.message }; - } - return { workerUrl, error: String(error) }; - } - } - - async fetchActionManifest(actionUrl: string) { + async fetchPluginManifest(actionUrl: string) { try { const response = await fetch(actionUrl); return await response.json(); @@ -131,24 +102,36 @@ export class ManifestFetcher { } } - private async _fetchPluginReadme(pluginUrl: string) { + async fetchPluginReadme(pluginUrl: string) { + async function handle404(result: string, octokit?: Octokit | null) { + if (result.includes("404: Not Found")) { + const [owner, repo] = pluginUrl.split("/").slice(3, 5); + const readme = await octokit?.repos.getContent({ + owner, + repo, + path: "README.md", + }); + + if (readme && "content" in readme.data) { + return atob(readme.data.content); + } else { + return "No README.md found"; + } + } + + return result; + } try { - const response = await fetch(pluginUrl, { - headers: { - "Content-Type": "application/json", - }, - method: "GET", - }); - return await response.text(); + const response = await fetch(pluginUrl, { signal: new AbortController().signal }); + return await handle404(await response.text(), this._octokit); } catch (e) { let error = e; try { - const res = await fetch(pluginUrl.replace(/development/g, "main")); - return await res.text(); + const res = await fetch(pluginUrl.replace(/development/g, "main"), { signal: new AbortController().signal }); + return await handle404(await res.text(), this._octokit); } catch (e) { error = e; } - console.error(error); if (error instanceof Error) { return error.message; } diff --git a/static/scripts/rendering/input-parsing.ts b/static/scripts/rendering/input-parsing.ts index 5cc341c..0f36331 100644 --- a/static/scripts/rendering/input-parsing.ts +++ b/static/scripts/rendering/input-parsing.ts @@ -70,12 +70,15 @@ export function parseConfigInputs( if (expectedType === "boolean") { value = (input as HTMLInputElement).checked; } else if (expectedType === "object" || expectedType === "array") { - try { - value = JSON.parse((input as HTMLTextAreaElement).value); - } catch (e) { - console.error(e); - throw new Error(`Invalid JSON input for ${expectedType} at key "${key}": ${input.value}`); - } + if (!input.value) { + value = expectedType === "object" ? {} : []; + } else + try { + value = JSON.parse((input as HTMLTextAreaElement).value); + } catch (e) { + console.error(e); + throw new Error(`Invalid JSON input for ${expectedType} at key "${key}": ${input.value}`); + } } else { value = (input as HTMLInputElement).value; } @@ -85,7 +88,12 @@ export function parseConfigInputs( if (validate(config)) { const missing = []; for (const key of required) { - if (!(config[key] || config[key] === "undefined" || config[key] === "null")) { + const isBoolean = schema.properties && schema.properties[key] && schema.properties[key].type === "boolean"; + if ((isBoolean && config[key] === false) || config[key] === true) { + continue; + } + + if (!config[key] || config[key] === "undefined" || config[key] === "null") { missing.push(key); } } diff --git a/static/scripts/rendering/write-add-remove.ts b/static/scripts/rendering/write-add-remove.ts index ea792e1..775a358 100644 --- a/static/scripts/rendering/write-add-remove.ts +++ b/static/scripts/rendering/write-add-remove.ts @@ -38,20 +38,15 @@ export function writeNewConfig(renderer: ManifestRenderer, option: "add" | "remo } renderer.configParser.loadConfig(); - - const officialPluginConfig: Record = getOfficialPluginConfig(); - - const pluginName = pluginManifest.name; - // this relies on the manifest matching the repo name - const normalizedPluginName = normalizePluginName(pluginName); - + const normalizedPluginName = normalizePluginName(pluginManifest.name); + const officialPluginConfig: Record = getOfficialPluginConfig(); const pluginUrl = Object.keys(officialPluginConfig).find((url) => { return url.includes(normalizedPluginName); }); if (!pluginUrl) { - toastNotification(`No plugin URL found for ${pluginName}.`, { + toastNotification(`No plugin URL found for ${normalizedPluginName}.`, { type: "error", shouldAutoDismiss: true, }); From 1f61f21928694774d77e4b87d6184c5424ea42a1 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Tue, 26 Nov 2024 10:23:47 +0000 Subject: [PATCH 29/33] chore: comments --- .cspell.json | 2 +- static/scripts/config-parser.ts | 7 +++++++ static/scripts/fetch-manifest.ts | 10 +++++++++- static/scripts/render-manifest.ts | 4 ++++ static/scripts/rendering/config-editor.ts | 21 +++++++++++++++++++- static/scripts/rendering/input-parsing.ts | 13 +++++++++++- static/scripts/rendering/org-select.ts | 3 +++ static/scripts/rendering/plugin-select.ts | 4 ++++ static/scripts/rendering/write-add-remove.ts | 8 +++++++- 9 files changed, 67 insertions(+), 5 deletions(-) diff --git a/.cspell.json b/.cspell.json index c9a8062..7ab02ee 100644 --- a/.cspell.json +++ b/.cspell.json @@ -8,5 +8,5 @@ "dictionaries": ["typescript", "node", "software-terms"], "import": ["@cspell/dict-typescript/cspell-ext.json", "@cspell/dict-node/cspell-ext.json", "@cspell/dict-software-terms"], "ignoreRegExpList": ["[0-9a-fA-F]{6}"], - "ignoreWords": ["ubiquibot", "Supabase", "supabase", "SUPABASE", "sonarjs", "mischeck"] + "ignoreWords": ["ubiquibot", "Supabase", "supabase", "SUPABASE", "sonarjs", "mischeck", "Typebox"] } diff --git a/static/scripts/config-parser.ts b/static/scripts/config-parser.ts index 28dd0ea..8820503 100644 --- a/static/scripts/config-parser.ts +++ b/static/scripts/config-parser.ts @@ -4,6 +4,13 @@ import { Octokit } from "@octokit/rest"; import { toastNotification } from "../utils/toaster"; import { CONFIG_FULL_PATH, CONFIG_ORG_REPO } from "@ubiquity-os/plugin-sdk/constants"; +/** + * Responsible for fetching, parsing, and updating the user's installed plugin configurations. + * + * - `configRepoExistenceCheck` checks if the user has a config repo and creates one if not + * - `repoFileExistenceCheck` checks if the user has a config file and creates one if not + * - `fetchUserInstalledConfig` fetches the user's installed config from the config repo + */ export class ConfigParser { repoConfig: string | null = null; repoConfigSha: string | null = null; diff --git a/static/scripts/fetch-manifest.ts b/static/scripts/fetch-manifest.ts index aa5f467..9a02094 100644 --- a/static/scripts/fetch-manifest.ts +++ b/static/scripts/fetch-manifest.ts @@ -3,6 +3,15 @@ import { Manifest, ManifestPreDecode } from "../types/plugins"; import { DEV_CONFIG_FULL_PATH, CONFIG_FULL_PATH, CONFIG_ORG_REPO } from "@ubiquity-os/plugin-sdk/constants"; import { getOfficialPluginConfig } from "../utils/storage"; +/** + * Responsible for: + * - Mainly UbiquityOS Marketplace data fetching (config-parser fetches user configs) + * - Fetching the manifest.json files from the marketplace + * - Fetching the README.md files from the marketplace + * - Fetching the official plugin config from the orgs + * - Capturing the worker and action urls from the official plugin config (will be taken from the manifest directly soon) + * - Storing the fetched data in localStorage + */ export class ManifestFetcher { private _orgs: string[]; private _octokit: Octokit | null; @@ -41,7 +50,6 @@ export class ManifestFetcher { } checkManifestCache(): Record { - // check if the manifest is already in the cache const manifestCache = localStorage.getItem("manifestCache"); if (manifestCache) { return JSON.parse(manifestCache); diff --git a/static/scripts/render-manifest.ts b/static/scripts/render-manifest.ts index 4ed20c4..aabb867 100644 --- a/static/scripts/render-manifest.ts +++ b/static/scripts/render-manifest.ts @@ -4,6 +4,10 @@ import { ExtendedHtmlElement } from "../types/github"; import { controlButtons } from "./rendering/control-buttons"; import { createBackButton } from "./rendering/navigation"; +/** + * More of a controller than a renderer, this is responsible for rendering the manifest GUI + * and managing the state of the GUI with the help of the rendering functions. + */ export class ManifestRenderer { private _manifestGui: HTMLElement; private _manifestGuiBody: ExtendedHtmlElement; diff --git a/static/scripts/rendering/config-editor.ts b/static/scripts/rendering/config-editor.ts index 47085ed..d3fd371 100644 --- a/static/scripts/rendering/config-editor.ts +++ b/static/scripts/rendering/config-editor.ts @@ -8,6 +8,22 @@ import MarkdownIt from "markdown-it"; import { getManifestCache } from "../../utils/storage"; const md = new MarkdownIt(); +/** + * Displays the plugin configuration editor. + * + * - `pluginManifest` should never be null or there was a problem fetching from the marketplace + * - `plugin` should only be passed in if you intend on replacing the default configuration with their installed configuration + * + * Allows for: + * - Adding a single plugin configuration + * - Removing a single plugin configuration + * - Resetting the plugin configuration to the schema default + * - Building multiple plugins like a "shopping cart" and they all get pushed at once in the background + * + * Compromises: + * - Typebox Unions get JSON.stringify'd and displayed as one string meaning `text-conversation-rewards` has a monster config for HTML tags + * - Plugin config objects are split like `plugin.config.key` and `plugin.config.key2` and `plugin.config.key3` and so on + */ export function renderConfigEditor(renderer: ManifestRenderer, pluginManifest: Manifest | null, plugin?: Plugin["uses"][0]["with"]): void { renderer.currentStep = "configEditor"; renderer.backButton.style.display = "block"; @@ -16,6 +32,7 @@ export function renderConfigEditor(renderer: ManifestRenderer, pluginManifest: M processProperties(renderer, pluginManifest, pluginManifest?.configuration.properties || {}, null); const configInputs = document.querySelectorAll(".config-input"); + // If plugin is passed in, we want to inject those values into the inputs if (plugin) { configInputs.forEach((input) => { const key = input.getAttribute("data-config-key"); @@ -56,6 +73,7 @@ export function renderConfigEditor(renderer: ManifestRenderer, pluginManifest: M } const parsedConfig = renderer.configParser.parseConfig(renderer.configParser.repoConfig || localStorage.getItem("config")); + // for when `resetToDefault` is called and no plugin gets passed in, we still want to show the remove button const isInstalled = parsedConfig.plugins?.find((p) => p.uses[0].plugin.includes(normalizePluginName(pluginManifest?.name || ""))); loadListeners({ @@ -75,7 +93,7 @@ export function renderConfigEditor(renderer: ManifestRenderer, pluginManifest: M remove.classList.add("disabled"); } - resetToDefaultButton.hidden = !!plugin; + resetToDefaultButton.hidden = !!(plugin || isInstalled); const manifestCache = getManifestCache(); const pluginUrls = Object.keys(manifestCache); const pluginUrl = pluginUrls.find((url) => { @@ -130,6 +148,7 @@ async function loadListeners({ handleResetToDefault(renderer, pluginManifest); } + // ensure the listeners are removed before adding new ones await (async () => { getTrackedEventListeners(remove, "click")?.forEach((listener) => { removeTrackedEventListener(remove, "click", listener); diff --git a/static/scripts/rendering/input-parsing.ts b/static/scripts/rendering/input-parsing.ts index 0f36331..a8bd6c0 100644 --- a/static/scripts/rendering/input-parsing.ts +++ b/static/scripts/rendering/input-parsing.ts @@ -2,8 +2,13 @@ import AJV, { AnySchemaObject } from "ajv"; import { createInputRow } from "../../utils/element-helpers"; import { ManifestRenderer } from "../render-manifest"; import { Manifest } from "../../types/plugins"; + +// Without the raw Typebox Schema it was difficult to use Typebox which is why I've used AJV to validate the configuration. const ajv = new AJV({ allErrors: true, coerceTypes: true, strict: true }); +/** + * This creates the input rows for the configuration editor for any given plugin. + */ export function processProperties( renderer: ManifestRenderer, manifest: Manifest | null | undefined, @@ -11,7 +16,6 @@ export function processProperties( prefix: string | null = null ) { const required = manifest?.configuration?.required || []; - Object.keys(props).forEach((key) => { const fullKey = prefix ? `${prefix}.${key}` : key; const prop = props[key]; @@ -35,6 +39,13 @@ export function processProperties( }); } +/** + * This parse the inputs from the configuration editor and returns the configuration object. + * It also returns an array of missing required fields if any. + * + * It should become a priority to establish API like usage of `null` and `undefined` in our schemas so it's + * easier and less buggy when using the installer. + */ export function parseConfigInputs( configInputs: NodeListOf, manifest: Manifest diff --git a/static/scripts/rendering/org-select.ts b/static/scripts/rendering/org-select.ts index 2599210..065d49a 100644 --- a/static/scripts/rendering/org-select.ts +++ b/static/scripts/rendering/org-select.ts @@ -6,6 +6,9 @@ import { controlButtons } from "./control-buttons"; import { renderPluginSelector } from "./plugin-select"; import { closeAllSelect, updateGuiTitle } from "./utils"; +/** + * Renders the orgs for the authenticated user to select from. + */ export function renderOrgPicker(renderer: ManifestRenderer, orgs: string[]) { renderer.currentStep = "orgPicker"; controlButtons({ hide: true }); diff --git a/static/scripts/rendering/plugin-select.ts b/static/scripts/rendering/plugin-select.ts index a67338d..c8ce75b 100644 --- a/static/scripts/rendering/plugin-select.ts +++ b/static/scripts/rendering/plugin-select.ts @@ -7,6 +7,10 @@ import { renderConfigEditor } from "./config-editor"; import { controlButtons } from "./control-buttons"; import { closeAllSelect, updateGuiTitle } from "./utils"; +/** + * Renders a dropdown of plugins taken from the marketplace with an installed indicator. + * The user can select a plugin and it will render the configuration editor for that plugin. + */ export function renderPluginSelector(renderer: ManifestRenderer): void { renderer.currentStep = "pluginSelector"; renderer.backButton.style.display = "block"; diff --git a/static/scripts/rendering/write-add-remove.ts b/static/scripts/rendering/write-add-remove.ts index 775a358..5abe271 100644 --- a/static/scripts/rendering/write-add-remove.ts +++ b/static/scripts/rendering/write-add-remove.ts @@ -6,6 +6,13 @@ import { getOfficialPluginConfig } from "../../utils/storage"; import { renderConfigEditor } from "./config-editor"; import { normalizePluginName } from "./utils"; +/** + * Writes the new configuration to the config file. This does not push the config to GitHub + * only updates the local config. The actual push event is handled via a toast notification. + * + * - Acts as a "save" button for the configuration editor + * - Adds or removes a plugin configuration from the config file + */ export function writeNewConfig(renderer: ManifestRenderer, option: "add" | "remove") { const selectedManifest = localStorage.getItem("selectedPluginManifest"); if (!selectedManifest) { @@ -38,7 +45,6 @@ export function writeNewConfig(renderer: ManifestRenderer, option: "add" | "remo } renderer.configParser.loadConfig(); - // this relies on the manifest matching the repo name const normalizedPluginName = normalizePluginName(pluginManifest.name); const officialPluginConfig: Record = getOfficialPluginConfig(); const pluginUrl = Object.keys(officialPluginConfig).find((url) => { From 22b791298ed70e940a9f8b3d1f5632c092efe301 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Wed, 27 Nov 2024 17:53:24 +0000 Subject: [PATCH 30/33] chore: fix config save --- static/scripts/config-parser.ts | 12 +++++------- static/scripts/rendering/plugin-select.ts | 7 +++---- static/scripts/rendering/utils.ts | 5 +---- static/scripts/rendering/write-add-remove.ts | 4 ++-- 4 files changed, 11 insertions(+), 17 deletions(-) diff --git a/static/scripts/config-parser.ts b/static/scripts/config-parser.ts index 8820503..8f3ac27 100644 --- a/static/scripts/config-parser.ts +++ b/static/scripts/config-parser.ts @@ -115,9 +115,7 @@ export class ConfigParser { return YAML.parse(`${this.newConfigYml}`); } - async updateConfig(org: string, octokit: Octokit, option: "add" | "remove", path = CONFIG_FULL_PATH, repo = CONFIG_ORG_REPO) { - this.repoConfig = this.newConfigYml; - this.saveConfig(); + async updateConfig(org: string, octokit: Octokit, path = CONFIG_FULL_PATH, repo = CONFIG_ORG_REPO) { return this.createOrUpdateFileContents(org, repo, path, octokit); } @@ -151,8 +149,7 @@ export class ConfigParser { } addPlugin(plugin: Plugin) { - const config = this.loadConfig(); - const parsedConfig = this.parseConfig(config); + const parsedConfig = this.parseConfig(this.repoConfig); parsedConfig.plugins ??= []; const existingPlugin = parsedConfig.plugins.find((p) => p.uses[0].plugin === plugin.uses[0].plugin); @@ -163,12 +160,12 @@ export class ConfigParser { } this.newConfigYml = YAML.stringify(parsedConfig); + this.repoConfig = this.newConfigYml; this.saveConfig(); } removePlugin(plugin: Plugin) { - const config = this.loadConfig(); - const parsedConfig = this.parseConfig(config); + const parsedConfig = this.parseConfig(this.repoConfig); if (!parsedConfig.plugins) { toastNotification("No plugins found in config", { type: "error" }); return; @@ -176,6 +173,7 @@ export class ConfigParser { parsedConfig.plugins = parsedConfig.plugins.filter((p: Plugin) => p.uses[0].plugin !== plugin.uses[0].plugin); this.newConfigYml = YAML.stringify(parsedConfig); + this.repoConfig = this.newConfigYml; this.saveConfig(); } diff --git a/static/scripts/rendering/plugin-select.ts b/static/scripts/rendering/plugin-select.ts index c8ce75b..a7ee070 100644 --- a/static/scripts/rendering/plugin-select.ts +++ b/static/scripts/rendering/plugin-select.ts @@ -5,7 +5,7 @@ import { STRINGS } from "../../utils/strings"; import { ManifestRenderer } from "../render-manifest"; import { renderConfigEditor } from "./config-editor"; import { controlButtons } from "./control-buttons"; -import { closeAllSelect, updateGuiTitle } from "./utils"; +import { closeAllSelect, normalizePluginName, updateGuiTitle } from "./utils"; /** * Renders a dropdown of plugins taken from the marketplace with an installed indicator. @@ -62,9 +62,8 @@ export function renderPluginSelector(renderer: ManifestRenderer): void { if (!cleanManifestCache[url]?.name) { return; } - - const [, repo] = url.replace("https://raw.githubusercontent.com/", "").split("/"); - const reg = new RegExp(`${repo}`, "gi"); + const normalizedName = normalizePluginName(cleanManifestCache[url].name); + const reg = new RegExp(normalizedName, "i"); const installedPlugin: Plugin | undefined = installedPlugins.find((plugin) => plugin.uses[0].plugin.match(reg)); const defaultForInstalled: ManifestPreDecode | null = cleanManifestCache[url]; const optionText = defaultForInstalled.name; diff --git a/static/scripts/rendering/utils.ts b/static/scripts/rendering/utils.ts index 9789aa9..39604a7 100644 --- a/static/scripts/rendering/utils.ts +++ b/static/scripts/rendering/utils.ts @@ -52,8 +52,5 @@ export function removeTrackedEventListener(target: EventTarget, type: string, li } export function getTrackedEventListeners(target: EventTarget, type: string): EventListener[] { - if (eventListenersMap.has(target)) { - return eventListenersMap.get(target)?.get(type) || []; - } - return []; + return eventListenersMap.get(target)?.get(type) || []; } diff --git a/static/scripts/rendering/write-add-remove.ts b/static/scripts/rendering/write-add-remove.ts index 5abe271..b338a6c 100644 --- a/static/scripts/rendering/write-add-remove.ts +++ b/static/scripts/rendering/write-add-remove.ts @@ -93,7 +93,7 @@ function handleAddPlugin(renderer: ManifestRenderer, plugin: Plugin, pluginManif } try { - await renderer.configParser.updateConfig(org, octokit, "add"); + await renderer.configParser.updateConfig(org, octokit); } catch (error) { console.error("Error pushing config to GitHub:", error); toastNotification("An error occurred while pushing the configuration to GitHub.", { @@ -129,7 +129,7 @@ function handleRemovePlugin(renderer: ManifestRenderer, plugin: Plugin, pluginMa } try { - await renderer.configParser.updateConfig(org, octokit, "remove"); + await renderer.configParser.updateConfig(org, octokit); } catch (error) { console.error("Error pushing config to GitHub:", error); toastNotification("An error occurred while pushing the configuration to GitHub.", { From c2c587a165b6983fcebf125d7cbc52faf60c677e Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Wed, 27 Nov 2024 17:57:16 +0000 Subject: [PATCH 31/33] chore: auto dismiss add/remove plugin notification --- static/scripts/rendering/write-add-remove.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/static/scripts/rendering/write-add-remove.ts b/static/scripts/rendering/write-add-remove.ts index b338a6c..abfd3ca 100644 --- a/static/scripts/rendering/write-add-remove.ts +++ b/static/scripts/rendering/write-add-remove.ts @@ -80,6 +80,7 @@ function handleAddPlugin(renderer: ManifestRenderer, plugin: Plugin, pluginManif toastNotification(`Configuration for ${pluginManifest.name} saved successfully. Do you want to push to GitHub?`, { type: "success", actionText: "Push to GitHub", + shouldAutoDismiss: true, action: async () => { const octokit = renderer.auth.octokit; if (!octokit) { @@ -116,6 +117,7 @@ function handleRemovePlugin(renderer: ManifestRenderer, plugin: Plugin, pluginMa toastNotification(`Configuration for ${pluginManifest.name} removed successfully. Do you want to push to GitHub?`, { type: "success", actionText: "Push to GitHub", + shouldAutoDismiss: true, action: async () => { const octokit = renderer.auth.octokit; if (!octokit) { From 728218d1c4386674308ba3054c9b18fec82ae6cb Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Wed, 27 Nov 2024 20:15:39 +0000 Subject: [PATCH 32/33] chore: single push method, commit title update --- static/scripts/config-parser.ts | 2 +- static/scripts/rendering/write-add-remove.ts | 79 +++++++------------- 2 files changed, 28 insertions(+), 53 deletions(-) diff --git a/static/scripts/config-parser.ts b/static/scripts/config-parser.ts index 8f3ac27..25ea64e 100644 --- a/static/scripts/config-parser.ts +++ b/static/scripts/config-parser.ts @@ -142,7 +142,7 @@ export class ConfigParser { owner: org, repo: repo, path, - message: `chore: updating config`, + message: `chore: Plugin Installer UI - update`, content: btoa(this.newConfigYml), sha, }); diff --git a/static/scripts/rendering/write-add-remove.ts b/static/scripts/rendering/write-add-remove.ts index abfd3ca..f069d30 100644 --- a/static/scripts/rendering/write-add-remove.ts +++ b/static/scripts/rendering/write-add-remove.ts @@ -81,34 +81,7 @@ function handleAddPlugin(renderer: ManifestRenderer, plugin: Plugin, pluginManif type: "success", actionText: "Push to GitHub", shouldAutoDismiss: true, - action: async () => { - const octokit = renderer.auth.octokit; - if (!octokit) { - throw new Error("Octokit not found"); - } - - const org = localStorage.getItem("selectedOrg"); - - if (!org) { - throw new Error("No selected org found"); - } - - try { - await renderer.configParser.updateConfig(org, octokit); - } catch (error) { - console.error("Error pushing config to GitHub:", error); - toastNotification("An error occurred while pushing the configuration to GitHub.", { - type: "error", - shouldAutoDismiss: true, - }); - return; - } - - toastNotification("Configuration pushed to GitHub successfully.", { - type: "success", - shouldAutoDismiss: true, - }); - }, + action: () => notificationConfigPush(renderer), }); } @@ -118,34 +91,36 @@ function handleRemovePlugin(renderer: ManifestRenderer, plugin: Plugin, pluginMa type: "success", actionText: "Push to GitHub", shouldAutoDismiss: true, - action: async () => { - const octokit = renderer.auth.octokit; - if (!octokit) { - throw new Error("Octokit not found"); - } + action: () => notificationConfigPush(renderer), + }); +} + +async function notificationConfigPush(renderer: ManifestRenderer) { + const octokit = renderer.auth.octokit; + if (!octokit) { + throw new Error("Octokit not found"); + } - const org = localStorage.getItem("selectedOrg"); + const org = localStorage.getItem("selectedOrg"); - if (!org) { - throw new Error("No selected org found"); - } + if (!org) { + throw new Error("No selected org found"); + } - try { - await renderer.configParser.updateConfig(org, octokit); - } catch (error) { - console.error("Error pushing config to GitHub:", error); - toastNotification("An error occurred while pushing the configuration to GitHub.", { - type: "error", - shouldAutoDismiss: true, - }); - return; - } + try { + await renderer.configParser.updateConfig(org, octokit); + } catch (error) { + console.error("Error pushing config to GitHub:", error); + toastNotification("An error occurred while pushing the configuration to GitHub.", { + type: "error", + shouldAutoDismiss: true, + }); + return; + } - toastNotification("Configuration pushed to GitHub successfully.", { - type: "success", - shouldAutoDismiss: true, - }); - }, + toastNotification("Configuration pushed to GitHub successfully.", { + type: "success", + shouldAutoDismiss: true, }); } From 1ac5807e9bd0ec2b0c4681f06ba4235fdb82f6f4 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Thu, 28 Nov 2024 16:57:44 +0000 Subject: [PATCH 33/33] fix: check for boolean input on user config injection --- static/scripts/rendering/config-editor.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/static/scripts/rendering/config-editor.ts b/static/scripts/rendering/config-editor.ts index d3fd371..3c30f43 100644 --- a/static/scripts/rendering/config-editor.ts +++ b/static/scripts/rendering/config-editor.ts @@ -51,8 +51,10 @@ export function renderConfigEditor(renderer: ManifestRenderer, pluginManifest: M let value: string; - if (typeof currentObj === "object") { + if (typeof currentObj === "object" || Array.isArray(currentObj)) { value = JSON.stringify(currentObj, null, 2); + } else if (typeof currentObj === "boolean") { + value = currentObj ? "true" : "false"; } else { value = currentObj as string; } @@ -62,6 +64,10 @@ export function renderConfigEditor(renderer: ManifestRenderer, pluginManifest: M } else { (input as HTMLInputElement).value = value; } + + if (input.tagName === "INPUT" && (input as HTMLInputElement).type === "checkbox") { + (input as HTMLInputElement).checked = value === "true"; + } }); }