diff --git a/extensions/multi-force/CHANGELOG.md b/extensions/multi-force/CHANGELOG.md
index 84d4f1a57a279..5da2b34c01be2 100644
--- a/extensions/multi-force/CHANGELOG.md
+++ b/extensions/multi-force/CHANGELOG.md
@@ -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!
diff --git a/extensions/multi-force/metadata/multi-force-1-min.png b/extensions/multi-force/metadata/multi-force-1-min.png
index f08d8b61e7466..1a93a03cf550e 100644
Binary files a/extensions/multi-force/metadata/multi-force-1-min.png and b/extensions/multi-force/metadata/multi-force-1-min.png differ
diff --git a/extensions/multi-force/metadata/multi-force-2-min.png b/extensions/multi-force/metadata/multi-force-2-min.png
index 40f7619dc7105..a6a0addb2d763 100644
Binary files a/extensions/multi-force/metadata/multi-force-2-min.png and b/extensions/multi-force/metadata/multi-force-2-min.png differ
diff --git a/extensions/multi-force/metadata/multi-force-3-min.png b/extensions/multi-force/metadata/multi-force-3-min.png
index db2f09e695a75..53c439d14c328 100644
Binary files a/extensions/multi-force/metadata/multi-force-3-min.png and b/extensions/multi-force/metadata/multi-force-3-min.png differ
diff --git a/extensions/multi-force/metadata/multi-force-5-min.png b/extensions/multi-force/metadata/multi-force-5-min.png
index 201af0ebd6495..39a48258116cd 100644
Binary files a/extensions/multi-force/metadata/multi-force-5-min.png and b/extensions/multi-force/metadata/multi-force-5-min.png differ
diff --git a/extensions/multi-force/metadata/multi-force-7-min.png b/extensions/multi-force/metadata/multi-force-7-min.png
index 7429655394840..0cf7216cb81f6 100644
Binary files a/extensions/multi-force/metadata/multi-force-7-min.png and b/extensions/multi-force/metadata/multi-force-7-min.png differ
diff --git a/extensions/multi-force/src/components/MutiForce.tsx b/extensions/multi-force/src/components/MutiForce.tsx
index f1bab2d406039..7d441d5e38eb0 100644
--- a/extensions/multi-force/src/components/MutiForce.tsx
+++ b/extensions/multi-force/src/components/MutiForce.tsx
@@ -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();
@@ -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 ? (
) : (
+ {recentlyUsedOrgs.length > 0 && (
+
+ {recentlyUsedOrgs.map((org, index) => (
+
+ ))}
+
+ )}
{Array.from(orgs.keys())
.sort()
.map((key, keyIndex) =>
orgs.get(key) && orgs.get(key)!.length > 0 ? (
{orgs.get(key)!.map((org, index) => (
-
+
))}
) : null,
diff --git a/extensions/multi-force/src/components/listItems/OrgListItem.tsx b/extensions/multi-force/src/components/listItems/OrgListItem.tsx
index 08f34f681312b..0fc7e4d288e54 100644
--- a/extensions/multi-force/src/components/listItems/OrgListItem.tsx
+++ b/extensions/multi-force/src/components/listItems/OrgListItem.tsx
@@ -9,6 +9,7 @@ import {
Toast,
confirmAlert,
Alert,
+ Color,
} from "@raycast/api";
import { deleteOrg, openOrg } from "../../utils";
import { useMultiForceContext, useLoadingContext } from "../providers/OrgListProvider";
@@ -16,6 +17,35 @@ 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();
@@ -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();
@@ -67,11 +101,27 @@ export function OrgListItem(props: { index: number; org: DeveloperOrg }) {
}
};
+ const expirationStatus = getExpirationStatus(org);
+
return (
{
- 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();
@@ -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);
@@ -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,
@@ -139,6 +134,21 @@ export function DeveloperOrgDetails(props: { org: DeveloperOrg; dispatch: Dispat
+ {org.expirationDate && (
+ {
+ 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",
+ });
+ })()}
+ />
+ )}
{
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 {
- 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) {