b470cb21a1
## Summary This PR upgrades Apollo Client from v3.10.0 to v4 and refactors error handling patterns across the codebase to use a new centralized `useSnackBarOnQueryError` hook. ## Key Changes - **Dependency Update**: Upgraded `@apollo/client` from `^3.10.0` to `^3.11.0` in root package.json - **New Hook**: Added `useSnackBarOnQueryError` hook for centralized Apollo query error handling with snack bar notifications - **Error Handling Refactor**: Updated 100+ files to use the new error handling pattern: - Removed direct `ApolloError` imports where no longer needed - Replaced manual error handling logic with `useSnackBarOnQueryError` hook - Simplified error handling in hooks and components across multiple modules - **GraphQL Codegen**: Updated codegen configuration files to work with Apollo Client v3.11.0 - **Type Definitions**: Added TypeScript declaration file for `apollo-upload-client` module - **Test Updates**: Updated test files to reflect new error handling patterns ## Notable Implementation Details - The new `useSnackBarOnQueryError` hook provides a consistent way to handle Apollo query errors with automatic snack bar notifications - Changes span across multiple feature areas: auth, object records, settings, workflows, billing, and more - All changes maintain backward compatibility while improving code maintainability and reducing duplication - Jest configuration updated to work with the new Apollo Client version https://claude.ai/code/session_019WGZ6Rd7sEHuBg9sTrXRqJ --------- Co-authored-by: Claude <noreply@anthropic.com>
268 lines
8.8 KiB
TypeScript
268 lines
8.8 KiB
TypeScript
import { currentUserState } from '@/auth/states/currentUserState';
|
|
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
|
import { canManageFeatureFlagsState } from '@/client-config/states/canManageFeatureFlagsState';
|
|
import { SettingsAdminTableCard } from '@/settings/admin-panel/components/SettingsAdminTableCard';
|
|
import { useFeatureFlagState } from '@/settings/admin-panel/hooks/useFeatureFlagState';
|
|
import { useImpersonationAuth } from '@/settings/admin-panel/hooks/useImpersonationAuth';
|
|
import { useImpersonationRedirect } from '@/settings/admin-panel/hooks/useImpersonationRedirect';
|
|
import { userLookupResultState } from '@/settings/admin-panel/states/userLookupResultState';
|
|
import { type WorkspaceInfo } from '@/settings/admin-panel/types/WorkspaceInfo';
|
|
import { getWorkspaceSchemaName } from '@/settings/admin-panel/utils/getWorkspaceSchemaName';
|
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
|
import { Table } from '@/ui/layout/table/components/Table';
|
|
import { TableBody } from '@/ui/layout/table/components/TableBody';
|
|
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
|
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
|
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
|
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
|
|
import { styled } from '@linaria/react';
|
|
import { useLingui } from '@lingui/react/macro';
|
|
import { isNonEmptyString } from '@sniptt/guards';
|
|
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
|
import { useState } from 'react';
|
|
import { getImageAbsoluteURI, isDefined } from 'twenty-shared/utils';
|
|
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
|
import { AvatarOrIcon, Chip } from 'twenty-ui/components';
|
|
import {
|
|
H2Title,
|
|
IconEyeShare,
|
|
IconHome,
|
|
IconId,
|
|
IconLink,
|
|
IconUser,
|
|
} from 'twenty-ui/display';
|
|
import { Button, Toggle } from 'twenty-ui/input';
|
|
import { Section } from 'twenty-ui/layout';
|
|
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
|
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
|
import { useMutation } from '@apollo/client/react';
|
|
import {
|
|
type FeatureFlagKey,
|
|
ImpersonateDocument,
|
|
UpdateWorkspaceFeatureFlagDocument,
|
|
} from '~/generated-metadata/graphql';
|
|
|
|
type SettingsAdminWorkspaceContentProps = {
|
|
activeWorkspace: WorkspaceInfo | undefined;
|
|
};
|
|
|
|
const StyledContainer = styled.div`
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: ${themeCssVariables.spacing[3]};
|
|
margin-top: ${themeCssVariables.spacing[6]};
|
|
`;
|
|
|
|
const StyledButtonContainer = styled.div`
|
|
margin-top: ${themeCssVariables.spacing[3]};
|
|
`;
|
|
|
|
export const SettingsAdminWorkspaceContent = ({
|
|
activeWorkspace,
|
|
}: SettingsAdminWorkspaceContentProps) => {
|
|
const canManageFeatureFlags = useAtomStateValue(canManageFeatureFlagsState);
|
|
const { enqueueErrorSnackBar } = useSnackBar();
|
|
const [currentUser] = useAtomState(currentUserState);
|
|
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
|
|
|
const [updateFeatureFlag] = useMutation(UpdateWorkspaceFeatureFlagDocument);
|
|
const [isImpersonateLoading, setIsImpersonationLoading] = useState(false);
|
|
const { executeImpersonationAuth } = useImpersonationAuth();
|
|
const { executeImpersonationRedirect } = useImpersonationRedirect();
|
|
const [impersonate] = useMutation(ImpersonateDocument);
|
|
|
|
const { updateFeatureFlagState } = useFeatureFlagState();
|
|
const userLookupResult = useAtomStateValue(userLookupResultState);
|
|
|
|
const { t } = useLingui();
|
|
|
|
const handleImpersonate = async (workspaceId: string) => {
|
|
if (!userLookupResult?.user.id) {
|
|
enqueueErrorSnackBar({ message: t`Please search for a user first` });
|
|
return;
|
|
}
|
|
|
|
setIsImpersonationLoading(true);
|
|
|
|
await impersonate({
|
|
variables: { userId: userLookupResult.user.id, workspaceId },
|
|
onCompleted: async (data) => {
|
|
const { loginToken, workspace } = data.impersonate;
|
|
const isCurrentWorkspace = workspace.id === currentWorkspace?.id;
|
|
if (isCurrentWorkspace) {
|
|
await executeImpersonationAuth(loginToken.token);
|
|
return;
|
|
}
|
|
|
|
return executeImpersonationRedirect(
|
|
workspace.workspaceUrls,
|
|
loginToken.token,
|
|
'_blank',
|
|
);
|
|
},
|
|
onError: (error) => {
|
|
const errorMessage = error.message;
|
|
enqueueErrorSnackBar({
|
|
message: t`Failed to impersonate user. ${errorMessage}`,
|
|
});
|
|
},
|
|
}).finally(() => {
|
|
setIsImpersonationLoading(false);
|
|
});
|
|
};
|
|
|
|
const handleFeatureFlagUpdate = async (
|
|
workspaceId: string,
|
|
featureFlag: FeatureFlagKey,
|
|
value: boolean,
|
|
) => {
|
|
const previousValue = userLookupResult?.workspaces
|
|
.find((workspace) => workspace.id === workspaceId)
|
|
?.featureFlags.find((flag) => flag.key === featureFlag)?.value;
|
|
|
|
updateFeatureFlagState(workspaceId, featureFlag, value);
|
|
await updateFeatureFlag({
|
|
variables: {
|
|
workspaceId,
|
|
featureFlag,
|
|
value,
|
|
},
|
|
|
|
onError: (error) => {
|
|
if (isDefined(previousValue)) {
|
|
updateFeatureFlagState(workspaceId, featureFlag, previousValue);
|
|
}
|
|
const errorMessage = error.message;
|
|
enqueueErrorSnackBar({
|
|
message: t`Failed to update feature flag. ${errorMessage}`,
|
|
});
|
|
},
|
|
});
|
|
};
|
|
|
|
const getWorkspaceUrl = (workspaceUrls: WorkspaceInfo['workspaceUrls']) => {
|
|
return workspaceUrls.customUrl ?? workspaceUrls.subdomainUrl;
|
|
};
|
|
|
|
const workspaceInfoItems = [
|
|
{
|
|
Icon: IconHome,
|
|
label: t`Name`,
|
|
value: (
|
|
<Chip
|
|
label={activeWorkspace?.name ?? ''}
|
|
emptyLabel={t`Untitled`}
|
|
leftComponent={
|
|
<AvatarOrIcon
|
|
avatarUrl={
|
|
getImageAbsoluteURI({
|
|
imageUrl: isNonEmptyString(activeWorkspace?.logo)
|
|
? activeWorkspace?.logo
|
|
: DEFAULT_WORKSPACE_LOGO,
|
|
baseUrl: REACT_APP_SERVER_BASE_URL,
|
|
}) ?? ''
|
|
}
|
|
/>
|
|
}
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
Icon: IconId,
|
|
label: t`ID`,
|
|
value: activeWorkspace?.id,
|
|
},
|
|
{
|
|
Icon: IconId,
|
|
label: t`Schema name`,
|
|
value: isDefined(activeWorkspace?.id)
|
|
? getWorkspaceSchemaName(activeWorkspace.id)
|
|
: '',
|
|
},
|
|
{
|
|
Icon: IconLink,
|
|
label: t`URL`,
|
|
value: activeWorkspace?.workspaceUrls
|
|
? getWorkspaceUrl(activeWorkspace.workspaceUrls)
|
|
: '',
|
|
},
|
|
{
|
|
Icon: IconUser,
|
|
label: t`Members`,
|
|
value: activeWorkspace?.totalUsers,
|
|
},
|
|
];
|
|
|
|
if (!activeWorkspace) return null;
|
|
|
|
return (
|
|
<StyledContainer>
|
|
<Section>
|
|
<H2Title
|
|
title={t`Workspace Info`}
|
|
description={t`About this workspace`}
|
|
/>
|
|
<SettingsAdminTableCard
|
|
items={workspaceInfoItems}
|
|
gridAutoColumns="1fr 4fr"
|
|
/>
|
|
<StyledButtonContainer>
|
|
{currentUser?.canImpersonate && (
|
|
<Button
|
|
Icon={IconEyeShare}
|
|
variant="primary"
|
|
accent="default"
|
|
title={
|
|
activeWorkspace.allowImpersonation === false
|
|
? t`Impersonation is disabled for this workspace`
|
|
: t`Impersonate`
|
|
}
|
|
onClick={() => handleImpersonate(activeWorkspace.id)}
|
|
disabled={
|
|
isImpersonateLoading ||
|
|
activeWorkspace.allowImpersonation === false
|
|
}
|
|
dataTestId="impersonate-button"
|
|
/>
|
|
)}
|
|
</StyledButtonContainer>
|
|
</Section>
|
|
{canManageFeatureFlags && (
|
|
<Table>
|
|
<TableBody>
|
|
<TableRow
|
|
gridAutoColumns="1fr 100px"
|
|
mobileGridAutoColumns="1fr 80px"
|
|
>
|
|
<TableHeader>{t`Feature Flag`}</TableHeader>
|
|
<TableHeader align="right">{t`Status`}</TableHeader>
|
|
</TableRow>
|
|
|
|
{activeWorkspace.featureFlags.map((flag) => (
|
|
<TableRow
|
|
gridAutoColumns="1fr 100px"
|
|
mobileGridAutoColumns="1fr 80px"
|
|
key={flag.key}
|
|
>
|
|
<TableCell>{flag.key}</TableCell>
|
|
<TableCell align="right">
|
|
<Toggle
|
|
value={flag.value}
|
|
onChange={(newValue) =>
|
|
handleFeatureFlagUpdate(
|
|
activeWorkspace.id,
|
|
flag.key,
|
|
newValue,
|
|
)
|
|
}
|
|
/>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
)}
|
|
</StyledContainer>
|
|
);
|
|
};
|