Skip to content

Commit

Permalink
Update multi-force extension
Browse files Browse the repository at this point in the history
- resolve merge conflicts
- Merge branch \'contributions/merge-1741021922315\'
- Pull contributions
- Merge pull request raycast#1 from Decoder22/most-recent-section
- fix linting issues
- fix date misalignment issue
- fix days that show warning
- Implement scratch org expiration warnings
- implement the most recent section
- Cleanup unused images
- Clean Commit
- Remove additional images
- Merge branch \'contributions/merge-1723173902265564000\'
- Pull contributions
- Merge branch \'contributions/merge-1723173681370051000\'
- Pull contributions
- Update snapshots
- Update Changelog and readme
- Update readme
- Implement Redirect On Open
- Resolve douple authentication error
- fixing linting issues
- Update metadata images
- Update readme with changes
- run lint
- Add icons and form handling
- Implement Feedback and Refactoring 1. Updated Keyboard Shortcuts to match feedback 2. Added action icons. 3. Implemented ContextAPI and Reducers 4. Refactored code to make it easier to understand
- Clean up Org Deletion code to match Raycast Standards
- Delete unnecessary video
- Add changelog and demo video
- Fix refresh icon
- Better null checking
- Fix linting issues
- handle missing alias when reading SF auths
- Add the directBridge2 flag in OAuth
- Update installation instructions
- Update Readme with beta installation instructions
- Add visibility to username on the page
- Clean up screenshots
- Fix Authenticate to Org bug
- Fix build issues
- Fix linting
- Add section grouping
- Fix lint
- Add empty list page
- Update screenshots to match format
- Update readme and name
- Update the README
- Fix linting issues
- Add icon and logout command
- Finalize base commands
- Open Org Works
- initial commit
  • Loading branch information
Decoder22 committed Mar 3, 2025
1 parent 945bd99 commit 9e050c5
Show file tree
Hide file tree
Showing 12 changed files with 107 additions and 13 deletions.
3 changes: 3 additions & 0 deletions extensions/multi-force/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# MultiForce Changelog

## 1.2 - 2025-02-28
Version 1.2 introduces a recently used section to the org list. This section shows the 3 orgs that you most recently created or opened. This version also adds a warning indicator to scratch orgs that are set to expire in the next 7 days.

## 1.1 - 2024-08-06
Version 1.1 introduces the ability to choose where you want your login to take you. When creating an org, you now have the ability to choose if you want to automatically open to the Lightning Home or Setup Home in your org. You can also provide custom paths to open!

Expand Down
Binary file modified extensions/multi-force/metadata/multi-force-1-min.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified extensions/multi-force/metadata/multi-force-2-min.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified extensions/multi-force/metadata/multi-force-3-min.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified extensions/multi-force/metadata/multi-force-5-min.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified extensions/multi-force/metadata/multi-force-7-min.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 21 additions & 2 deletions extensions/multi-force/src/components/MutiForce.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import { useEffect } from "react";
import { useEffect, useMemo } from "react";
import { List, showToast } from "@raycast/api";
import { EmptyOrgList } from "./pages";
import { OrgListReducerType, DeveloperOrg } from "../types";
import { useLoadingContext, useMultiForceContext } from "./providers/OrgListProvider";
import { OrgListItem } from "./listItems/OrgListItem";
import { combineOrgList, getOrgList, loadOrgs, orgListsAreDifferent } from "../utils";
import { RECENTLY_USED_SECTION } from "../constants";

// Helper function to get recently used orgs
const getRecentlyUsedOrgs = (orgs: DeveloperOrg[]): DeveloperOrg[] => {
return orgs
.filter((org) => org.lastViewedAt && org.lastViewedAt > 0)
.sort((a, b) => (b.lastViewedAt || 0) - (a.lastViewedAt || 0))
.slice(0, 3); // Show last 3 used orgs
};

export default function MultiForce() {
const { orgs, dispatch } = useMultiForceContext();
Expand Down Expand Up @@ -57,17 +66,27 @@ export default function MultiForce() {
checkStorage();
}, []);

