Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] Added workspace class and editor preferences at Repo, Context and Global levels with cascade #30

Merged
merged 7 commits into from
Mar 16, 2023
86 changes: 75 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
9 changes: 7 additions & 2 deletions src/components/BranchListItem.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Action, ActionPanel, Color, List, open } from "@raycast/api";
import { Action, ActionPanel, Color, List, open, useNavigation } from "@raycast/api";

import { branchStatus, GitpodIcons } from "../../constants";
import { BranchDetailsFragment, UserFieldsFragment } from "../generated/graphql";
import OpenInGitpod from "../helpers/openInGitpod";
import ContextPreferences from "../preferences/context_preferences";

type BranchItemProps = {
branch: BranchDetailsFragment;
@@ -14,6 +16,7 @@ export default function BranchListItem({ branch, mainBranch, repository }: Branc
const accessories: List.Item.Accessory[] = [];
const branchURL = "https://github.com/" + repository + "/tree/" + branch.branchName;

const { push } = useNavigation();
if (branch.compData) {
if (branch.compData.status) {
switch (branch.compData.status.toString()) {
@@ -66,15 +69,17 @@ export default function BranchListItem({ branch, mainBranch, repository }: Branc
<Action
title="Open Branch in Gitpod"
onAction={() => {
open(`https://gitpod.io/#${branchURL}`);
OpenInGitpod(branchURL,"Branch",repository,branch.branchName)
}}
shortcut={{ modifiers: ["cmd"], key: "g" }}
/>
<Action
title="Open Branch in GitHub"
onAction={() => {
open(branchURL);
}}
/>
<Action title="Configure Workspace" onAction={()=> push(<ContextPreferences type="Branch" repository={repository} context={branch.branchName}/>)} shortcut={{ modifiers: ["cmd"], key: "w" }}/>
</ActionPanel>
}
/>
16 changes: 7 additions & 9 deletions src/components/IssueListItem.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Action, ActionPanel, Icon, List, open } from "@raycast/api";
import { Action, ActionPanel, Icon, List, open, useNavigation } from "@raycast/api";
import { MutatePromise } from "@raycast/utils";
import { format } from "date-fns";

@@ -9,6 +9,8 @@ import {
UserFieldsFragment,
} from "../generated/graphql";
import { getIssueAuthor, getIssueStatus } from "../helpers/issue";
import OpenInGitpod from "../helpers/openInGitpod";
import ContextPreferences from "../preferences/context_preferences";

type IssueListItemProps = {
issue: IssueFieldsFragment;
@@ -20,6 +22,7 @@ type IssueListItemProps = {
};

export default function IssueListItem({ issue }: IssueListItemProps) {
const { push } = useNavigation();
const updatedAt = new Date(issue.updatedAt);

const author = getIssueAuthor(issue);
@@ -62,25 +65,20 @@ export default function IssueListItem({ issue }: IssueListItemProps) {
<Action
title="Open Issue in Gitpod"
onAction={() => {
open(`https://gitpod.io/#${issue.url}`);
OpenInGitpod(issue.url,"Issue",issue.repository.nameWithOwner, issue.title)
}}
shortcut={{ modifiers: ["cmd"], key: "g" }}
/>
<Action
title="View Issue in GitHub"
onAction={() => {
open(issue.url);
}}
/>
<Action title="Configure Workspace" onAction={()=> push(<ContextPreferences repository={issue.repository.nameWithOwner} type="Issue" context={issue.title} />)} shortcut={{ modifiers: ["cmd"], key: "w" }}/>
</ActionPanel>
}
/>
);
}

// <IssueActions issue={issue} mutateList={mutateList} viewer={viewer}>
// <Action.Push
// title="Show Details"
// icon={Icon.Sidebar}
// target={<IssueDetail initialIssue={issue} viewer={viewer} mutateList={mutateList} />}
// />
// </IssueActions>
29 changes: 10 additions & 19 deletions src/components/PullRequestListItem.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,27 @@
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 { format } from "date-fns";
import { pull } from "lodash";
import { useMemo } from "react";

import { MyPullRequestsQuery, PullRequestFieldsFragment, UserFieldsFragment } from "../generated/graphql";
import { PullRequestFieldsFragment, UserFieldsFragment } from "../generated/graphql";
import OpenInGitpod from "../helpers/openInGitpod";
import {
getCheckStateAccessory,
getNumberOfComments,
getPullRequestAuthor,
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<MyPullRequestsQuery | undefined> | MutatePromise<PullRequestFieldsFragment[] | undefined>;
};

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);
@@ -79,27 +78,19 @@ export default function PullRequestListItem({ pullRequest, viewer }: PullRequest
<Action
title="Open PR in Gitpod"
onAction={() => {
open(`https://gitpod.io/#${pullRequest.permalink}`);
OpenInGitpod(pullRequest.permalink, "Pull Request", pullRequest.repository.nameWithOwner,pullRequest.title);
}}
shortcut={{ modifiers: ["cmd"], key: "g" }}
/>
<Action
title="View PR in GitHub"
onAction={() => {
open(pullRequest.permalink);
}}
/>
<Action title="Configure Workspace" onAction={()=> push(<ContextPreferences type="Pull Request" repository={pullRequest.repository.nameWithOwner} context={pullRequest.title}/>)} shortcut={{ modifiers: ["cmd"], key: "w" }}/>
</ActionPanel>
}
/>
);
}

{
/* <PullRequestActions pullRequest={pullRequest} viewer={viewer} mutateList={mutateList}>
<Action.Push
title="Show Details"
icon={Icon.Sidebar}
target={<PullRequestDetail initialPullRequest={pullRequest} viewer={viewer} mutateList={mutateList} />}
/>
</PullRequestActions>; */
}
6 changes: 5 additions & 1 deletion src/components/RepositoryListItem.tsx
Original file line number Diff line number Diff line change
@@ -3,8 +3,10 @@ import { MutatePromise } from "@raycast/utils";

import { GitpodIcons } from "../../constants";
import { ExtendedRepositoryFieldsFragment } from "../generated/graphql";
import OpenInGitpod from "../helpers/openInGitpod";
import { getGitHubUser } from "../helpers/users";
import SearchContext from "../open_repo_context";
import RepositoryPreference from "../preferences/repository_preferences";

type RepositoryListItemProps = {
repository: ExtendedRepositoryFieldsFragment;
@@ -81,7 +83,9 @@ export default function RepositoryListItem({ repository, isGitpodified, onVisit
push(<SearchContext repository={repository} />);
}}
/>
<Action title="Trigger Workspace" onAction={showLaunchToast} />
<Action title="Open Repo in GitHub" onAction={()=>open(repository.url)} />
<Action title="Trigger Workspace" onAction={()=>OpenInGitpod(repository.url,"Repository",repository.nameWithOwner)} shortcut={{ modifiers: ["cmd"], key: "g" }}/>
<Action title="Configure Workspace" onAction={()=> push(<RepositoryPreference repository={repository.nameWithOwner} />)} shortcut={{ modifiers: ["cmd"], key: "w" }}/>
</ActionPanel>
}
/>
2 changes: 1 addition & 1 deletion src/components/SearchRepositoryDropdown.tsx
Original file line number Diff line number Diff line change
@@ -8,13 +8,13 @@ export default function SearchRepositoryDropdown(props: { onFilterChange: (filte
return (
<List.Dropdown tooltip="Filter Repositories" onChange={props.onFilterChange} storeValue>
<List.Dropdown.Section>
<List.Dropdown.Item title={"All Repositories"} value={""} />
{viewer ? (
<List.Dropdown.Item
title={"My Repositories"}
value={`user:${viewer.login} ${viewer.organizations?.nodes?.map((org) => `org:${org?.login}`).join(" ")}`}
/>
) : null}
<List.Dropdown.Item title={"All Repositories"} value={""} />
</List.Dropdown.Section>

<List.Dropdown.Section>
41 changes: 41 additions & 0 deletions src/helpers/branch.ts
Original file line number Diff line number Diff line change
@@ -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<string>(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<BranchDetailsFragment[]>("BranchHistory", []);
const [migratedHistory, setMigratedHistory] = useCachedState<boolean>("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 };
}
55 changes: 55 additions & 0 deletions src/helpers/openInGitpod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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 default async function OpenInGitpod(contextUrl: string, type: "Branch" | "Pull Request" | "Issue" | "Repository", repository: string, context?: string) {
let preferences = getPreferenceValues<Preferences>();

if (type === "Branch" || type === "Pull Request" || type === "Issue") {
const item = await LocalStorage.getItem<string>(`${repository}%${context}`)
const contextPref = item ? await JSON.parse(item) : null
if (contextPref && contextPref.preferredEditor && contextPref.preferredEditorClass) {
preferences = contextPref
} else {
const repoItem = await LocalStorage.getItem<string>(`${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<string>(`${repository}`);
const repoPref = item ? await JSON.parse(item) : null
if (repoPref && repoPref.preferredEditor && repoPref.preferredEditorClass) {
preferences = repoPref
}
}

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}&workspaceClass=${preferences.preferredEditorClass}#${contextUrl}`);
}, 1000);
} catch (error) {
await showToast({
title: "Error launching workspace",
style: Toast.Style.Failure,
});
}
}
6 changes: 2 additions & 4 deletions src/helpers/withGithubClient.tsx
Original file line number Diff line number Diff line change
@@ -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 });
91 changes: 91 additions & 0 deletions src/preferences/context_preferences.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
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
}

interface Preferences {
preferredEditor: string;
useLatest: boolean;
preferredEditorClass: "g1-standard" | "g1-large";
}

async function getDefaultValue(repository: string, context: string) {
let defaultPrefValue: Preferences = getPreferenceValues<Preferences>();
const item = await LocalStorage.getItem<string>(`${repository}%${context}`)
const contextPref = item ? await JSON.parse(item) : null
if (contextPref && contextPref.preferredEditor && contextPref.preferredEditorClass) {
defaultPrefValue = contextPref
} else {
const repoItem = await LocalStorage.getItem<string>(`${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 }: ContextPreferenceProps) {
const [defaultPrefValue, setDefaultPrefValue] = useState<Preferences | null>(null)

useEffect(() => {
const getUsers = async () => {
const res = await getDefaultValue(repository, context);
setDefaultPrefValue(res);
};

getUsers();
}, []);

const { pop } = useNavigation();

return (
defaultPrefValue &&
(<Form
navigationTitle={`${type} - ${context}`}
actions={
<ActionPanel>
<Action.SubmitForm title="Set Context Preferences" onSubmit={async (values: Preferences) => {
try {
await LocalStorage.setItem(`${repository}%${context}`, JSON.stringify(values));
await showToast({
title: "Preferences saved successfully",
style: Toast.Style.Success,
});
pop();
}
catch (error) {
await showToast({
title: "Error saving preferences",
style: Toast.Style.Failure,
});
}
}} />
</ActionPanel>
}
>
<Form.Dropdown id="preferredEditor" title="Preferred Editor" defaultValue={defaultPrefValue.preferredEditor} info={`Pick your favorite Editor for ${repository}'s "${context}" ${type}`}>
<Form.Dropdown.Item value="code" title="VS Code Browser" />
<Form.Dropdown.Item value="code-desktop" title="VS Code Desktop" />
<Form.Dropdown.Item value="intellij" title="IntelliJ" />
<Form.Dropdown.Item value="goland" title="GoLand" />
<Form.Dropdown.Item value="phpstorm" title="PhpStorm" />
<Form.Dropdown.Item value="pycharm" title="PyCharm" />
<Form.Dropdown.Item value="rubymine" title="RubyMine" />
<Form.Dropdown.Item value="webstorm" title="WebStorm" />
<Form.Dropdown.Item value="rider" title="Rider" />
<Form.Dropdown.Item value="clion" title="CLion" />
</Form.Dropdown>
<Form.Checkbox id="useLatest" info="Use the latest version for each editor. Insiders for VS Code, EAP for JetBrains IDEs." label="Latest Release (Unstable)" defaultValue={defaultPrefValue.useLatest} />
<Form.Dropdown id="preferredEditorClass" title="Workspace Class" info="Up to 4 cores, 8GB RAM, 30GB storage in Standard & Up to 8 cores, 16GB RAM, 50GB storage in Large" defaultValue={defaultPrefValue.preferredEditorClass}>
<Form.Dropdown.Item value="g1-standard" title="Standard" />
<Form.Dropdown.Item value="g1-large" title="Large" />
</Form.Dropdown>
</Form>)
);
}
83 changes: 83 additions & 0 deletions src/preferences/repository_preferences.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { ActionPanel, Form, Action, LocalStorage, showHUD, useNavigation, showToast, Toast, getPreferenceValues } from "@raycast/api";
import { useEffect, useState } from "react";

type RepositoryPreferenceProps = {
repository: string;
}

interface Preferences {
preferredEditor: string;
useLatest: boolean;
preferredEditorClass: "g1-standard" | "g1-large";
}

async function getDefaultValue(repository: string) {
let defaultPrefValue: Preferences = getPreferenceValues<Preferences>();

const item = await LocalStorage.getItem<string>(`${repository}`)
const contextPref = item ? await JSON.parse(item) : null
if (contextPref && contextPref.preferredEditor && contextPref.preferredEditorClass) {
defaultPrefValue = contextPref
}
return defaultPrefValue
}

export default function RepositoryPreference({ repository }: RepositoryPreferenceProps) {
const [defaultPrefValue, setDefaultPrefValue] = useState<Preferences | null>(null)

const { pop } = useNavigation();

useEffect(() => {
const loadDefaultValues = async () => {
const res = await getDefaultValue(repository);
setDefaultPrefValue(res);
};

loadDefaultValues();
}, []);

return (
defaultPrefValue && (
<Form
navigationTitle={`${repository}`}
actions={
<ActionPanel>
<Action.SubmitForm title="Set Repository Preferences" onSubmit={async (values: Preferences) => {
try {
await LocalStorage.setItem(`${repository}`, JSON.stringify(values));
await showToast({
title: "Preferences saved successfully",
style: Toast.Style.Success,
});
pop();
}
catch (error) {
await showToast({
title: "Error saving preferences",
style: Toast.Style.Failure,
});
}
}} />
</ActionPanel>
}
>
<Form.Dropdown id="preferredEditor" title="Preferred Editor" defaultValue={defaultPrefValue.preferredEditor} info={`Pick your favorite Editor for ${repository}`}>
<Form.Dropdown.Item value="code" title="VS Code Browser" />
<Form.Dropdown.Item value="code-desktop" title="VS Code Desktop" />
<Form.Dropdown.Item value="intellij" title="IntelliJ" />
<Form.Dropdown.Item value="goland" title="GoLand" />
<Form.Dropdown.Item value="phpstorm" title="PhpStorm" />
<Form.Dropdown.Item value="pycharm" title="PyCharm" />
<Form.Dropdown.Item value="rubymine" title="RubyMine" />
<Form.Dropdown.Item value="webstorm" title="WebStorm" />
<Form.Dropdown.Item value="rider" title="Rider" />
<Form.Dropdown.Item value="clion" title="CLion" />
</Form.Dropdown>
<Form.Checkbox id="useLatest" info="Use the latest version for each editor. Insiders for VS Code, EAP for JetBrains IDEs." label="Latest Release (Unstable)" defaultValue={defaultPrefValue.useLatest} />
<Form.Dropdown id="preferredEditorClass" title="Workspace Class" info="Up to 4 cores, 8GB RAM, 30GB storage in Standard & Up to 8 cores, 16GB RAM, 50GB storage in Large" defaultValue={defaultPrefValue.preferredEditorClass}>
<Form.Dropdown.Item value="g1-standard" title="Standard" />
<Form.Dropdown.Item value="g1-large" title="Large" />
</Form.Dropdown>
</Form>)
);
}