mirror of
https://github.com/Kong/insomnia.git
synced 2026-09-21 05:35:28 -04:00
refactor: account data fetching (#10463)
This commit is contained in:
1 parent
7c334de276
commit
511cd126c4
20 files changed
+197
-143
No files matched your search
@@ -1,26 +1,63 @@
|
||||
import { getCurrentPlan, getUserProfile } from 'insomnia-api';
|
||||
import { type CurrentPlan, getCurrentPlan, getUserProfile, type Organization, type User } from 'insomnia-api';
|
||||
import { services } from 'insomnia-data';
|
||||
|
||||
import { invariant } from '~/common/utils/invariant';
|
||||
|
||||
export interface OrganizationData {
|
||||
organizations: Organization[];
|
||||
user?: User;
|
||||
currentPlan?: CurrentPlan;
|
||||
}
|
||||
|
||||
// Fetches the organization triple (organizations / user / current plan) from the
|
||||
// cloud API in parallel. Shared by both the TanStack query (the source of truth for
|
||||
// React consumers) and `syncOrganizations` (the localStorage write path still used by
|
||||
// the non-React readers). Throws if any of the three fail their invariant.
|
||||
export async function fetchOrganizationData(sessionId: string): Promise<OrganizationData> {
|
||||
const [organizations, user, currentPlan] = await Promise.all([
|
||||
services.organization.list(),
|
||||
getUserProfile({ sessionId }),
|
||||
getCurrentPlan({ sessionId }),
|
||||
]);
|
||||
|
||||
invariant(organizations, 'Failed to load organizations');
|
||||
invariant(user && user.id, 'Failed to load user');
|
||||
invariant(currentPlan && currentPlan.planId, 'Failed to load current plan');
|
||||
|
||||
return { organizations, user, currentPlan };
|
||||
}
|
||||
|
||||
// Write-through helper: persists the organization triple to localStorage so the
|
||||
// non-React readers (`getInitialEntry`, various loaders, event-stream handlers, etc.)
|
||||
// keep working while the migration to TanStack Query is incremental.
|
||||
export function writeOrganizationDataToLocalStorage(accountId: string, data: OrganizationData) {
|
||||
invariant(accountId, 'Account ID is not defined');
|
||||
localStorage.setItem(`${accountId}:spaces`, JSON.stringify(data.organizations));
|
||||
localStorage.setItem(`${accountId}:user`, JSON.stringify(data.user));
|
||||
localStorage.setItem(`${accountId}:currentPlan`, JSON.stringify(data.currentPlan));
|
||||
}
|
||||
|
||||
// Reads the persisted triple back from localStorage. Used to seed the query's
|
||||
// `initialData` so the first paint is instant (matching the old loader, which read
|
||||
// localStorage synchronously). Defaults mirror the previous loader behaviour.
|
||||
export function readOrganizationDataFromLocalStorage(accountId: string): OrganizationData {
|
||||
const userRaw = localStorage.getItem(`${accountId}:user`);
|
||||
const currentPlanRaw = localStorage.getItem(`${accountId}:currentPlan`);
|
||||
return {
|
||||
organizations: JSON.parse(localStorage.getItem(`${accountId}:spaces`) || '[]') as Organization[],
|
||||
// Leave these undefined (not `{}`) when the key is absent, so consumers can rely on the
|
||||
// declared optional shape — casting `{}` to User/CurrentPlan hides missing fields from the
|
||||
// type checker and causes runtime NPEs (e.g. `currentPlan.type.includes`).
|
||||
user: userRaw ? (JSON.parse(userRaw) as User) : undefined,
|
||||
currentPlan: currentPlanRaw ? (JSON.parse(currentPlanRaw) as CurrentPlan) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// This is reusable action/loader implementations.
|
||||
export async function syncOrganizations(sessionId: string, accountId: string) {
|
||||
try {
|
||||
const [organizations, user, currentPlan] = await Promise.all([
|
||||
services.organization.list(),
|
||||
getUserProfile({ sessionId }),
|
||||
getCurrentPlan({ sessionId }),
|
||||
]);
|
||||
|
||||
invariant(organizations, 'Failed to load organizations');
|
||||
invariant(user && user.id, 'Failed to load user');
|
||||
invariant(currentPlan && currentPlan.planId, 'Failed to load current plan');
|
||||
|
||||
invariant(accountId, 'Account ID is not defined');
|
||||
|
||||
localStorage.setItem(`${accountId}:spaces`, JSON.stringify(organizations));
|
||||
localStorage.setItem(`${accountId}:user`, JSON.stringify(user));
|
||||
localStorage.setItem(`${accountId}:currentPlan`, JSON.stringify(currentPlan));
|
||||
const data = await fetchOrganizationData(sessionId);
|
||||
writeOrganizationDataToLocalStorage(accountId, data);
|
||||
} catch (error) {
|
||||
console.log('[organization] Failed to load Organizations', error);
|
||||
}
|
||||
|
||||
+3
-3
@@ -33,7 +33,6 @@ import { getAllLocalFiles } from '~/common/project';
|
||||
import { sortMethodMap } from '~/common/sorting';
|
||||
import { invariant } from '~/common/utils/invariant';
|
||||
import { useRootLoaderData } from '~/root';
|
||||
import { useOrganizationLoaderData } from '~/routes/organization';
|
||||
import { useInsomniaSyncPullRemoteFileActionFetcher } from '~/routes/organization.$organizationId.insomnia-sync.pull-remote-file';
|
||||
import { useProjectLoaderData, useProjectRouteContext } from '~/routes/organization.$organizationId.project.$projectId';
|
||||
import { useWorkspaceNewActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.new';
|
||||
@@ -53,6 +52,7 @@ import { OrganizationTabList } from '~/ui/components/tabs/tab-list';
|
||||
import { TimeFromNow } from '~/ui/components/time-from-now';
|
||||
import { showResourceNotFoundToast } from '~/ui/components/toast-notification';
|
||||
import { useInsomniaEventStreamContext } from '~/ui/context/app/insomnia-event-stream-context';
|
||||
import { useOrganizations } from '~/ui/hooks/use-account-server-data';
|
||||
import { useGitFileIssues } from '~/ui/hooks/use-git-file-issues';
|
||||
import { useTabNavigate } from '~/ui/hooks/use-insomnia-tab';
|
||||
import { useOrganizationData } from '~/ui/hooks/use-organization-data';
|
||||
@@ -97,7 +97,7 @@ const Component = ({ loaderData }: Route.ComponentProps) => {
|
||||
)
|
||||
.map(f => f.formData?.get('backendProjectId'));
|
||||
|
||||
const organizationData = useOrganizationLoaderData();
|
||||
const organizations = useOrganizations();
|
||||
const { presence } = useInsomniaEventStreamContext();
|
||||
const { issuesByWorkspaceId } = useGitFileIssues();
|
||||
const storageRules = useOrganizationStorageRule(organizationId);
|
||||
@@ -120,7 +120,7 @@ const Component = ({ loaderData }: Route.ComponentProps) => {
|
||||
const [importModalType, setImportModalType] = useState<'file' | 'clipboard' | 'uri' | null>(null);
|
||||
const [isNewProjectModalOpen, setIsNewProjectModalOpen] = useState(false);
|
||||
const [isUpdateProjectModalOpen, setIsUpdateProjectModalOpen] = useState(false);
|
||||
const organization = organizationData?.organizations.find(o => o.id === organizationId);
|
||||
const organization = organizations.find(o => o.id === organizationId);
|
||||
const isUserOwner = Boolean(organization?.is_owner);
|
||||
const collectionItems = useMemo(
|
||||
() =>
|
||||
|
||||
+3
-3
@@ -33,7 +33,6 @@ import { buildRunnerItemKey, type RunnerItemStatus, type RunnerLiveItem } from '
|
||||
import { invariant } from '~/common/utils/invariant';
|
||||
import { defaultSendActionRuntime } from '~/network/network';
|
||||
import { useRootLoaderData } from '~/root';
|
||||
import { useOrganizationLoaderData } from '~/routes/organization';
|
||||
import type { CollectionRunnerContext } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.send';
|
||||
import { sendActionImplementation } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.send';
|
||||
import { AnalyticsEvent } from '~/ui/analytics';
|
||||
@@ -55,6 +54,7 @@ import { Tooltip } from '~/ui/components/tooltip';
|
||||
import { ResponseTimelineViewer } from '~/ui/components/viewers/response-timeline-viewer';
|
||||
import { useInsomniaTabContext } from '~/ui/context/app/insomnia-tab-context';
|
||||
import { useRunnerContext } from '~/ui/context/app/runner-context';
|
||||
import { useCurrentPlan } from '~/ui/hooks/use-account-server-data';
|
||||
import { buildRunnerTabId } from '~/ui/hooks/use-insomnia-tab';
|
||||
import { useRunnerRequestList } from '~/ui/hooks/use-runner-request-list';
|
||||
import {
|
||||
@@ -155,7 +155,7 @@ export const Runner: FC = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const [errorMsg, setErrorMsg] = useState<null | string>(null);
|
||||
|
||||
const organizationData = useOrganizationLoaderData();
|
||||
const currentPlan = useCurrentPlan();
|
||||
const targetFolderId = searchParams.get('folder') || '';
|
||||
|
||||
const { organizationId, projectId, workspaceId } = useParams() as {
|
||||
@@ -290,7 +290,7 @@ export const Runner: FC = () => {
|
||||
|
||||
window.main.trackAnalyticsEvent({
|
||||
event: AnalyticsEvent.collectionRunExecute,
|
||||
properties: { plan: organizationData?.currentPlan?.type || 'scratchpad', iterations: iterationCount },
|
||||
properties: { plan: currentPlan?.type || 'scratchpad', iterations: iterationCount },
|
||||
});
|
||||
|
||||
updateTabById?.(buildRunnerTabId(workspaceId, targetFolderId), { temporary: false });
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { Organization } from 'insomnia-api';
|
||||
import { services } from 'insomnia-data';
|
||||
import { href, redirect } from 'react-router';
|
||||
|
||||
import { syncOrganizations } from '~/common/organization';
|
||||
import { syncProjects } from '~/common/project';
|
||||
import { invariant } from '~/common/utils/invariant';
|
||||
import { findMigrationTargetSpaceId, migrateProjectsUnderOrganization } from '~/ui/organization-utils';
|
||||
@@ -27,12 +26,6 @@ export async function clientAction({ request }: Route.ClientActionArgs) {
|
||||
const { id: sessionId, accountId } = await services.userSession.get();
|
||||
|
||||
const taskPromiseList = [];
|
||||
if (asyncTaskList.includes(AsyncTask.SyncOrganization)) {
|
||||
invariant(sessionId, 'sessionId is required');
|
||||
invariant(accountId, 'accountId is required');
|
||||
taskPromiseList.push(syncOrganizations(sessionId, accountId));
|
||||
}
|
||||
|
||||
if (asyncTaskList.includes(AsyncTask.MigrateProjects)) {
|
||||
const organizations = JSON.parse(localStorage.getItem(`${accountId}:spaces`) || '[]') as Organization[];
|
||||
invariant(organizations.length, 'Failed to fetch organizations.');
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { services } from 'insomnia-data';
|
||||
|
||||
import { syncOrganizations } from '~/common/organization';
|
||||
import { createFetcherSubmitHook } from '~/ui/utils/router';
|
||||
|
||||
import type { Route } from './+types/organization.sync';
|
||||
|
||||
export async function clientAction(_args: Route.ClientActionArgs) {
|
||||
const { id: sessionId, accountId } = await services.userSession.get();
|
||||
|
||||
if (sessionId) {
|
||||
await syncOrganizations(sessionId, accountId);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export const useOrganizationSyncActionFetcher = createFetcherSubmitHook(
|
||||
submit => () => {
|
||||
return submit(
|
||||
{},
|
||||
{
|
||||
method: 'POST',
|
||||
action: '/organization/sync',
|
||||
},
|
||||
);
|
||||
},
|
||||
clientAction,
|
||||
);
|
||||
@@ -1,9 +1,9 @@
|
||||
import { type Billing, type CurrentPlan, type FeatureList, type Organization, type User } from 'insomnia-api';
|
||||
import { type CurrentPlan, type User } from 'insomnia-api';
|
||||
import type { Settings } from 'insomnia-data';
|
||||
import { models, services } from 'insomnia-data';
|
||||
import { models } from 'insomnia-data';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Button, Link, ToggleButton, Tooltip, TooltipTrigger } from 'react-aria-components';
|
||||
import { href, NavLink, Outlet, useLocation, useNavigate, useParams, useRouteLoaderData } from 'react-router';
|
||||
import { href, NavLink, Outlet, useLocation, useNavigate, useParams } from 'react-router';
|
||||
import * as reactUse from 'react-use';
|
||||
|
||||
import { useRootLoaderData } from '~/root';
|
||||
@@ -30,45 +30,10 @@ import { InsomniaEventStreamProvider } from '~/ui/context/app/insomnia-event-str
|
||||
import { SidebarContext } from '~/ui/context/app/insomnia-sidebar-context';
|
||||
import { InsomniaTabProvider } from '~/ui/context/app/insomnia-tab-context';
|
||||
import { RunnerProvider } from '~/ui/context/app/runner-context';
|
||||
import { useCurrentPlan, useCurrentUser, useOrganizations } from '~/ui/hooks/use-account-server-data';
|
||||
import { useCloseConnection } from '~/ui/hooks/use-close-connection';
|
||||
import type { AsyncTask } from '~/ui/utils/router';
|
||||
|
||||
import type { Route } from './+types/organization';
|
||||
|
||||
export interface OrganizationLoaderData {
|
||||
organizations: Organization[];
|
||||
user?: User;
|
||||
currentPlan?: CurrentPlan;
|
||||
}
|
||||
|
||||
export async function clientLoader(_args: Route.ClientLoaderArgs) {
|
||||
const { id, accountId } = await services.userSession.get();
|
||||
if (id) {
|
||||
const organizations = JSON.parse(localStorage.getItem(`${accountId}:spaces`) || '[]') as Organization[];
|
||||
const user = JSON.parse(localStorage.getItem(`${accountId}:user`) || '{}') as User;
|
||||
const currentPlan = JSON.parse(localStorage.getItem(`${accountId}:currentPlan`) || '{}') as CurrentPlan;
|
||||
return {
|
||||
organizations,
|
||||
user,
|
||||
currentPlan,
|
||||
};
|
||||
}
|
||||
return {
|
||||
organizations: [],
|
||||
user: undefined,
|
||||
currentPlan: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export interface OrganizationFeatureLoaderData {
|
||||
featuresPromise: Promise<FeatureList>;
|
||||
billingPromise: Promise<Billing>;
|
||||
}
|
||||
|
||||
export const useOrganizationLoaderData = () => {
|
||||
return useRouteLoaderData<typeof clientLoader>('routes/organization');
|
||||
};
|
||||
|
||||
interface IndicatorProps {
|
||||
asyncTaskStatus: 'error' | 'idle' | 'loading' | 'submitting';
|
||||
settings: Settings;
|
||||
@@ -194,8 +159,10 @@ const LoginUserActions = ({
|
||||
);
|
||||
};
|
||||
|
||||
const Component = ({ loaderData }: Route.ComponentProps) => {
|
||||
const { organizations, user, currentPlan } = loaderData;
|
||||
const Component = () => {
|
||||
const organizations = useOrganizations();
|
||||
const user = useCurrentUser();
|
||||
const currentPlan = useCurrentPlan();
|
||||
const { settings } = useRootLoaderData()!;
|
||||
|
||||
const workspaceData = useWorkspaceLoaderData();
|
||||
|
||||
@@ -3,12 +3,12 @@ import React, { useState } from 'react';
|
||||
import { Button } from 'react-aria-components';
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
|
||||
import { useOrganizationLoaderData } from '~/routes/organization';
|
||||
import { useRequestLoaderData } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId';
|
||||
import {
|
||||
isInMockContentTypeList,
|
||||
useMockRoutePatcher,
|
||||
} from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId';
|
||||
import { useCurrentPlan } from '~/ui/hooks/use-account-server-data';
|
||||
|
||||
import { getContentTypeName, getMimeTypeFromContentType } from '../../../common/constants';
|
||||
import { useWorkspaceLoaderData } from '../../../routes/organization.$organizationId.project.$projectId.workspace.$workspaceId';
|
||||
@@ -23,8 +23,8 @@ export const MockResponseExtractor = () => {
|
||||
|
||||
const { activeProject, activeWorkspace } = useWorkspaceLoaderData()!;
|
||||
const isLocalProject = !activeProject?.remoteId;
|
||||
const { currentPlan } = useOrganizationLoaderData()!;
|
||||
const isEnterprise = currentPlan?.type.includes('enterprise');
|
||||
const currentPlan = useCurrentPlan();
|
||||
const isEnterprise = currentPlan?.type?.includes('enterprise');
|
||||
|
||||
// In a local project, users are not allowed to create a cloud mock server, only enterprise users can create a self-hosted mock server.
|
||||
// In a local project, users without enterprise plan can't create cloud mock server route from a request response
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useTrialStartActionFetcher } from '~/routes/trial.start';
|
||||
import { Icon } from '~/ui/components/icon';
|
||||
import { TrialConfirmationModal } from '~/ui/components/modals/trial-confirmation-modal';
|
||||
import { Tooltip } from '~/ui/components/tooltip';
|
||||
import { useInvalidateAccountData } from '~/ui/hooks/use-account-server-data';
|
||||
import { usePlanData } from '~/ui/hooks/use-plan';
|
||||
import { useUserService } from '~/ui/hooks/use-user-service';
|
||||
import { formatNumber } from '~/ui/utils';
|
||||
@@ -29,6 +30,7 @@ export const HeaderPlanIndicator = ({ isMinimal }: Props) => {
|
||||
const planName = `${planDisplayName} Plan`;
|
||||
|
||||
const startFetcher = useTrialStartActionFetcher();
|
||||
const invalidateAccountData = useInvalidateAccountData();
|
||||
const { load: usageLoad, state: usageState, data: usageData } = useResourceUsageFetcher();
|
||||
|
||||
function handleStartTrial() {
|
||||
@@ -66,8 +68,11 @@ export const HeaderPlanIndicator = ({ isMinimal }: Props) => {
|
||||
useEffect(() => {
|
||||
if (startFetcher.data?.success) {
|
||||
setCanTrial(false);
|
||||
// trial.start wrote the new plan to localStorage; refresh the query so every
|
||||
// plan consumer (this indicator included) reflects the upgraded plan.
|
||||
invalidateAccountData();
|
||||
}
|
||||
}, [startFetcher.data?.success]);
|
||||
}, [startFetcher.data?.success, invalidateAccountData]);
|
||||
|
||||
const isUnlimited = isEnterpriseLike;
|
||||
|
||||
|
||||
@@ -9,13 +9,13 @@ import * as reactUse from 'react-use';
|
||||
import type { ScanResult } from '~/common/import';
|
||||
import { importScannedResources } from '~/routes/import.resources';
|
||||
import { scanImportResources } from '~/routes/import.scan';
|
||||
import { useOrganizationLoaderData } from '~/routes/organization';
|
||||
import { createProject } from '~/routes/organization.$organizationId.project.new';
|
||||
import { Checkbox } from '~/ui/components/base/checkbox';
|
||||
import { Modal, type ModalHandle } from '~/ui/components/base/modal';
|
||||
import { ModalHeader } from '~/ui/components/base/modal-header';
|
||||
import { Icon } from '~/ui/components/icon';
|
||||
import { Button } from '~/ui/components/themed-button';
|
||||
import { useOrganizations } from '~/ui/hooks/use-account-server-data';
|
||||
import { selectFileOrFolder } from '~/ui/utils/select-file-or-folder';
|
||||
|
||||
import { showModal } from '..';
|
||||
@@ -503,9 +503,9 @@ export const ImportProjectsModal = ({ organizationId, onHide }: { organizationId
|
||||
modalRef.current?.show();
|
||||
}, []);
|
||||
|
||||
const organizationData = useOrganizationLoaderData();
|
||||
const organizations = useOrganizations();
|
||||
const organizationName =
|
||||
organizationData?.organizations.find(org => org.id === organizationId)?.name || 'Organization';
|
||||
organizations.find(org => org.id === organizationId)?.name || 'Organization';
|
||||
|
||||
const [projectItems, setProjectItems] = useState<ProjectImportItem[]>([]);
|
||||
const [processingUIStatus, setProcessingUiStatus] = useState<'loading' | 'importing' | 'error' | 'complete'>(
|
||||
|
||||
@@ -5,10 +5,10 @@ import { Button, Dialog, Heading, Input, Modal, ModalOverlay } from 'react-aria-
|
||||
import { useRootLoaderData } from '~/root';
|
||||
import { useResetVaultKeyFetcher } from '~/routes/auth.reset-vault-key';
|
||||
import { useValidateVaultKeyActionFetcher } from '~/routes/auth.validate-vault-key';
|
||||
import { useOrganizationLoaderData } from '~/routes/organization';
|
||||
import { PromptButton } from '~/ui/components/base/prompt-button';
|
||||
import { Icon } from '~/ui/components/icon';
|
||||
import { VaultKeyDisplayInput } from '~/ui/components/settings/vault-key-panel';
|
||||
import { useOrganizations } from '~/ui/hooks/use-account-server-data';
|
||||
|
||||
export interface InputVaultKeyModalProps {
|
||||
onClose: (vaultKey?: string) => void;
|
||||
@@ -23,7 +23,7 @@ export const InputVaultKeyModal = (props: InputVaultKeyModalProps) => {
|
||||
const [resetDone, setResetDone] = useState(false);
|
||||
const resetVaultKeyFetcher = useResetVaultKeyFetcher();
|
||||
const validateVaultKeyFetcher = useValidateVaultKeyActionFetcher();
|
||||
const { organizations } = useOrganizationLoaderData()!;
|
||||
const organizations = useOrganizations();
|
||||
const isLoading = resetVaultKeyFetcher.state !== 'idle' || validateVaultKeyFetcher.state !== 'idle';
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -18,11 +18,11 @@ import { getAppWebsiteBaseURL } from '~/common/constants';
|
||||
import { docsPricingLearnMoreLink } from '~/common/documentation';
|
||||
import { debounce } from '~/common/misc';
|
||||
import { useRootLoaderData } from '~/root';
|
||||
import { useOrganizationLoaderData } from '~/routes/organization';
|
||||
import { useCollaboratorsSearchLoaderFetcher } from '~/routes/organization.$organizationId.collaborators-search';
|
||||
import { AnalyticsEvent } from '~/ui/analytics';
|
||||
import { Icon } from '~/ui/components/icon';
|
||||
import { useIsLightTheme } from '~/ui/hooks/theme';
|
||||
import { useOrganizations } from '~/ui/hooks/use-account-server-data';
|
||||
|
||||
import { startInvite } from './encryption';
|
||||
import { OrganizationMemberRolesSelector, SELECTOR_TYPE } from './organization-member-roles-selector';
|
||||
@@ -122,8 +122,8 @@ export const InviteForm = ({
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const { userSession } = useRootLoaderData()!;
|
||||
const organizationData = useOrganizationLoaderData();
|
||||
const organization = organizationData?.organizations.find(o => o.id === organizationId);
|
||||
const organizations = useOrganizations();
|
||||
const organization = organizations.find(o => o.id === organizationId);
|
||||
const isUserOwner = Boolean(organization?.is_owner);
|
||||
const sessionId = userSession.id;
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ import { useParams, useSearchParams } from 'react-router';
|
||||
import { getAppWebsiteBaseURL } from '~/common/constants';
|
||||
import { debounce } from '~/common/misc';
|
||||
import { invariant } from '~/common/utils/invariant';
|
||||
import { useOrganizationLoaderData } from '~/routes/organization';
|
||||
import { useCollaboratorsFetcher } from '~/routes/organization.$organizationId.collaborators';
|
||||
import { useInviteFetcher } from '~/routes/organization.$organizationId.collaborators.invites.$invitationId';
|
||||
import { useReinviteFetcher } from '~/routes/organization.$organizationId.collaborators.invites.$invitationId.reinvite';
|
||||
@@ -42,6 +41,7 @@ import { PromptButton } from '~/ui/components/base/prompt-button';
|
||||
import { Icon } from '~/ui/components/icon';
|
||||
import { AlertModal } from '~/ui/components/modals/alert-modal';
|
||||
import { showModal } from '~/ui/components/modals/index';
|
||||
import { useOrganizations } from '~/ui/hooks/use-account-server-data';
|
||||
|
||||
import { InviteForm } from './invite-form';
|
||||
import { OrganizationMemberRolesSelector, SELECTOR_TYPE } from './organization-member-roles-selector';
|
||||
@@ -597,8 +597,8 @@ export const InviteModalContainer: FC<{
|
||||
}> = ({ isOpen, setIsOpen }) => {
|
||||
const [loadingOrgInfo, setLoadingOrgInfo] = useState(true);
|
||||
const { organizationId } = useParams();
|
||||
const organizationData = useOrganizationLoaderData();
|
||||
const currentOrg = organizationData?.organizations.find(o => o.id === organizationId);
|
||||
const organizations = useOrganizations();
|
||||
const currentOrg = organizations.find(o => o.id === organizationId);
|
||||
const [allRoles, setAllRoles] = useState<Role[]>([]);
|
||||
const [currentUserRoleInOrg, setCurrentUserRoleInOrg] = useState<Role | null>(null);
|
||||
const [orgFeatures, setOrgFeatures] = useState<FeatureList | null>(null);
|
||||
|
||||
@@ -5,8 +5,8 @@ import React, { type FC, type MouseEventHandler, useEffect, useRef, useState } f
|
||||
import { OverlayContainer } from 'react-aria';
|
||||
import { href, useNavigate, useParams } from 'react-router';
|
||||
|
||||
import { useOrganizationLoaderData } from '~/routes/organization';
|
||||
import { useWorkspaceMoveActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.move';
|
||||
import { useOrganizations } from '~/ui/hooks/use-account-server-data';
|
||||
|
||||
import { getWorkspaceLabel } from '../../../common/get-workspace-label';
|
||||
import { scopeToBgColorMap, scopeToIconMap, scopeToTextColorMap } from '../../../common/get-workspace-label';
|
||||
@@ -26,7 +26,7 @@ export const WorkspaceDuplicateModal: FC<WorkspaceDuplicateModalProps> = ({ work
|
||||
organizationId: string;
|
||||
projectId: string;
|
||||
};
|
||||
const organizationData = useOrganizationLoaderData();
|
||||
const organizations = useOrganizations();
|
||||
const [selectedOrgId, setSelectedOrgId] = useState(organizationId);
|
||||
const [projectOptions, setProjectOptions] = useState<BaseModel[]>([]);
|
||||
const [selectedProjectId, setSelectedProjectId] = useState('');
|
||||
@@ -115,7 +115,7 @@ export const WorkspaceDuplicateModal: FC<WorkspaceDuplicateModalProps> = ({ work
|
||||
<label>
|
||||
Organization:
|
||||
<select name="orgId" value={selectedOrgId} onChange={e => setSelectedOrgId(e.target.value)}>
|
||||
{organizationData?.organizations.map(({ id, name }) => (
|
||||
{organizations.map(({ id, name }) => (
|
||||
<option key={id} value={id}>
|
||||
{name}
|
||||
</option>
|
||||
|
||||
@@ -7,9 +7,9 @@ import { Button } from '~/basic-components/button';
|
||||
import { LearnMoreLink } from '~/basic-components/link';
|
||||
import { getAppWebsiteBaseURL } from '~/common/constants';
|
||||
import { docsPricingLearnMoreLink } from '~/common/documentation';
|
||||
import { useOrganizationLoaderData } from '~/routes/organization';
|
||||
import type { ProjectType } from '~/ui/components/project/utils';
|
||||
import { useIsLightTheme } from '~/ui/hooks/theme';
|
||||
import { useOrganizations } from '~/ui/hooks/use-account-server-data';
|
||||
|
||||
interface Props {
|
||||
isGitSyncEnabled: boolean;
|
||||
@@ -20,9 +20,9 @@ export const ProjectTypeWarning = ({ isGitSyncEnabled, storageType, storageRules
|
||||
const isLightTheme = useIsLightTheme();
|
||||
const showStorageRestrictionMessage =
|
||||
!storageRules.enableCloudSync || !storageRules.enableLocalVault || !storageRules.enableGitSync;
|
||||
const organizationData = useOrganizationLoaderData();
|
||||
const organizations = useOrganizations();
|
||||
const { organizationId } = useParams() as { organizationId: string };
|
||||
const organization = organizationData?.organizations.find(o => o.id === organizationId);
|
||||
const organization = organizations.find(o => o.id === organizationId);
|
||||
// TODO: extract to a hook later
|
||||
const isUserOwner = Boolean(organization?.is_owner);
|
||||
return (
|
||||
|
||||
@@ -19,7 +19,6 @@ import { Button, Heading, ListBox, ListBoxItem, Popover, Select, SelectValue } f
|
||||
import { href, useParams } from 'react-router';
|
||||
|
||||
import { useRootLoaderData } from '~/root';
|
||||
import { useOrganizationLoaderData } from '~/routes/organization';
|
||||
import { useProjectListWorkspacesLoaderFetcher } from '~/routes/organization.$organizationId.project.$projectId.list-workspaces';
|
||||
import { useProjectMoveActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.move';
|
||||
import { useProjectMoveWorkspaceActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.move-workspace';
|
||||
@@ -27,6 +26,7 @@ import { useWorkspaceLoaderData } from '~/routes/organization.$organizationId.pr
|
||||
import { useUntrackedProjectsLoaderFetcher } from '~/routes/untracked-projects';
|
||||
import { AlertModal } from '~/ui/components/modals/alert-modal';
|
||||
import { ImportProjectsModal } from '~/ui/components/modals/import-modal/import-projects-modal';
|
||||
import { useOrganizations } from '~/ui/hooks/use-account-server-data';
|
||||
import { useOrganizationPermissions } from '~/ui/hooks/use-organization-features';
|
||||
import { usePlanData } from '~/ui/hooks/use-plan';
|
||||
|
||||
@@ -633,8 +633,7 @@ export const ImportExport: FC<Props> = ({ hideSettingsModal, onModalChange }) =>
|
||||
projectId: string;
|
||||
workspaceId?: string;
|
||||
};
|
||||
const organizationData = useOrganizationLoaderData();
|
||||
const organizations = organizationData?.organizations || [];
|
||||
const organizations = useOrganizations();
|
||||
|
||||
const { features } = useOrganizationPermissions();
|
||||
const { isEnterprisePlan } = usePlanData();
|
||||
@@ -681,7 +680,7 @@ export const ImportExport: FC<Props> = ({ hideSettingsModal, onModalChange }) =>
|
||||
const projectName = activeProject?.name ?? getProductName();
|
||||
const projects = projectLoaderData?.projects || [];
|
||||
const organizationName =
|
||||
organizationData?.organizations.find(org => org.id === organizationId)?.name || 'Organization';
|
||||
organizations.find(org => org.id === organizationId)?.name || 'Organization';
|
||||
|
||||
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
|
||||
const [isImportProjectsModalOpen, setIsImportProjectsModalOpen] = useState(false);
|
||||
|
||||
@@ -10,9 +10,9 @@ import { useProjectLoaderData } from '~/routes/organization.$organizationId.proj
|
||||
import { useWorkspaceLoaderData } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId';
|
||||
import { useInsomniaSyncDataActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.sync-data';
|
||||
import { useOrganizationSyncProjectsActionFetcher } from '~/routes/organization.$organizationId.sync-projects';
|
||||
import { useOrganizationSyncActionFetcher } from '~/routes/organization.sync';
|
||||
import uiEventBus, { CLOUD_SYNC_FILE_CHANGE } from '~/ui/event-bus';
|
||||
import { avatarImageCache } from '~/ui/hooks/image-cache';
|
||||
import { useInvalidateAccountData } from '~/ui/hooks/use-account-server-data';
|
||||
import { useInvalidateOrganizationStorageRule } from '~/ui/hooks/use-organization-storage-rule';
|
||||
|
||||
const InsomniaEventStreamContext = createContext<{
|
||||
@@ -97,7 +97,7 @@ export const InsomniaEventStreamProvider: FC<PropsWithChildren> = ({ children })
|
||||
const remoteId = projectData?.activeProject?.remoteId || workspaceData?.activeProject.remoteId;
|
||||
|
||||
const [presence, setPresence] = useState<UserPresence[]>([]);
|
||||
const { submit: syncOrganizationsSubmit } = useOrganizationSyncActionFetcher();
|
||||
const invalidateAccountData = useInvalidateAccountData();
|
||||
const invalidateStorageRule = useInvalidateOrganizationStorageRule();
|
||||
const { submit: syncProjectsSubmit } = useOrganizationSyncProjectsActionFetcher();
|
||||
const { submit: syncDataSubmit } = useInsomniaSyncDataActionFetcher();
|
||||
@@ -183,7 +183,7 @@ export const InsomniaEventStreamProvider: FC<PropsWithChildren> = ({ children })
|
||||
if (event.avatar) {
|
||||
window.setTimeout(() => avatarImageCache.invalidate(event.avatar), CDN_INVALIDATION_TTL);
|
||||
}
|
||||
syncOrganizationsSubmit();
|
||||
invalidateAccountData();
|
||||
} else if (event.type === 'StorageRuleChanged' && (event.team.startsWith('org_') || event.team.startsWith('team_'))) {
|
||||
invalidateStorageRule(event.team);
|
||||
} else if (event.type === 'TeamProjectChanged' && event.team === organizationId) {
|
||||
@@ -249,7 +249,7 @@ export const InsomniaEventStreamProvider: FC<PropsWithChildren> = ({ children })
|
||||
organizationId,
|
||||
revalidate,
|
||||
syncDataSubmit,
|
||||
syncOrganizationsSubmit,
|
||||
invalidateAccountData,
|
||||
syncProjectsSubmit,
|
||||
invalidateStorageRule,
|
||||
userSession.accountId,
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import type { CurrentPlan, Organization, User } from 'insomnia-api';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import {
|
||||
fetchOrganizationData,
|
||||
type OrganizationData,
|
||||
readOrganizationDataFromLocalStorage,
|
||||
writeOrganizationDataToLocalStorage,
|
||||
} from '~/common/organization';
|
||||
import { useRootLoaderData } from '~/root';
|
||||
import { useServerDataQueryClient } from '~/ui/context/app/server-data-context';
|
||||
import { useServerQuery } from '~/ui/hooks/use-query';
|
||||
|
||||
// Account-scoped server data (organizations / user / current plan).
|
||||
const accountServerDataKey = (accountId: string) => ['account-server-data', accountId] as const;
|
||||
|
||||
const loggedOutData: OrganizationData = { organizations: [], user: undefined, currentPlan: undefined };
|
||||
|
||||
// Stable selector references so TanStack doesn't re-run them every render.
|
||||
const selectOrganizations = (data: OrganizationData) => data.organizations;
|
||||
const selectUser = (data: OrganizationData) => data.user;
|
||||
const selectCurrentPlan = (data: OrganizationData) => data.currentPlan;
|
||||
|
||||
/**
|
||||
* The single source of truth (for React consumers) of the current account's organizations,
|
||||
* user and current plan.
|
||||
* Freshness: the server-data client's default `staleTime` refetches on mount, so the value is
|
||||
* authoritative regardless of what localStorage held. The queryFn writes the result through to
|
||||
* localStorage — a bridge that keeps the remaining non-React readers (`getInitialEntry`, various
|
||||
* loaders, event-stream handlers) working while their migration is done in a later phase.
|
||||
*
|
||||
* Refresh is event-driven and owned by `useInvalidateAccountData` (SSE `OrganizationChanged`,
|
||||
* trial start, etc.).
|
||||
*
|
||||
* Prefer the slice hooks below (`useOrganizations` / `useCurrentUser` / `useCurrentPlan`) at call
|
||||
* sites; each subscribes to only its slice.
|
||||
*/
|
||||
function useAccountServerData<TData>(select: (data: OrganizationData) => TData): TData | undefined {
|
||||
const { userSession } = useRootLoaderData()!;
|
||||
const sessionId = userSession.id;
|
||||
const accountId = userSession.accountId;
|
||||
const isLoggedIn = !!sessionId;
|
||||
|
||||
const { data } = useServerQuery({
|
||||
queryKey: accountServerDataKey(accountId),
|
||||
queryFn: async () => {
|
||||
const result = await fetchOrganizationData(sessionId);
|
||||
writeOrganizationDataToLocalStorage(accountId, result);
|
||||
return result;
|
||||
},
|
||||
// Seed from localStorage for an instant first paint (matching the old loader, which read
|
||||
// localStorage synchronously). When logged out we intentionally keep `user`/`currentPlan`
|
||||
// undefined so consumers can distinguish "no user".
|
||||
initialData: () => (isLoggedIn ? readOrganizationDataFromLocalStorage(accountId) : loggedOutData),
|
||||
enabled: isLoggedIn,
|
||||
select,
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/** The current account's organizations. Always an array. */
|
||||
export function useOrganizations(): Organization[] {
|
||||
return useAccountServerData(selectOrganizations) ?? [];
|
||||
}
|
||||
|
||||
/** The signed-in user, or undefined when logged out / not yet loaded. */
|
||||
export function useCurrentUser(): User | undefined {
|
||||
return useAccountServerData(selectUser);
|
||||
}
|
||||
|
||||
/** The account's current plan, or undefined when logged out / not yet loaded. */
|
||||
export function useCurrentPlan(): CurrentPlan | undefined {
|
||||
return useAccountServerData(selectCurrentPlan);
|
||||
}
|
||||
|
||||
export function useInvalidateAccountData() {
|
||||
const queryClient = useServerDataQueryClient();
|
||||
const { userSession } = useRootLoaderData()!;
|
||||
const accountId = userSession.accountId;
|
||||
|
||||
return useCallback(
|
||||
() => queryClient.invalidateQueries({ queryKey: accountServerDataKey(accountId) }),
|
||||
[queryClient, accountId],
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { models } from 'insomnia-data';
|
||||
import { useParams } from 'react-router';
|
||||
|
||||
import { useRootLoaderData } from '~/root';
|
||||
import { useOrganizationLoaderData } from '~/routes/organization';
|
||||
import { useCurrentPlan, useOrganizations } from '~/ui/hooks/use-account-server-data';
|
||||
|
||||
export const usePlanData = () => {
|
||||
let isOwner = false;
|
||||
@@ -14,19 +14,15 @@ export const usePlanData = () => {
|
||||
let isEnterprisePlan = false;
|
||||
const { userSession } = useRootLoaderData()!;
|
||||
const { organizationId } = useParams<{ organizationId: string }>();
|
||||
const organizationData = useOrganizationLoaderData();
|
||||
const organizations = useOrganizations();
|
||||
const currentPlan = useCurrentPlan();
|
||||
// ensure user has logged in with valid organization
|
||||
if (
|
||||
organizationData &&
|
||||
userSession &&
|
||||
Array.isArray(organizationData.organizations) &&
|
||||
organizationData.organizations.length > 0
|
||||
) {
|
||||
const currentOrg = organizationData.organizations.find(organization => organization.id === organizationId);
|
||||
if (userSession && Array.isArray(organizations) && organizations.length > 0) {
|
||||
const currentOrg = organizations.find(organization => organization.id === organizationId);
|
||||
if (currentOrg && userSession.accountId) {
|
||||
isOwner = Boolean(currentOrg.is_owner);
|
||||
}
|
||||
planType = organizationData.currentPlan?.type || planType;
|
||||
planType = currentPlan?.type || planType;
|
||||
isFreePlan = planType.includes('free');
|
||||
isTeamPlan = planType.includes('team');
|
||||
isEnterprisePlan = planType.includes('enterprise');
|
||||
@@ -34,7 +30,7 @@ export const usePlanData = () => {
|
||||
}
|
||||
return {
|
||||
isOwner,
|
||||
currentPlan: organizationData?.currentPlan,
|
||||
currentPlan,
|
||||
planDisplayName,
|
||||
isFreePlan,
|
||||
isTeamPlan,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useOrganizationLoaderData } from '~/routes/organization';
|
||||
import { useCurrentPlan, useCurrentUser } from '~/ui/hooks/use-account-server-data';
|
||||
import { diffInDayCeil } from '~/ui/utils';
|
||||
|
||||
export function useUserService() {
|
||||
const { currentPlan, user } = useOrganizationLoaderData()!;
|
||||
const currentPlan = useCurrentPlan();
|
||||
const user = useCurrentUser();
|
||||
const isPro = currentPlan?.type === 'individual' || currentPlan?.type === 'team';
|
||||
const isEnterpriseOwner = currentPlan?.type === 'enterprise';
|
||||
const isEnterpriseMember = currentPlan?.type === 'enterprise-member';
|
||||
|
||||
@@ -7,7 +7,6 @@ import { href, matchPath, type PathMatch, useFetcher } from 'react-router';
|
||||
import { CURRENT_MIGRATION_VERSION } from '~/sync/git/git-migration-version';
|
||||
|
||||
export const enum AsyncTask {
|
||||
SyncOrganization,
|
||||
MigrateProjects,
|
||||
SyncProjects,
|
||||
}
|
||||
@@ -142,7 +141,7 @@ export const getInitialEntry = async () => {
|
||||
pathname: await getInitialRouteForOrganization({ organizationId, navigateToWorkspace: true }),
|
||||
state: {
|
||||
// async task need to execute when first entry
|
||||
asyncTaskList: [AsyncTask.SyncOrganization, AsyncTask.MigrateProjects, AsyncTask.SyncProjects],
|
||||
asyncTaskList: [AsyncTask.MigrateProjects, AsyncTask.SyncProjects],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user