const allOrgs = useMemo(() => Array.from(orgs.values()).flat(), [orgs]);
const recentlyUsedOrgs = useMemo(() => getRecentlyUsedOrgs(allOrgs), [allOrgs]);

return Array.from(orgs.keys()).length === 0 && !isLoading ? (
<EmptyOrgList />
) : (
<List isLoading={isLoading}>
{recentlyUsedOrgs.length > 0 && (
<List.Section title={RECENTLY_USED_SECTION}>
{recentlyUsedOrgs.map((org, index) => (
<OrgListItem key={`recent-${index}`} index={index} org={org} />
))}
</List.Section>
)}
{Array.from(orgs.keys())
.sort()
.map((key, keyIndex) =>
orgs.get(key) && orgs.get(key)!.length > 0 ? (
<List.Section title={key} key={keyIndex}>
{orgs.get(key)!.map((org, index) => (
<OrgListItem key={index} index={index} org={org}></OrgListItem>
<OrgListItem key={index} index={index} org={org} />
))}
</List.Section>
) : null,
Expand Down
50 changes: 50 additions & 0 deletions extensions/multi-force/src/components/listItems/OrgListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,43 @@ import {
Toast,
confirmAlert,
Alert,
Color,
} from "@raycast/api";
import { deleteOrg, openOrg } from "../../utils";
import { useMultiForceContext, useLoadingContext } from "../providers/OrgListProvider";
import { OrgListReducerType, DeveloperOrg } from "../../types";
import { AuthenticateNewOrg, DeveloperOrgDetails } from "../pages";
import { HOME_PATH, SETUP_PATH } from "../../constants";

// Helper function to get expiration status
const getExpirationStatus = (org: DeveloperOrg): { icon?: Icon; tooltip?: string; tintColor?: Color } => {
if (!org.expirationDate) return {};

// Parse the date and set it to midnight in local timezone
const [year, month, day] = org.expirationDate.split("-").map(Number);
const expirationDate = new Date(year, month - 1, day); // month is 0-based in JS
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); // midnight today

const daysUntilExpiration = Math.ceil((expirationDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));

if (daysUntilExpiration <= 0) {
return {
icon: Icon.ExclamationMark,
tooltip: "Scratch org has expired",
tintColor: Color.Red,
};
}
if (daysUntilExpiration <= 7) {
return {
icon: Icon.Warning,
tooltip: `Scratch org expires in ${daysUntilExpiration} day${daysUntilExpiration === 1 ? "" : "s"}`,
tintColor: Color.Yellow,
};
}
return {};
};

export function OrgListItem(props: { index: number; org: DeveloperOrg }) {
const { index, org } = props;
const { orgs, dispatch } = useMultiForceContext();
Expand All @@ -30,6 +60,10 @@ export function OrgListItem(props: { index: number; org: DeveloperOrg }) {
});
try {
await openOrg(orgAlias, url);
dispatch({
type: OrgListReducerType.UPDATE_ORG,
updatedOrg: { ...org, lastViewedAt: Date.now() },
});
setIsLoading(false);
toast.hide();
popToRoot();
Expand Down Expand Up @@ -67,11 +101,27 @@ export function OrgListItem(props: { index: number; org: DeveloperOrg }) {
}
};

const expirationStatus = getExpirationStatus(org);

return (
<List.Item
key={index}
icon={{ source: "Salesforce.com_logo.svg.png", tintColor: org.color ?? "#0000FF" }}
title={org.label ? `${org.label} (${org.alias})` : org.alias}
accessories={
expirationStatus.icon
? [
{
icon: expirationStatus.icon,
tooltip: expirationStatus.tooltip,
tag: {
value: expirationStatus.tooltip || "",
color: expirationStatus.tintColor,
},
},
]
: undefined
}
actions={
<ActionPanel>
<Action
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,11 @@ export function DeveloperOrgDetails(props: { org: DeveloperOrg; dispatch: Dispat
};

const setPathValue = (path: string) => {
console.log("Set path value: " + path);
setPath(path);
setValue("openToPath", path);
};

useEffect(() => {
console.log("Use effect");
console.log(org);
async function getSectionList() {
const storedOrgs = await loadOrgs();
const sects = new Set<string>();
Expand All @@ -71,7 +68,6 @@ export function DeveloperOrgDetails(props: { org: DeveloperOrg; dispatch: Dispat
: org.openToPath
? CUSTOM_KEY
: HOME_PATH;
console.log(`Opening to path: ${pathToOpen}`);
setPathValue(pathToOpen);
}
setValue("color", org.color ?? DEFAULT_COLOR);
Expand Down Expand Up @@ -99,7 +95,6 @@ export function DeveloperOrgDetails(props: { org: DeveloperOrg; dispatch: Dispat
if (values.customPath) {
updatedOrg.openToPath = values.customPath;
}
console.log(updatedOrg);
dispatch({
type: OrgListReducerType.UPDATE_ORG,
updatedOrg: updatedOrg,
Expand Down Expand Up @@ -139,6 +134,21 @@ export function DeveloperOrgDetails(props: { org: DeveloperOrg; dispatch: Dispat
<Form.Description title="Org URL" text={org.instanceUrl} />
<Form.Description title="Username" text={org.username} />
<Form.Description title="Org Alias" text={org.alias} />
{org.expirationDate && (
<Form.Description
title="Expiration Date"
text={(() => {
const [year, month, day] = org.expirationDate.split("-").map(Number);
const date = new Date(year, month - 1, day); // month is 0-based in JS
return date.toLocaleDateString(undefined, {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
});
})()}
/>
)}
<Form.TextField
title={ORG_LABEL_LABEL}
{...itemProps.label}
Expand Down
1 change: 1 addition & 0 deletions extensions/multi-force/src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,4 @@ export const SECTION_DESCRIPTION =
export const NEW_SECTION_NAME_LABEL = "New Section Name";
export const NEW_SECTION_DESCRIPTION =
"Enter a new name for grouping your orgs into sections. When you save an org with a new section name, the option will get added to the Section dropdown above.";
export const RECENTLY_USED_SECTION = "Recently Used";
2 changes: 2 additions & 0 deletions extensions/multi-force/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,6 @@ export interface DeveloperOrg {
instanceUrl: string;
section?: string;
openToPath?: string;
lastViewedAt?: number;
expirationDate?: string;
}
21 changes: 15 additions & 6 deletions extensions/multi-force/src/utils/salesforceUtility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
SfdcUrl,
WebOAuthServer,
SfProject,
OrgAuthorization,
} from "@salesforce/core";
import isWsl from "is-wsl";
import { execSync } from "node:child_process";
Expand All @@ -22,23 +23,31 @@ import { HOME_PATH } from "../constants";
export async function getOrgList(): Promise<DeveloperOrg[]> {
process.env["SF_DISABLE_LOG_FILE"] = "true";
const authInfos = await AuthInfo.listAllAuthorizations();
const orgs: DeveloperOrg[] = authInfos.map((authInfo): DeveloperOrg => {
const { username } = authInfo;

// Get detailed org info for each authorization
const orgsPromises = authInfos.map(async (orgAuthorization: OrgAuthorization) => {
const { username } = orgAuthorization;

const authInfo = await AuthInfo.create({ username });
// Get the fields directly from AuthInfo
const fields = authInfo.getFields(true); // Pass true to get all fields

return {
alias: authInfo.aliases && authInfo.aliases.length > 0 ? authInfo.aliases[0] : username,
alias: orgAuthorization.aliases && orgAuthorization.aliases.length > 0 ? orgAuthorization.aliases[0] : username,
username,
instanceUrl: authInfo.instanceUrl ?? "",
instanceUrl: orgAuthorization.instanceUrl ?? "",
expirationDate: fields.expirationDate,
};
});

const orgs = await Promise.all(orgsPromises);
return orgs;
}

async function executeLoginFlow(oauthConfig: OAuth2Config): Promise<AuthInfo> {
console.log(oauthConfig);
const oauthServer = await WebOAuthServer.create({ oauthConfig });
try {
await oauthServer.start();
console.log(oauthServer.getAuthorizationUrl());
await open(oauthServer.getAuthorizationUrl());
return oauthServer.authorizeAndSave();
} catch (err) {
Expand Down

0 comments on commit 9e050c5

Please sign in to comment.