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) => {