Prettier 2: Electric Boogaloo (#6113)

# Description of Changes
When I added Prettier formatting in #6052, my aim was to use just the
default settings in Prettier. Turns out, Prettier looks _really hard_
for any config files if it's not explicitly given one, which means that
if a developer has some sort of Prettier config file lying around on
their system, Prettier might find it and use it. Also, Prettier changes
its defaults based on stuff in `.editorconfig` without any good way of
disabling that behaviour explicitly in its config file.

To solve both of these issues, I've introduced a `.prettierrc` file
which sets Prettier's defaults explicitly, and then reformatted all our
code _again_ in Prettier's actual default settings. This should achieve
the aim of #6052 and remove the possibility for it breaking on different
dev computers.
This commit is contained in:
James Brunton
2026-04-17 09:50:16 +00:00
committed by GitHub
parent de8c483054
commit 8ab060a4be
1207 changed files with 52646 additions and 15352 deletions
@@ -1,4 +1,10 @@
import React, { createContext, useContext, useState, ReactNode, useCallback } from "react";
import React, {
createContext,
useContext,
useState,
ReactNode,
useCallback,
} from "react";
import { SaaSStripeCheckout } from "@app/components/shared/billing/SaaSStripeCheckout";
import { useSaaSBilling } from "@app/contexts/SaasBillingContext";
@@ -9,7 +15,9 @@ interface SaaSCheckoutContextType {
closeCheckout: () => void;
}
const SaaSCheckoutContext = createContext<SaaSCheckoutContextType | undefined>(undefined);
const SaaSCheckoutContext = createContext<SaaSCheckoutContextType | undefined>(
undefined,
);
export const useSaaSCheckout = () => {
const context = useContext(SaaSCheckoutContext);
@@ -23,7 +31,9 @@ interface SaaSCheckoutProviderProps {
children: ReactNode;
}
export const SaaSCheckoutProvider: React.FC<SaaSCheckoutProviderProps> = ({ children }) => {
export const SaaSCheckoutProvider: React.FC<SaaSCheckoutProviderProps> = ({
children,
}) => {
const [opened, setOpened] = useState(false);
const [selectedPlan, setSelectedPlan] = useState<string | null>(null);
@@ -48,7 +58,10 @@ export const SaaSCheckoutProvider: React.FC<SaaSCheckoutProviderProps> = ({ chil
try {
await refreshBilling();
} catch (error) {
console.error("[SaaSCheckoutContext] Failed to refresh billing after checkout:", error);
console.error(
"[SaaSCheckoutContext] Failed to refresh billing after checkout:",
error,
);
}
}, [refreshBilling]);
@@ -62,7 +75,12 @@ export const SaaSCheckoutProvider: React.FC<SaaSCheckoutProviderProps> = ({ chil
}}
>
{children}
<SaaSStripeCheckout opened={opened} onClose={closeCheckout} planId={selectedPlan} onSuccess={handleCheckoutSuccess} />
<SaaSStripeCheckout
opened={opened}
onClose={closeCheckout}
planId={selectedPlan}
onSuccess={handleCheckoutSuccess}
/>
</SaaSCheckoutContext.Provider>
);
};
@@ -1,4 +1,11 @@
import { createContext, useContext, useEffect, useState, ReactNode, useCallback } from "react";
import {
createContext,
useContext,
useEffect,
useState,
ReactNode,
useCallback,
} from "react";
import apiClient from "@app/services/apiClient";
import { authService } from "@app/services/authService";
import { connectionModeService } from "@app/services/connectionModeService";
@@ -81,7 +88,9 @@ export function SaaSTeamProvider({ children }: { children: ReactNode }) {
const [teams, setTeams] = useState<Team[]>([]);
const [teamMembers, setTeamMembers] = useState<TeamMember[]>([]);
const [teamInvitations, setTeamInvitations] = useState<TeamInvitation[]>([]);
const [receivedInvitations, setReceivedInvitations] = useState<TeamInvitation[]>([]);
const [receivedInvitations, setReceivedInvitations] = useState<
TeamInvitation[]
>([]);
const [loading, setLoading] = useState(true);
const [isSaasMode, setIsSaasMode] = useState(false);
const [isAuthenticated, setIsAuthenticated] = useState(false);
@@ -98,7 +107,8 @@ export function SaaSTeamProvider({ children }: { children: ReactNode }) {
checkAccess();
// Subscribe to connection mode changes
const unsubscribe = connectionModeService.subscribeToModeChanges(checkAccess);
const unsubscribe =
connectionModeService.subscribeToModeChanges(checkAccess);
return unsubscribe;
}, []);
@@ -113,12 +123,16 @@ export function SaaSTeamProvider({ children }: { children: ReactNode }) {
const fetchMyTeams = useCallback(async () => {
// CRITICAL: Only fetch if in SaaS mode and authenticated
if (!isSaasMode || !isAuthenticated) {
console.log("[SaaSTeamContext] Skipping team fetch - not in SaaS mode or not authenticated");
console.log(
"[SaaSTeamContext] Skipping team fetch - not in SaaS mode or not authenticated",
);
return null;
}
try {
const response = await apiClient.get<Team[]>("/api/v1/team/my", { suppressErrorToast: true });
const response = await apiClient.get<Team[]>("/api/v1/team/my", {
suppressErrorToast: true,
});
setTeams(response.data);
const activeTeam = response.data[0];
@@ -140,12 +154,17 @@ export function SaaSTeamProvider({ children }: { children: ReactNode }) {
async (teamId: number) => {
// CRITICAL: Only fetch if in SaaS mode and authenticated
if (!isSaasMode || !isAuthenticated) {
console.log("[SaaSTeamContext] Skipping members fetch - not in SaaS mode or not authenticated");
console.log(
"[SaaSTeamContext] Skipping members fetch - not in SaaS mode or not authenticated",
);
return;
}
try {
const response = await apiClient.get<TeamMember[]>(`/api/v1/team/${teamId}/members`, { suppressErrorToast: true });
const response = await apiClient.get<TeamMember[]>(
`/api/v1/team/${teamId}/members`,
{ suppressErrorToast: true },
);
setTeamMembers(response.data);
} catch (error) {
console.error("[SaaSTeamContext] Failed to fetch team members:", error);
@@ -162,12 +181,18 @@ export function SaaSTeamProvider({ children }: { children: ReactNode }) {
}
try {
const response = await apiClient.get<TeamInvitation[]>(`/api/v1/team/${teamId}/invitations`, {
suppressErrorToast: true,
});
const response = await apiClient.get<TeamInvitation[]>(
`/api/v1/team/${teamId}/invitations`,
{
suppressErrorToast: true,
},
);
setTeamInvitations(response.data);
} catch (error) {
console.error("[SaaSTeamContext] Failed to fetch team invitations:", error);
console.error(
"[SaaSTeamContext] Failed to fetch team invitations:",
error,
);
}
},
[isSaasMode, isAuthenticated],
@@ -182,11 +207,20 @@ export function SaaSTeamProvider({ children }: { children: ReactNode }) {
console.log("[SaaSTeamContext] Fetching received team invitations");
try {
const response = await apiClient.get<TeamInvitation[]>("/api/v1/team/invitations/pending", { suppressErrorToast: true });
console.log("[SaaSTeamContext] Received invitations response:", response.data);
const response = await apiClient.get<TeamInvitation[]>(
"/api/v1/team/invitations/pending",
{ suppressErrorToast: true },
);
console.log(
"[SaaSTeamContext] Received invitations response:",
response.data,
);
setReceivedInvitations(response.data);
} catch (error) {
console.error("[SaaSTeamContext] Failed to fetch received invitations:", error);
console.error(
"[SaaSTeamContext] Failed to fetch received invitations:",
error,
);
}
}, [isSaasMode, isAuthenticated]);
@@ -206,7 +240,12 @@ export function SaaSTeamProvider({ children }: { children: ReactNode }) {
}, [isSaasMode, isAuthenticated, fetchMyTeams, fetchReceivedInvitations]);
useEffect(() => {
if (currentTeam && !currentTeam.isPersonal && isSaasMode && isAuthenticated) {
if (
currentTeam &&
!currentTeam.isPersonal &&
isSaasMode &&
isAuthenticated
) {
fetchTeamMembers(currentTeam.teamId);
// Only fetch invitations if user is team leader
if (currentTeam.isLeader) {
@@ -219,7 +258,13 @@ export function SaaSTeamProvider({ children }: { children: ReactNode }) {
setTeamInvitations([]);
}
setLoading(false);
}, [currentTeam, isSaasMode, isAuthenticated, fetchTeamMembers, fetchTeamInvitations]);
}, [
currentTeam,
isSaasMode,
isAuthenticated,
fetchTeamMembers,
fetchTeamInvitations,
]);
const inviteUser = async (email: string) => {
if (!currentTeam) throw new Error("No current team");
@@ -261,7 +306,9 @@ export function SaaSTeamProvider({ children }: { children: ReactNode }) {
if (!currentTeam) throw new Error("No current team");
if (!isSaasMode) throw new Error("Not in SaaS mode");
await apiClient.delete(`/api/v1/team/${currentTeam.teamId}/members/${memberId}`);
await apiClient.delete(
`/api/v1/team/${currentTeam.teamId}/members/${memberId}`,
);
await refreshTeams();
await fetchTeamMembers(currentTeam.teamId);
};
@@ -277,7 +324,9 @@ export function SaaSTeamProvider({ children }: { children: ReactNode }) {
const refreshTeams = useCallback(async () => {
if (!isSaasMode || !isAuthenticated) {
console.log("[SaaSTeamContext] Skipping refresh - not in SaaS mode or not authenticated");
console.log(
"[SaaSTeamContext] Skipping refresh - not in SaaS mode or not authenticated",
);
return;
}
@@ -290,7 +339,14 @@ export function SaaSTeamProvider({ children }: { children: ReactNode }) {
await fetchTeamInvitations(newCurrentTeam.teamId);
}
}
}, [isSaasMode, isAuthenticated, fetchMyTeams, fetchReceivedInvitations, fetchTeamMembers, fetchTeamInvitations]);
}, [
isSaasMode,
isAuthenticated,
fetchMyTeams,
fetchReceivedInvitations,
fetchTeamMembers,
fetchTeamInvitations,
]);
const isTeamLeader = currentTeam?.isLeader ?? false;
const isPersonalTeam = currentTeam?.isPersonal ?? true;
@@ -1,5 +1,18 @@
import { createContext, useContext, useEffect, useState, ReactNode, useCallback, useRef, useMemo } from "react";
import { saasBillingService, BillingStatus, PlanPrice } from "@app/services/saasBillingService";
import {
createContext,
useContext,
useEffect,
useState,
ReactNode,
useCallback,
useRef,
useMemo,
} from "react";
import {
saasBillingService,
BillingStatus,
PlanPrice,
} from "@app/services/saasBillingService";
import { authService } from "@app/services/authService";
import { connectionModeService } from "@app/services/connectionModeService";
import { useSaaSTeam } from "@app/contexts/SaaSTeamContext";
@@ -80,7 +93,9 @@ const SaasBillingContext = createContext<SaasBillingContextType>({
});
export function SaasBillingProvider({ children }: { children: ReactNode }) {
const [billingStatus, setBillingStatus] = useState<BillingStatus | null>(null);
const [billingStatus, setBillingStatus] = useState<BillingStatus | null>(
null,
);
const [plans, setPlans] = useState<Map<string, PlanPrice>>(new Map());
const [plansLoading, setPlansLoading] = useState(false);
const [plansError, setPlansError] = useState<string | null>(null);
@@ -89,24 +104,35 @@ export function SaasBillingProvider({ children }: { children: ReactNode }) {
// lastFetchTimeRef is the source of truth for cache logic (always current, no stale closure).
// lastFetchTimeValue mirrors it as state purely to drive re-renders and expose through context.
const lastFetchTimeRef = useRef<number | null>(null);
const [lastFetchTimeValue, setLastFetchTimeValue] = useState<number | null>(null);
const [lastFetchTimeValue, setLastFetchTimeValue] = useState<number | null>(
null,
);
// billingStatusRef mirrors billingStatus so fetchBillingData can read the current value
// without needing billingStatus in its useCallback dep array.
const billingStatusRef = useRef<BillingStatus | null>(null);
// plansLastFetchTimeRef is the source of truth for timing; plansLastFetchTimeValue
// is the state mirror exposed via context so consumers can react to it.
const plansLastFetchTimeRef = useRef<number | null>(null);
const [plansLastFetchTimeValue, setPlansLastFetchTimeValue] = useState<number | null>(null);
const [plansLastFetchTimeValue, setPlansLastFetchTimeValue] = useState<
number | null
>(null);
// In-flight deduplication — prevents concurrent duplicate network requests.
const plansFetchInProgressRef = useRef(false);
const [isSaasMode, setIsSaasMode] = useState(false);
const [isAuthenticated, setIsAuthenticated] = useState(false);
// Access team context for derived state
const { currentTeam, isPersonalTeam, isTeamLeader, loading: teamLoading } = useSaaSTeam();
const {
currentTeam,
isPersonalTeam,
isTeamLeader,
loading: teamLoading,
} = useSaaSTeam();
// Compute derived state: user is managed member if in non-personal team but not leader
const isManagedTeamMember = currentTeam ? !isPersonalTeam && !isTeamLeader : false;
const isManagedTeamMember = currentTeam
? !isPersonalTeam && !isTeamLeader
: false;
// Check if in SaaS mode and authenticated (same pattern as SaaSTeamContext)
useEffect(() => {
@@ -120,7 +146,8 @@ export function SaasBillingProvider({ children }: { children: ReactNode }) {
checkAccess();
// Subscribe to connection mode changes
const unsubscribe = connectionModeService.subscribeToModeChanges(checkAccess);
const unsubscribe =
connectionModeService.subscribeToModeChanges(checkAccess);
return unsubscribe;
}, []);
@@ -151,7 +178,11 @@ export function SaasBillingProvider({ children }: { children: ReactNode }) {
// Cache check: Skip if fresh data exists (<5 min old)
const now = Date.now();
if (billingStatusRef.current && lastFetchTimeRef.current && now - lastFetchTimeRef.current < 300000) {
if (
billingStatusRef.current &&
lastFetchTimeRef.current &&
now - lastFetchTimeRef.current < 300000
) {
return;
}
@@ -199,7 +230,9 @@ export function SaasBillingProvider({ children }: { children: ReactNode }) {
setPlansLastFetchTimeValue(now);
} catch (err) {
console.error("[SaasBillingContext] Failed to fetch plans:", err);
setPlansError(err instanceof Error ? err.message : "Failed to fetch plans");
setPlansError(
err instanceof Error ? err.message : "Failed to fetch plans",
);
} finally {
plansFetchInProgressRef.current = false;
setPlansLoading(false);
@@ -237,7 +270,13 @@ export function SaasBillingProvider({ children }: { children: ReactNode }) {
) {
fetchBillingData();
}
}, [isSaasMode, isAuthenticated, teamLoading, isManagedTeamMember, fetchBillingData]);
}, [
isSaasMode,
isAuthenticated,
teamLoading,
isManagedTeamMember,
fetchBillingData,
]);
// Public refresh methods
const refreshBilling = useCallback(async () => {
@@ -282,7 +321,10 @@ export function SaasBillingProvider({ children }: { children: ReactNode }) {
if (newBalance <= 0 && (billingStatus.creditBalance ?? 0) > 0) {
window.dispatchEvent(
new CustomEvent("credits:exhausted", {
detail: { previousBalance: billingStatus.creditBalance ?? 0, currentBalance: newBalance },
detail: {
previousBalance: billingStatus.creditBalance ?? 0,
currentBalance: newBalance,
},
}),
);
}
@@ -336,7 +378,11 @@ export function SaasBillingProvider({ children }: { children: ReactNode }) {
],
);
return <SaasBillingContext.Provider value={contextValue}>{children}</SaasBillingContext.Provider>;
return (
<SaasBillingContext.Provider value={contextValue}>
{children}
</SaasBillingContext.Provider>
);
}
export function useSaaSBilling() {
@@ -349,12 +395,21 @@ export function useSaaSBilling() {
// Note: context.loading includes teamLoading, so this waits for team to load
useEffect(() => {
const needsFetch =
context.billingStatus === null && context.lastFetchTime === null && !context.loading && !context.isManagedTeamMember; // Managed members don't need billing data
context.billingStatus === null &&
context.lastFetchTime === null &&
!context.loading &&
!context.isManagedTeamMember; // Managed members don't need billing data
if (needsFetch) {
context.refreshBilling();
}
}, [context.billingStatus, context.lastFetchTime, context.loading, context.isManagedTeamMember, context.refreshBilling]);
}, [
context.billingStatus,
context.lastFetchTime,
context.loading,
context.isManagedTeamMember,
context.refreshBilling,
]);
return context;
}
@@ -365,10 +420,13 @@ export function useSaaSBilling() {
* Safe to call from multiple components — in-flight deduplication is handled by fetchPlansData.
*/
export function usePlanPricing() {
const { plans, plansLoading, plansError, plansLastFetchTime, refreshPlans } = useContext(SaasBillingContext);
const { plans, plansLoading, plansError, plansLastFetchTime, refreshPlans } =
useContext(SaasBillingContext);
useEffect(() => {
const isFresh = plansLastFetchTime !== null && Date.now() - plansLastFetchTime < PLANS_CACHE_TTL_MS;
const isFresh =
plansLastFetchTime !== null &&
Date.now() - plansLastFetchTime < PLANS_CACHE_TTL_MS;
if (!isFresh) {
refreshPlans();