diff --git a/package.json b/package.json index 3227824..e5e92e4 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,81 @@ "Productivity" ], "license": "MIT", + "preferences": [ + { + "name": "preferredEditor", + "title": "Default Workspace Editor", + "description": "Choose your Preferred editor for Gitpod", + "type": "dropdown", + "data": [ + { + "title": "VS Code Browser", + "value": "code" + }, + { + "title": "VS Code Desktop", + "value": "code-desktop" + }, + { + "title": "IntelliJ", + "value": "intellij" + }, + { + "title": "GoLand", + "value": "goland" + }, + { + "title": "PhpStorm", + "value": "phpstorm" + }, + { + "title": "PyCharm", + "value": "pycharm" + }, + { + "title": "RubyMine", + "value": "rubymine" + }, + { + "title": "WebStorm", + "value": "webstorm" + }, + { + "title": "Rider", + "value": "rider" + }, + { + "title": "CLion", + "value": "clion" + } + ], + "required": true + }, + { + "name": "useLatest", + "label": "Latest Release (Unstable)", + "description": "Use the latest version for each editor. Insiders for VS Code, EAP for JetBrains IDEs.", + "type": "checkbox", + "required": true + }, + { + "name": "preferredEditorClass", + "title": "Default Workspace Class", + "description": "Up to 4 cores, 8GB RAM, 30GB storage in Standard and Up to 8 cores, 16GB RAM, 50GB storage in Large", + "type": "dropdown", + "data": [ + { + "title": "Standard", + "value": "g1-standard" + }, + { + "title": "Large", + "value": "g1-large" + } + ], + "required": true + } + ], "commands": [ { "name": "open_in_gitpod", @@ -41,17 +116,6 @@ ] } ], - - "preferences": [ - { - "name": "personalAccessToken", - "title": "Personal Access Token", - "placeholder": "Visit https://github.com/settings/tokens", - "description": "Enter Classic Personal Access Token from GitHub", - "type": "password", - "required": true - } - ], "dependencies": { "@graphql-codegen/cli": "^2.16.2", "@graphql-codegen/typescript-graphql-request": "^4.5.8", diff --git a/src/components/BranchListItem.tsx b/src/components/BranchListItem.tsx index f957242..82af784 100644 --- a/src/components/BranchListItem.tsx +++ b/src/components/BranchListItem.tsx @@ -1,7 +1,10 @@ -import { Action, ActionPanel, Color, List, open } from "@raycast/api"; +import { Action, ActionPanel, Color, Icon, List, open, useNavigation } from "@raycast/api"; +import { usePromise } from "@raycast/utils"; -import { branchStatus, GitpodIcons } from "../../constants"; +import { branchStatus, GitpodIcons, UIColors } from "../../constants"; import { BranchDetailsFragment, UserFieldsFragment } from "../generated/graphql"; +import OpenInGitpod, { getPreferencesForContext } from "../helpers/openInGitpod"; +import ContextPreferences from "../preferences/context_preferences"; type BranchItemProps = { branch: BranchDetailsFragment; @@ -11,9 +14,18 @@ type BranchItemProps = { }; export default function BranchListItem({ branch, mainBranch, repository }: BranchItemProps) { - const accessories: List.Item.Accessory[] = []; + const accessories: List.Item.Accessory[] = [] const branchURL = "https://github.com/" + repository + "/tree/" + branch.branchName; + const { data: preferences, revalidate } = usePromise( + async () => { + const response = await getPreferencesForContext("Branch", repository, branch.branchName); + return response; + }, + ); + + const { push } = useNavigation(); + if (branch.compData) { if (branch.compData.status) { switch (branch.compData.status.toString()) { @@ -44,6 +56,19 @@ export default function BranchListItem({ branch, mainBranch, repository }: Branc } } + accessories.unshift( + { + text: { + value: preferences?.preferredEditorClass === "g1-large" ? "L" : "S", + }, + icon: { + source: Icon.ComputerChip, + tintColor: UIColors.gitpod_gold, + }, + tooltip: `Editor: ${preferences?.preferredEditor}, Class: ${preferences?.preferredEditorClass} ` + }, + + ) if (branch.compData.commits) { accessories.unshift({ tag: { @@ -66,8 +91,9 @@ export default function BranchListItem({ branch, mainBranch, repository }: Branc { - open(`https://gitpod.io/#${branchURL}`); + OpenInGitpod(branchURL, "Branch", repository, branch.branchName) }} + shortcut={{ modifiers: ["cmd"], key: "g" }} /> + push()} shortcut={{ modifiers: ["cmd"], key: "w" }} /> } /> diff --git a/src/components/IssueListItem.tsx b/src/components/IssueListItem.tsx index e9de57c..43db255 100644 --- a/src/components/IssueListItem.tsx +++ b/src/components/IssueListItem.tsx @@ -1,7 +1,8 @@ -import { Action, ActionPanel, Icon, List, open } from "@raycast/api"; -import { MutatePromise } from "@raycast/utils"; +import { Action, ActionPanel, Icon, List, open, useNavigation } from "@raycast/api"; +import { MutatePromise, usePromise } from "@raycast/utils"; import { format } from "date-fns"; +import { UIColors } from "../../constants"; import { IssueFieldsFragment, SearchCreatedIssuesQuery, @@ -9,27 +10,47 @@ import { UserFieldsFragment, } from "../generated/graphql"; import { getIssueAuthor, getIssueStatus } from "../helpers/issue"; +import OpenInGitpod, { getPreferencesForContext } from "../helpers/openInGitpod"; +import ContextPreferences from "../preferences/context_preferences"; type IssueListItemProps = { issue: IssueFieldsFragment; viewer?: UserFieldsFragment; mutateList?: - | MutatePromise - | MutatePromise - | MutatePromise; + | MutatePromise + | MutatePromise + | MutatePromise; }; export default function IssueListItem({ issue }: IssueListItemProps) { + const { push } = useNavigation(); const updatedAt = new Date(issue.updatedAt); const author = getIssueAuthor(issue); const status = getIssueStatus(issue); + const { data: preferences, revalidate } = usePromise( + async () => { + const response = await getPreferencesForContext("Issue", issue.repository.nameWithOwner, issue.title); + return response; + }, + ); + const accessories: List.Item.Accessory[] = [ { date: updatedAt, tooltip: `Updated: ${format(updatedAt, "EEEE d MMMM yyyy 'at' HH:mm")}`, }, + { + text: { + value: preferences?.preferredEditorClass === "g1-large" ? "L" : "S", + }, + icon: { + source: Icon.ComputerChip, + tintColor: UIColors.gitpod_gold, + }, + tooltip: `Editor: ${preferences?.preferredEditor}, Class: ${preferences?.preferredEditorClass} ` + }, { icon: author.icon, tooltip: `Author: ${author.text}`, @@ -62,8 +83,9 @@ export default function IssueListItem({ issue }: IssueListItemProps) { { - open(`https://gitpod.io/#${issue.url}`); + OpenInGitpod(issue.url, "Issue", issue.repository.nameWithOwner, issue.title) }} + shortcut={{ modifiers: ["cmd"], key: "g" }} /> + push()} shortcut={{ modifiers: ["cmd"], key: "w" }} /> } /> ); } -// -// } -// /> -// diff --git a/src/components/PullRequestListItem.tsx b/src/components/PullRequestListItem.tsx index e87b4e3..e0a848c 100644 --- a/src/components/PullRequestListItem.tsx +++ b/src/components/PullRequestListItem.tsx @@ -1,9 +1,11 @@ -import { Action, ActionPanel, Icon, List, open } from "@raycast/api"; -// import { MutatePromise } from "@raycast/utils"; +import { Action, ActionPanel, Icon, List, open, useNavigation } from "@raycast/api"; +import { usePromise } from "@raycast/utils"; import { format } from "date-fns"; import { useMemo } from "react"; -import { MyPullRequestsQuery, PullRequestFieldsFragment, UserFieldsFragment } from "../generated/graphql"; +import { UIColors } from "../../constants"; +import { PullRequestFieldsFragment, UserFieldsFragment } from "../generated/graphql"; +import OpenInGitpod, { getPreferencesForContext } from "../helpers/openInGitpod"; import { getCheckStateAccessory, getNumberOfComments, @@ -11,29 +13,44 @@ import { getPullRequestStatus, getReviewDecision, } from "../helpers/pull-request"; - -// import PullRequestActions from "./PullRequestActions"; -// import PullRequestDetail from "./PullRequestDetail"; +import ContextPreferences from "../preferences/context_preferences"; type PullRequestListItemProps = { pullRequest: PullRequestFieldsFragment; viewer?: UserFieldsFragment; - // mutateList: MutatePromise | MutatePromise; }; -export default function PullRequestListItem({ pullRequest, viewer }: PullRequestListItemProps) { +export default function PullRequestListItem({ pullRequest }: PullRequestListItemProps) { const updatedAt = new Date(pullRequest.updatedAt); + const { push } = useNavigation(); const numberOfComments = useMemo(() => getNumberOfComments(pullRequest), []); const author = getPullRequestAuthor(pullRequest); const status = getPullRequestStatus(pullRequest); const reviewDecision = getReviewDecision(pullRequest.reviewDecision); + const { data: preferences, revalidate } = usePromise( + async () => { + const response = await getPreferencesForContext("Pull Request", pullRequest.repository.nameWithOwner, pullRequest.title); + return response; + }, + ); + const accessories: List.Item.Accessory[] = [ { date: updatedAt, tooltip: `Updated: ${format(updatedAt, "EEEE d MMMM yyyy 'at' HH:mm")}`, }, + { + text: { + value: preferences?.preferredEditorClass === "g1-large" ? "L" : "S", + }, + icon: { + source: Icon.ComputerChip, + tintColor: UIColors.gitpod_gold, + }, + tooltip: `Editor: ${preferences?.preferredEditor}, Class: ${preferences?.preferredEditorClass} ` + }, { icon: author.icon, tooltip: `Author: ${author.text}`, @@ -79,8 +96,9 @@ export default function PullRequestListItem({ pullRequest, viewer }: PullRequest { - open(`https://gitpod.io/#${pullRequest.permalink}`); + OpenInGitpod(pullRequest.permalink, "Pull Request", pullRequest.repository.nameWithOwner, pullRequest.title); }} + shortcut={{ modifiers: ["cmd"], key: "g" }} /> + push()} shortcut={{ modifiers: ["cmd"], key: "w" }} /> } /> ); } - -{ - /* - } - /> -; */ -} diff --git a/src/components/RepositoryListItem.tsx b/src/components/RepositoryListItem.tsx index 991478f..8fcaf03 100644 --- a/src/components/RepositoryListItem.tsx +++ b/src/components/RepositoryListItem.tsx @@ -1,10 +1,12 @@ -import { Color, List, ActionPanel, Action, showToast, Toast, open, useNavigation } from "@raycast/api"; -import { MutatePromise } from "@raycast/utils"; +import { Color, List, ActionPanel, Action, open, useNavigation, Icon } from "@raycast/api"; +import { MutatePromise, usePromise } from "@raycast/utils"; -import { GitpodIcons } from "../../constants"; +import { GitpodIcons, UIColors } from "../../constants"; import { ExtendedRepositoryFieldsFragment } from "../generated/graphql"; +import OpenInGitpod, { getPreferencesForContext } from "../helpers/openInGitpod"; import { getGitHubUser } from "../helpers/users"; import SearchContext from "../open_repo_context"; +import RepositoryPreference from "../preferences/repository_preferences"; type RepositoryListItemProps = { repository: ExtendedRepositoryFieldsFragment; @@ -18,22 +20,29 @@ export default function RepositoryListItem({ repository, isGitpodified, onVisit const owner = getGitHubUser(repository.owner); const numberOfStars = repository.stargazerCount; + const { data: preferences, revalidate } = usePromise( + async () => { + const response = await getPreferencesForContext("Repository", repository.nameWithOwner); + return response; + }, + ); + const accessories: List.Item.Accessory[] = [ + { + text: { + value: preferences?.preferredEditorClass === "g1-large" ? "L" : "S", + }, + icon: { + source: Icon.ComputerChip, + tintColor: UIColors.gitpod_gold, + }, + tooltip: `Editor: ${preferences?.preferredEditor}, Class: ${preferences?.preferredEditorClass} ` + }, { icon: isGitpodified ? GitpodIcons.gitpod_logo_primary : GitpodIcons.gitpod_logo_secondary, }, ]; - const showLaunchToast = async () => { - await showToast({ - title: "Launching your workspace", - style: Toast.Style.Success, - }); - setTimeout(() => { - open(`https://gitpod.io/#${repository.url}`); - }, 1500); - }; - accessories.unshift( { text: { @@ -65,11 +74,11 @@ export default function RepositoryListItem({ repository, isGitpodified, onVisit title={repository.name} {...(numberOfStars > 0 ? { - subtitle: { - value: `${numberOfStars}`, - tooltip: `Number of Stars: ${numberOfStars}`, - }, - } + subtitle: { + value: `${numberOfStars}`, + tooltip: `Number of Stars: ${numberOfStars}`, + }, + } : {})} accessories={accessories} actions={ @@ -81,7 +90,9 @@ export default function RepositoryListItem({ repository, isGitpodified, onVisit push(); }} /> - + open(repository.url)} /> + OpenInGitpod(repository.url, "Repository", repository.nameWithOwner)} shortcut={{ modifiers: ["cmd"], key: "g" }} /> + push()} shortcut={{ modifiers: ["cmd"], key: "w" }} /> } /> diff --git a/src/components/SearchRepositoryDropdown.tsx b/src/components/SearchRepositoryDropdown.tsx index c34fece..15d3185 100644 --- a/src/components/SearchRepositoryDropdown.tsx +++ b/src/components/SearchRepositoryDropdown.tsx @@ -8,13 +8,13 @@ export default function SearchRepositoryDropdown(props: { onFilterChange: (filte return ( + {viewer ? ( `org:${org?.login}`).join(" ")}`} /> ) : null} - diff --git a/src/helpers/branch.ts b/src/helpers/branch.ts new file mode 100644 index 0000000..0022180 --- /dev/null +++ b/src/helpers/branch.ts @@ -0,0 +1,41 @@ +import { LocalStorage } from "@raycast/api"; +import { useCachedState } from "@raycast/utils"; +import { useEffect } from "react"; + +import { BranchDetailsFragment, ExtendedRepositoryFieldsFragment } from "../generated/graphql"; + +const VISITED_BRANCH_KEY = "VISITED_BRANCHES"; +const VISITED_BRANCH_LENGTH = 10; + +// History was stored in `LocalStorage` before, after migration it's stored in `Cache` +async function loadVisitedBranches() { + const item = await LocalStorage.getItem(VISITED_BRANCH_KEY); + if (item) { + const parsed = JSON.parse(item).slice(0, VISITED_BRANCH_LENGTH); + return parsed as BranchDetailsFragment[]; + } else { + return []; + } +} +export function useBranchHistory() { + const [history, setHistory] = useCachedState("BranchHistory", []); + const [migratedHistory, setMigratedHistory] = useCachedState("migratedBranchHistory", false); + + useEffect(() => { + if (!migratedHistory) { + loadVisitedBranches().then((branches) => { + setHistory(branches); + setMigratedHistory(true); + }); + } + }, [migratedHistory]); + + function visitBranch(branch: BranchDetailsFragment, repository: ExtendedRepositoryFieldsFragment) { + const visitedBranch = [branch, ...(history?.filter((item) => item.branchName !== branch.branchName) ?? [])]; + LocalStorage.setItem(VISITED_BRANCH_KEY, JSON.stringify(visitedBranch)); + const nextBranch = visitedBranch.slice(0, VISITED_BRANCH_LENGTH); + setHistory(nextBranch); + } + + return { history, visitBranch }; +} diff --git a/src/helpers/openInGitpod.ts b/src/helpers/openInGitpod.ts new file mode 100644 index 0000000..9186b73 --- /dev/null +++ b/src/helpers/openInGitpod.ts @@ -0,0 +1,58 @@ +import { LocalStorage, open, showToast, Toast } from "@raycast/api"; +import { getPreferenceValues } from "@raycast/api"; + +interface Preferences { + preferredEditor: string; + useLatest: boolean; + preferredEditorClass: "g1-standard" | "g1-large"; +} + +export async function getPreferencesForContext(type: "Branch" | "Pull Request" | "Issue" | "Repository", repository: string, context?: string) { + let preferences = getPreferenceValues(); + if (type === "Branch" || type === "Pull Request" || type === "Issue") { + const item = await LocalStorage.getItem(`${repository}%${context}`) + const contextPref = item ? await JSON.parse(item) : null + if (contextPref && contextPref.preferredEditor && contextPref.preferredEditorClass) { + preferences = contextPref + } else { + const repoItem = await LocalStorage.getItem(`${repository}`); + const repoPref = repoItem ? await JSON.parse(repoItem) : null + if (repoPref && repoPref.preferredEditor && repoPref.preferredEditorClass) { + preferences = repoPref + } + } + } else if (type === "Repository") { + const item = await LocalStorage.getItem(`${repository}`); + const repoPref = item ? await JSON.parse(item) : null + if (repoPref && repoPref.preferredEditor && repoPref.preferredEditorClass) { + preferences = repoPref + } + } + return preferences; +} + +export default async function OpenInGitpod(contextUrl: string, type: "Branch" | "Pull Request" | "Issue" | "Repository", repository: string, context?: string) { + const preferences = await getPreferencesForContext(type, repository, context) + if (type === "Branch") { + //visit branch + } else if (type === "Pull Request") { + //vitit pr + } else if (type === "Issue") { + //visit issue + } + + try { + await showToast({ + title: "Launching your workspace", + style: Toast.Style.Success, + }); + setTimeout(() => { + open(`https://gitpod.io/?useLatest=${preferences.useLatest}&editor=${preferences.preferredEditor}${preferences.useLatest ? "-latest" : ""}&workspaceClass=${preferences.preferredEditorClass}#${contextUrl}`); + }, 1000); + } catch (error) { + await showToast({ + title: "Error launching workspace", + style: Toast.Style.Failure, + }); + } +} diff --git a/src/helpers/withGithubClient.tsx b/src/helpers/withGithubClient.tsx index 28f2b3e..15604dc 100644 --- a/src/helpers/withGithubClient.tsx +++ b/src/helpers/withGithubClient.tsx @@ -1,5 +1,4 @@ import { Detail, environment, MenuBarExtra } from "@raycast/api"; -import { getPreferenceValues } from "@raycast/api"; import { GraphQLClient } from "graphql-request"; import { Octokit } from "octokit"; import { useMemo, useState } from "react"; @@ -16,9 +15,8 @@ export function withGithubClient(component: JSX.Element) { // we use a `useMemo` instead of `useEffect` to avoid a render useMemo(() => { (async function () { - const { personalAccessToken } = getPreferenceValues(); - const token = personalAccessToken || (await authorize()); - const authorization = personalAccessToken ? `token ${token}` : `bearer ${token}`; + const token = await authorize(); + const authorization = `bearer ${token}`; github = getSdk(new GraphQLClient("https://api.github.com/graphql", { headers: { authorization } })); octokit = new Octokit({ auth: token }); diff --git a/src/preferences/context_preferences.tsx b/src/preferences/context_preferences.tsx new file mode 100644 index 0000000..d479e85 --- /dev/null +++ b/src/preferences/context_preferences.tsx @@ -0,0 +1,93 @@ +import { ActionPanel, Form, Action, LocalStorage, useNavigation, showToast, Toast, getPreferenceValues } from "@raycast/api"; +import { useEffect, useState } from "react"; + +type ContextPreferenceProps = { + repository: string; + type: "Branch" | "Pull Request" | "Issue"; + context: string + revalidate: () => void; +} + +interface Preferences { + preferredEditor: string; + useLatest: boolean; + preferredEditorClass: "g1-standard" | "g1-large"; +} + +async function getDefaultValue(repository: string, context: string) { + let defaultPrefValue: Preferences = getPreferenceValues(); + const item = await LocalStorage.getItem(`${repository}%${context}`) + const contextPref = item ? await JSON.parse(item) : null + if (contextPref && contextPref.preferredEditor && contextPref.preferredEditorClass) { + defaultPrefValue = contextPref + } else { + const repoItem = await LocalStorage.getItem(`${repository}`); + const repoPref = repoItem ? await JSON.parse(repoItem) : null + if (repoPref && repoPref.preferredEditor && repoPref.preferredEditorClass) { + defaultPrefValue = repoPref + } + } + + return defaultPrefValue +} + +export default function ContextPreferences({ repository, type, context, revalidate }: ContextPreferenceProps) { + const [defaultPrefValue, setDefaultPrefValue] = useState(null) + + useEffect(() => { + const getUsers = async () => { + const res = await getDefaultValue(repository, context); + setDefaultPrefValue(res); + }; + + getUsers(); + }, []); + + const { pop } = useNavigation(); + + return ( + defaultPrefValue && + (
+ { + try { + await LocalStorage.setItem(`${repository}%${context}`, JSON.stringify(values)); + await showToast({ + title: "Preferences saved successfully", + style: Toast.Style.Success, + }); + revalidate(); + pop(); + } + catch (error) { + await showToast({ + title: "Error saving preferences", + style: Toast.Style.Failure, + }); + } + }} /> + + } + > + + + + + + + + + + + + + + + + + + ) + ); +} diff --git a/src/preferences/repository_preferences.tsx b/src/preferences/repository_preferences.tsx new file mode 100644 index 0000000..8ed8250 --- /dev/null +++ b/src/preferences/repository_preferences.tsx @@ -0,0 +1,85 @@ +import { ActionPanel, Form, Action, LocalStorage, showHUD, useNavigation, showToast, Toast, getPreferenceValues } from "@raycast/api"; +import { useEffect, useState } from "react"; + +type RepositoryPreferenceProps = { + repository: string; + revalidate: ()=>void; +} + +interface Preferences { + preferredEditor: string; + useLatest: boolean; + preferredEditorClass: "g1-standard" | "g1-large"; +} + +async function getDefaultValue(repository: string) { + let defaultPrefValue: Preferences = getPreferenceValues(); + + const item = await LocalStorage.getItem(`${repository}`) + const contextPref = item ? await JSON.parse(item) : null + if (contextPref && contextPref.preferredEditor && contextPref.preferredEditorClass) { + defaultPrefValue = contextPref + } + return defaultPrefValue +} + +export default function RepositoryPreference({ repository ,revalidate}: RepositoryPreferenceProps) { + const [defaultPrefValue, setDefaultPrefValue] = useState(null) + + const { pop } = useNavigation(); + + useEffect(() => { + const loadDefaultValues = async () => { + const res = await getDefaultValue(repository); + setDefaultPrefValue(res); + }; + + loadDefaultValues(); + }, []); + + return ( + defaultPrefValue && ( +
+ { + try { + await LocalStorage.setItem(`${repository}`, JSON.stringify(values)); + await showToast({ + title: "Preferences saved successfully", + style: Toast.Style.Success, + }); + revalidate(); + pop(); + } + catch (error) { + await showToast({ + title: "Error saving preferences", + style: Toast.Style.Failure, + }); + } + }} /> + + } + > + + + + + + + + + + + + + + + + + + ) + ); +}