diff --git a/static/main.ts b/static/main.ts index eb5cc83..9b7e82a 100644 --- a/static/main.ts +++ b/static/main.ts @@ -18,23 +18,20 @@ export async function mainModule() { try { const ubiquityOrgsToFetchOfficialConfigFrom = ["ubiquity-os"]; const fetcher = new ManifestFetcher(ubiquityOrgsToFetchOfficialConfigFrom, auth.octokit); - const cache = fetcher.checkManifestCache(); if (auth.isActiveSession()) { - const userOrgs = await auth.getGitHubUserOrgs(); - - if (Object.keys(cache).length === 0) { - renderer.manifestGuiBody.dataset.loading = "true"; - const killNotification = toastNotification("Fetching manifest data...", { type: "info", shouldAutoDismiss: true }); - renderOrgPicker(renderer, []); - - await fetcher.fetchMarketplaceManifests(); - await fetcher.fetchOfficialPluginConfig(); - killNotification(); - renderer.manifestGuiBody.dataset.loading = "false"; - } + renderer.manifestGuiBody.dataset.loading = "true"; + const killNotification = toastNotification("Fetching manifest data...", { type: "info", shouldAutoDismiss: true }); + const userOrgs = await auth.getGitHubUserOrgs(); + const userOrgRepos = await auth.getGitHubUserOrgRepos(userOrgs); + localStorage.setItem("orgRepos", JSON.stringify(userOrgRepos)); renderOrgPicker(renderer, userOrgs); + + await fetcher.fetchMarketplaceManifests(); + await fetcher.fetchOfficialPluginConfig(); + renderer.manifestGuiBody.dataset.loading = "false"; + killNotification(); } else { renderOrgPicker(renderer, []); } diff --git a/static/scripts/authentication.ts b/static/scripts/authentication.ts index 327614a..74e8660 100644 --- a/static/scripts/authentication.ts +++ b/static/scripts/authentication.ts @@ -136,6 +136,16 @@ export class AuthService { return response.data.map((org: { login: string }) => org.login); } + public async getGitHubUserOrgRepos(orgs: string[]): Promise> { + const octokit = await this.getOctokit(); + const orgRepos: Record = {}; + for (const org of orgs) { + const response = await octokit.rest.repos.listForOrg({ org }); + orgRepos[org] = response.data.map((repo: { name: string }) => repo.name); + } + return orgRepos; + } + public async getOctokit(): Promise { if (this.octokit) return this.octokit; const token = await this.getSessionToken(); diff --git a/static/scripts/config-parser.ts b/static/scripts/config-parser.ts index 25ea64e..68fb36e 100644 --- a/static/scripts/config-parser.ts +++ b/static/scripts/config-parser.ts @@ -69,6 +69,10 @@ export class ConfigParser { } async fetchUserInstalledConfig(org: string, octokit: Octokit, repo = CONFIG_ORG_REPO, path = CONFIG_FULL_PATH) { + if (org === repo) { + repo = CONFIG_ORG_REPO; + } + if (repo === CONFIG_ORG_REPO) { await this.configRepoExistenceCheck(org, repo, octokit); } @@ -106,20 +110,22 @@ export class ConfigParser { } parseConfig(config?: string | null): PluginConfig { - if (config) { + if (config && typeof config === "string") { return YAML.parse(config); + } else { + return YAML.parse(this.loadConfig()); } - if (!this.newConfigYml) { - this.loadConfig(); - } - return YAML.parse(`${this.newConfigYml}`); } - async updateConfig(org: string, octokit: Octokit, path = CONFIG_FULL_PATH, repo = CONFIG_ORG_REPO) { + async updateConfig(org: string, octokit: Octokit, repo = CONFIG_ORG_REPO, path = CONFIG_FULL_PATH) { return this.createOrUpdateFileContents(org, repo, path, octokit); } async createOrUpdateFileContents(org: string, repo: string, path: string, octokit: Octokit) { + if (org === repo) { + repo = CONFIG_ORG_REPO; + } + const recentSha = await octokit.repos.getContent({ owner: org, repo: repo, diff --git a/static/scripts/render-manifest.ts b/static/scripts/render-manifest.ts index aabb867..09219f3 100644 --- a/static/scripts/render-manifest.ts +++ b/static/scripts/render-manifest.ts @@ -15,7 +15,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" | "pluginSelector" | "configEditor" = "orgPicker"; + private _currentStep: "orgPicker" | "repoPicker" | "pluginSelector" | "configEditor" = "orgPicker"; private _orgs: string[] = []; constructor(auth: AuthService) { @@ -45,11 +45,11 @@ export class ManifestRenderer { this._orgs = orgs; } - get currentStep(): "orgPicker" | "pluginSelector" | "configEditor" { + get currentStep(): "orgPicker" | "repoPicker" | "pluginSelector" | "configEditor" { return this._currentStep; } - set currentStep(step: "orgPicker" | "pluginSelector" | "configEditor") { + set currentStep(step: "orgPicker" | "repoPicker" | "pluginSelector" | "configEditor") { this._currentStep = step; } diff --git a/static/scripts/rendering/navigation.ts b/static/scripts/rendering/navigation.ts index d57b96a..a350b3d 100644 --- a/static/scripts/rendering/navigation.ts +++ b/static/scripts/rendering/navigation.ts @@ -2,6 +2,7 @@ import { createElement } from "../../utils/element-helpers"; import { ManifestRenderer } from "../render-manifest"; import { renderOrgPicker } from "./org-select"; import { renderPluginSelector } from "./plugin-select"; +import { renderRepoPicker } from "./repo-select"; export function createBackButton(renderer: ManifestRenderer): HTMLButtonElement { const backButton = createElement("button", { @@ -21,11 +22,12 @@ function handleBackButtonClick(renderer: ManifestRenderer): void { if (readmeContainer) { readmeContainer.remove(); } - + // "pluginSelector" | "configEditor" const step = renderer.currentStep; - - if (step === "pluginSelector" || step === "orgPicker") { + if (step === "repoPicker" || step === "orgPicker") { renderOrgPicker(renderer, renderer.orgs); + } else if (step === "pluginSelector") { + renderRepoPicker(renderer, JSON.parse(localStorage.getItem("orgRepos") || "{}")); } else if (step === "configEditor") { renderPluginSelector(renderer); } diff --git a/static/scripts/rendering/org-select.ts b/static/scripts/rendering/org-select.ts index 065d49a..3062032 100644 --- a/static/scripts/rendering/org-select.ts +++ b/static/scripts/rendering/org-select.ts @@ -1,9 +1,8 @@ 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 { renderRepoPicker } from "./repo-select"; import { closeAllSelect, updateGuiTitle } from "./utils"; /** @@ -85,16 +84,6 @@ function handleOrgSelection(renderer: ManifestRenderer, org: string): void { throw new Error("No org selected"); } localStorage.setItem("selectedOrg", org); - 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(); + const repos = JSON.parse(localStorage.getItem("orgRepos") || "{}"); + renderRepoPicker(renderer, repos); } diff --git a/static/scripts/rendering/repo-select.ts b/static/scripts/rendering/repo-select.ts new file mode 100644 index 0000000..675a7f6 --- /dev/null +++ b/static/scripts/rendering/repo-select.ts @@ -0,0 +1,130 @@ +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 { updateGuiTitle } from "./utils"; + +export function renderRepoPicker(renderer: ManifestRenderer, repos: Record): void { + renderer.currentStep = "repoPicker"; + controlButtons({ hide: true }); + renderer.backButton.style.display = "block"; + renderer.manifestGui?.classList.add("rendering"); + renderer.manifestGuiBody.innerHTML = null; + + if (!Reflect.ownKeys(repos).length) { + updateGuiTitle("No repositories found"); + renderer.manifestGuiBody.appendChild(document.createElement("tr")); + renderer.manifestGui?.classList.add("rendered"); + return; + } + + localStorage.setItem("orgRepos", JSON.stringify(repos)); + + const selectedOrg = localStorage.getItem("selectedOrg"); + + if (!selectedOrg) { + throw new Error(`No selected org found in local storage`); + } + + const topLevelRow = document.createElement("tr"); + const topLevelCell = document.createElement("td"); + topLevelCell.colSpan = 4; + topLevelCell.className = STRINGS.TDV_CENTERED; + + const useOrgConfigButton = createElement("button", { + id: "use-org-config-button", + textContent: `Use ${selectedOrg} config`, + }); + + useOrgConfigButton.style.width = "100%"; + useOrgConfigButton.style.textAlign = "left"; + + useOrgConfigButton.addEventListener("click", () => { + localStorage.setItem("selectedRepo", selectedOrg); + fetchOrgConfig(renderer, selectedOrg, selectedOrg) + .then(() => { + renderPluginSelector(renderer); + }) + .catch((error) => { + console.error(error); + toastNotification("Error fetching org config", { type: "error" }); + }); + }); + + topLevelCell.appendChild(useOrgConfigButton); + topLevelRow.appendChild(topLevelCell); + renderer.manifestGuiBody.appendChild(topLevelRow); + + const pickerRow = document.createElement("tr"); + const pickerCell = document.createElement("td"); + pickerCell.colSpan = 4; + pickerCell.className = STRINGS.TDV_CENTERED; + + updateGuiTitle("Select a Repository"); + + const repoSelect = createElement("select", { + id: "repo-picker-select", + class: STRINGS.PICKER_SELECT, + style: "width: 100%", + }); + + const defaultOption = createElement("option", { + value: null, + textContent: "Or select a repository...", + }); + repoSelect.appendChild(defaultOption); + + const orgRepos = repos[selectedOrg]; + + if (!orgRepos) { + throw new Error("No org repos found"); + } + + orgRepos.forEach((repo) => { + const option = createElement("option", { + value: repo, + textContent: repo, + }); + repoSelect.appendChild(option); + }); + + repoSelect.addEventListener("change", (event) => handleRepoSelection(event, renderer)); + pickerCell.appendChild(repoSelect); + pickerRow.appendChild(pickerCell); + renderer.manifestGuiBody.appendChild(pickerRow); + + renderer.manifestGui?.classList.add("rendered"); +} + +async function fetchOrgConfig(renderer: ManifestRenderer, org: string, repo: string): Promise { + const kill = toastNotification(`Fetching ${org} 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, repo); + kill(); +} + +function handleRepoSelection(event: Event, renderer: ManifestRenderer): void { + const selectElement = event.target as HTMLSelectElement; + const selectedRepo = selectElement.value; + if (selectedRepo) { + localStorage.setItem("selectedRepo", selectedRepo); + const selectedOrg = localStorage.getItem("selectedOrg"); + if (!selectedOrg) { + throw new Error("No selected org found"); + } + + fetchOrgConfig(renderer, selectedOrg, selectedRepo) + .then(() => { + renderPluginSelector(renderer); + }) + .catch((error) => { + console.error(error); + toastNotification("Error fetching org config", { type: "error" }); + }); + } +} diff --git a/static/scripts/rendering/write-add-remove.ts b/static/scripts/rendering/write-add-remove.ts index f069d30..7c41607 100644 --- a/static/scripts/rendering/write-add-remove.ts +++ b/static/scripts/rendering/write-add-remove.ts @@ -108,7 +108,12 @@ async function notificationConfigPush(renderer: ManifestRenderer) { } try { - await renderer.configParser.updateConfig(org, octokit); + const selectedRepo = localStorage.getItem("selectedRepo"); + if (!selectedRepo) { + throw new Error("No selected repo found"); + } + + await renderer.configParser.updateConfig(org, octokit, selectedRepo); } catch (error) { console.error("Error pushing config to GitHub:", error); toastNotification("An error occurred while pushing the configuration to GitHub.", { diff --git a/static/utils/strings.ts b/static/utils/strings.ts index 23dffe2..bb65ebe 100644 --- a/static/utils/strings.ts +++ b/static/utils/strings.ts @@ -4,4 +4,5 @@ export const STRINGS = { SELECT_SELECTED: ".select-selected", SELECT_HIDE: "select-hide", SELECT_ARROW_ACTIVE: "select-arrow-active", + PICKER_SELECT: "picker-select", }; diff --git a/static/utils/toaster.ts b/static/utils/toaster.ts index 7fd85c3..d726515 100644 --- a/static/utils/toaster.ts +++ b/static/utils/toaster.ts @@ -8,6 +8,7 @@ export function toastNotification( action?: () => void; shouldAutoDismiss?: boolean; duration?: number; + killAll?: boolean; } = {} ): () => void { const { type = "info", actionText, action, shouldAutoDismiss = false, duration = 5000 } = options; @@ -38,7 +39,16 @@ export function toastNotification( class: "toast-action", textContent: actionText, }); - actionButton.addEventListener("click", action); + + actionButton.addEventListener("click", async () => { + action(); + setTimeout(() => { + document.querySelectorAll(".toast").forEach((toast) => { + toast.classList.remove("show"); + setTimeout(() => toast.remove(), 250); + }); + }, 5000); + }); toastElement.appendChild(actionButton); }