Admin panel refactor (#10119)

addressing > 
There are two patterns to avoid:
Creating functions that return JSX like renderThing() -> this was taken
already addressed in https://github.com/twentyhq/twenty/pull/10011
Making a hook that "stores" all the logic of a component - > this PR is
addressing this particular pattern
In essence, handlers should remain in the component and be connected to
their events.
And everything in a handler can be abstracted into its dedicated hook.
For example:
const { myReactiveState } =
useRecoilValue(myReactiveStateComponentState);
const { removeThingFromOtherThing } = useRemoveThingFromOtherThing();

const handleClick = () => {
  if (isDefined(myReactiveState)) {
    removeThingFromOtherThing();
  }
}

Broadly speaking, this is how you can split large components into
several sub-hooks.

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
nitin
2025-02-11 22:40:28 +05:30
committed by GitHub
parent 83bf2d1739
commit 252922b522
11 changed files with 266 additions and 296 deletions

View File

@ -36,6 +36,8 @@ const StyledShowMoreButton = styled(Button)<{ isSelected?: boolean }>`
`} `}
`; `;
const StyledEnvVariablesDescription = styled.div``;
export const SettingsAdminEnvVariables = () => { export const SettingsAdminEnvVariables = () => {
const { data: environmentVariables } = useGetEnvironmentVariablesGroupedQuery( const { data: environmentVariables } = useGetEnvironmentVariablesGroupedQuery(
{ {
@ -65,59 +67,68 @@ export const SettingsAdminEnvVariables = () => {
); );
return ( return (
<Section> <>
{visibleGroups.map((group) => ( <StyledEnvVariablesDescription>
<StyledGroupContainer key={group.name}> These are only the server values. Ensure your worker environment has the
<H1Title title={group.name} fontColor={H1TitleFontColor.Primary} /> same variables and values, this is required for asynchronous tasks like
{group.description !== '' && ( email sync.
<StyledGroupDescription>{group.description}</StyledGroupDescription> </StyledEnvVariablesDescription>
)} <Section>
{group.variables.length > 0 && ( {visibleGroups.map((group) => (
<StyledGroupVariablesContainer> <StyledGroupContainer key={group.name}>
<SettingsAdminEnvVariablesTable variables={group.variables} /> <H1Title title={group.name} fontColor={H1TitleFontColor.Primary} />
</StyledGroupVariablesContainer> {group.description !== '' && (
)} <StyledGroupDescription>
</StyledGroupContainer> {group.description}
))} </StyledGroupDescription>
)}
{group.variables.length > 0 && (
<StyledGroupVariablesContainer>
<SettingsAdminEnvVariablesTable variables={group.variables} />
</StyledGroupVariablesContainer>
)}
</StyledGroupContainer>
))}
{hiddenGroups.length > 0 && ( {hiddenGroups.length > 0 && (
<> <>
<StyledButtonsRow> <StyledButtonsRow>
{hiddenGroups.map((group) => ( {hiddenGroups.map((group) => (
<StyledShowMoreButton <StyledShowMoreButton
key={group.name} key={group.name}
onClick={() => toggleGroupVisibility(group.name)} onClick={() => toggleGroupVisibility(group.name)}
title={group.name} title={group.name}
variant="secondary" variant="secondary"
isSelected={selectedGroup === group.name} isSelected={selectedGroup === group.name}
> >
{group.name} variables {group.name} variables
</StyledShowMoreButton> </StyledShowMoreButton>
))} ))}
</StyledButtonsRow> </StyledButtonsRow>
{selectedGroupData && ( {selectedGroupData && (
<StyledGroupContainer> <StyledGroupContainer>
<H1Title <H1Title
title={selectedGroupData.name} title={selectedGroupData.name}
fontColor={H1TitleFontColor.Primary} fontColor={H1TitleFontColor.Primary}
/> />
{selectedGroupData.description !== '' && ( {selectedGroupData.description !== '' && (
<StyledGroupDescription> <StyledGroupDescription>
{selectedGroupData.description} {selectedGroupData.description}
</StyledGroupDescription> </StyledGroupDescription>
)} )}
{selectedGroupData.variables.length > 0 && ( {selectedGroupData.variables.length > 0 && (
<StyledGroupVariablesContainer> <StyledGroupVariablesContainer>
<SettingsAdminEnvVariablesTable <SettingsAdminEnvVariablesTable
variables={selectedGroupData.variables} variables={selectedGroupData.variables}
/> />
</StyledGroupVariablesContainer> </StyledGroupVariablesContainer>
)} )}
</StyledGroupContainer> </StyledGroupContainer>
)} )}
</> </>
)} )}
</Section> </Section>
</>
); );
}; };

View File

@ -1,10 +1,9 @@
import { canManageFeatureFlagsState } from '@/client-config/states/canManageFeatureFlagsState'; import { canManageFeatureFlagsState } from '@/client-config/states/canManageFeatureFlagsState';
import { SettingsAdminWorkspaceContent } from '@/settings/admin-panel/components/SettingsAdminWorkspaceContent'; import { SettingsAdminWorkspaceContent } from '@/settings/admin-panel/components/SettingsAdminWorkspaceContent';
import { SETTINGS_ADMIN_USER_LOOKUP_WORKSPACE_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminUserLookupWorkspaceTabsId'; import { SETTINGS_ADMIN_USER_LOOKUP_WORKSPACE_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminUserLookupWorkspaceTabsId';
import { useImpersonate } from '@/settings/admin-panel/hooks/useImpersonate';
import { useUserLookup } from '@/settings/admin-panel/hooks/useUserLookup';
import { adminPanelErrorState } from '@/settings/admin-panel/states/adminPanelErrorState';
import { userLookupResultState } from '@/settings/admin-panel/states/userLookupResultState'; import { userLookupResultState } from '@/settings/admin-panel/states/userLookupResultState';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { TextInput } from '@/ui/input/components/TextInput'; import { TextInput } from '@/ui/input/components/TextInput';
import { TabList } from '@/ui/layout/tab/components/TabList'; import { TabList } from '@/ui/layout/tab/components/TabList';
import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
@ -12,7 +11,7 @@ import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/consta
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards'; import { isNonEmptyString } from '@sniptt/guards';
import { useState } from 'react'; import { useState } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilState, useRecoilValue } from 'recoil';
import { getImageAbsoluteURI, isDefined } from 'twenty-shared'; import { getImageAbsoluteURI, isDefined } from 'twenty-shared';
import { import {
Button, Button,
@ -23,6 +22,7 @@ import {
Section, Section,
} from 'twenty-ui'; } from 'twenty-ui';
import { REACT_APP_SERVER_BASE_URL } from '~/config'; import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { useUserLookupAdminPanelMutation } from '~/generated/graphql';
const StyledLinkContainer = styled.div` const StyledLinkContainer = styled.div`
margin-right: ${({ theme }) => theme.spacing(2)}; margin-right: ${({ theme }) => theme.spacing(2)};
@ -35,11 +35,6 @@ const StyledContainer = styled.div`
flex-direction: row; flex-direction: row;
`; `;
const StyledErrorSection = styled.div`
color: ${({ theme }) => theme.font.color.danger};
margin-top: ${({ theme }) => theme.spacing(2)};
`;
const StyledUserInfo = styled.div` const StyledUserInfo = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(5)}; margin-bottom: ${({ theme }) => theme.spacing(5)};
`; `;
@ -60,40 +55,48 @@ const StyledContentContainer = styled.div`
export const SettingsAdminGeneral = () => { export const SettingsAdminGeneral = () => {
const [userIdentifier, setUserIdentifier] = useState(''); const [userIdentifier, setUserIdentifier] = useState('');
const [userId, setUserId] = useState(''); const { enqueueSnackBar } = useSnackBar();
const { error: impersonateError } = useImpersonate();
const { activeTabId, setActiveTabId } = useTabList( const { activeTabId, setActiveTabId } = useTabList(
SETTINGS_ADMIN_USER_LOOKUP_WORKSPACE_TABS_ID, SETTINGS_ADMIN_USER_LOOKUP_WORKSPACE_TABS_ID,
); );
const userLookupResult = useRecoilValue(userLookupResultState); const [userLookupResult, setUserLookupResult] = useRecoilState(
const adminPanelError = useRecoilValue(adminPanelErrorState); userLookupResultState,
);
const [isUserLookupLoading, setIsUserLookupLoading] = useState(false);
const { handleUserLookup, isLoading } = useUserLookup(); const [userLookup] = useUserLookupAdminPanelMutation();
const canManageFeatureFlags = useRecoilValue(canManageFeatureFlagsState); const canManageFeatureFlags = useRecoilValue(canManageFeatureFlagsState);
const handleSearch = async () => { const handleSearch = async () => {
setActiveTabId(''); setActiveTabId('');
setIsUserLookupLoading(true);
setUserLookupResult(null);
const result = await handleUserLookup(userIdentifier); const response = await userLookup({
variables: { userIdentifier },
onCompleted: (data) => {
setIsUserLookupLoading(false);
if (isDefined(data?.userLookupAdminPanel)) {
setUserLookupResult(data.userLookupAdminPanel);
}
},
onError: (error) => {
setIsUserLookupLoading(false);
enqueueSnackBar(error.message, {
variant: SnackBarVariant.Error,
});
},
});
if (isDefined(result?.user?.id) && !adminPanelError) { const result = response.data?.userLookupAdminPanel;
setUserId(result.user.id.trim());
}
if ( if (isDefined(result?.workspaces) && result.workspaces.length > 0) {
isDefined(result?.workspaces) &&
result.workspaces.length > 0 &&
!adminPanelError
) {
setActiveTabId(result.workspaces[0].id); setActiveTabId(result.workspaces[0].id);
} }
}; };
const shouldShowUserData = userLookupResult && !adminPanelError;
const activeWorkspace = userLookupResult?.workspaces.find( const activeWorkspace = userLookupResult?.workspaces.find(
(workspace) => workspace.id === activeTabId, (workspace) => workspace.id === activeTabId,
); );
@ -139,7 +142,7 @@ export const SettingsAdminGeneral = () => {
onInputEnter={handleSearch} onInputEnter={handleSearch}
placeholder="Enter user ID or email address" placeholder="Enter user ID or email address"
fullWidth fullWidth
disabled={isLoading} disabled={isUserLookupLoading}
/> />
</StyledLinkContainer> </StyledLinkContainer>
<Button <Button
@ -148,18 +151,12 @@ export const SettingsAdminGeneral = () => {
accent="blue" accent="blue"
title="Search" title="Search"
onClick={handleSearch} onClick={handleSearch}
disabled={!userIdentifier.trim() || isLoading} disabled={!userIdentifier.trim() || isUserLookupLoading}
/> />
</StyledContainer> </StyledContainer>
{(adminPanelError || impersonateError) && (
<StyledErrorSection>
{adminPanelError ?? impersonateError}
</StyledErrorSection>
)}
</Section> </Section>
{shouldShowUserData && ( {isDefined(userLookupResult) && (
<Section> <Section>
<StyledUserInfo> <StyledUserInfo>
<H1Title title="User Info" fontColor={H1TitleFontColor.Primary} /> <H1Title title="User Info" fontColor={H1TitleFontColor.Primary} />
@ -180,10 +177,7 @@ export const SettingsAdminGeneral = () => {
/> />
</StyledTabListContainer> </StyledTabListContainer>
<StyledContentContainer> <StyledContentContainer>
<SettingsAdminWorkspaceContent <SettingsAdminWorkspaceContent activeWorkspace={activeWorkspace} />
activeWorkspace={activeWorkspace}
userId={userId}
/>
</StyledContentContainer> </StyledContentContainer>
</Section> </Section>
)} )}

View File

@ -1,19 +1,30 @@
import { Button, H2Title, IconUser, Toggle } from 'twenty-ui'; import { Button, H2Title, IconUser, Toggle } from 'twenty-ui';
import { currentUserState } from '@/auth/states/currentUserState';
import { canManageFeatureFlagsState } from '@/client-config/states/canManageFeatureFlagsState'; import { canManageFeatureFlagsState } from '@/client-config/states/canManageFeatureFlagsState';
import { useFeatureFlag } from '@/settings/admin-panel/hooks/useFeatureFlag'; import { useFeatureFlagState } from '@/settings/admin-panel/hooks/useFeatureFlagState';
import { useImpersonate } from '@/settings/admin-panel/hooks/useImpersonate'; 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 { WorkspaceInfo } from '@/settings/admin-panel/types/WorkspaceInfo'; import { WorkspaceInfo } from '@/settings/admin-panel/types/WorkspaceInfo';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { Table } from '@/ui/layout/table/components/Table'; import { Table } from '@/ui/layout/table/components/Table';
import { TableCell } from '@/ui/layout/table/components/TableCell'; import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableHeader } from '@/ui/layout/table/components/TableHeader'; import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow'; import { TableRow } from '@/ui/layout/table/components/TableRow';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil'; import { useState } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared';
import {
FeatureFlagKey,
useImpersonateMutation,
useUpdateWorkspaceFeatureFlagMutation,
} from '~/generated/graphql';
type SettingsAdminWorkspaceContentProps = { type SettingsAdminWorkspaceContentProps = {
activeWorkspace: WorkspaceInfo | undefined; activeWorkspace: WorkspaceInfo | undefined;
userId: string;
}; };
const StyledTable = styled(Table)` const StyledTable = styled(Table)`
@ -22,17 +33,85 @@ const StyledTable = styled(Table)`
export const SettingsAdminWorkspaceContent = ({ export const SettingsAdminWorkspaceContent = ({
activeWorkspace, activeWorkspace,
userId,
}: SettingsAdminWorkspaceContentProps) => { }: SettingsAdminWorkspaceContentProps) => {
const canManageFeatureFlags = useRecoilValue(canManageFeatureFlagsState); const canManageFeatureFlags = useRecoilValue(canManageFeatureFlagsState);
const [isImpersonateLoading, setIsImpersonationLoading] = useState(false);
const { enqueueSnackBar } = useSnackBar();
const [currentUser] = useRecoilState(currentUserState);
const { const { executeImpersonationAuth } = useImpersonationAuth();
handleImpersonate, const { executeImpersonationRedirect } = useImpersonationRedirect();
isLoading: isImpersonateLoading, const [updateFeatureFlag] = useUpdateWorkspaceFeatureFlagMutation();
canImpersonate, const [impersonate] = useImpersonateMutation();
} = useImpersonate();
const { handleFeatureFlagUpdate } = useFeatureFlag(); const { updateFeatureFlagState } = useFeatureFlagState();
const [userLookupResult, setUserLookupResult] = useRecoilState(
userLookupResultState,
);
const handleImpersonate = async (workspaceId: string) => {
if (!userLookupResult?.user.id) {
enqueueSnackBar('Please search for a user first', {
variant: SnackBarVariant.Error,
});
return;
}
setIsImpersonationLoading(true);
await impersonate({
variables: { userId: userLookupResult.user.id, workspaceId },
onCompleted: async (data) => {
const { loginToken, workspace } = data.impersonate;
const isCurrentWorkspace = workspace.id === activeWorkspace?.id;
setUserLookupResult(null);
if (isCurrentWorkspace) {
await executeImpersonationAuth(loginToken.token);
return;
}
return executeImpersonationRedirect(
workspace.workspaceUrls,
loginToken.token,
);
},
onError: (error) => {
enqueueSnackBar(`Failed to impersonate user. ${error.message}`, {
variant: SnackBarVariant.Error,
});
},
}).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);
}
enqueueSnackBar(`Failed to update feature flag. ${error.message}`, {
variant: SnackBarVariant.Error,
});
},
});
};
if (!activeWorkspace) return null; if (!activeWorkspace) return null;
@ -45,13 +124,13 @@ export const SettingsAdminWorkspaceContent = ({
}`} }`}
description={'Total Users'} description={'Total Users'}
/> />
{canImpersonate && ( {currentUser?.canImpersonate && (
<Button <Button
Icon={IconUser} Icon={IconUser}
variant="primary" variant="primary"
accent="blue" accent="blue"
title={'Impersonate'} title={'Impersonate'}
onClick={() => handleImpersonate(userId, activeWorkspace.id)} onClick={() => handleImpersonate(activeWorkspace.id)}
disabled={ disabled={
isImpersonateLoading || activeWorkspace.allowImpersonation === false isImpersonateLoading || activeWorkspace.allowImpersonation === false
} }

View File

@ -1,63 +0,0 @@
import { adminPanelErrorState } from '@/settings/admin-panel/states/adminPanelErrorState';
import { userLookupResultState } from '@/settings/admin-panel/states/userLookupResultState';
import { useRecoilState, useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared';
import {
FeatureFlagKey,
useUpdateWorkspaceFeatureFlagMutation,
} from '~/generated/graphql';
export const useFeatureFlag = () => {
const [userLookupResult, setUserLookupResult] = useRecoilState(
userLookupResultState,
);
const setError = useSetRecoilState(adminPanelErrorState);
const [updateFeatureFlag] = useUpdateWorkspaceFeatureFlagMutation();
const handleFeatureFlagUpdate = async (
workspaceId: string,
featureFlag: FeatureFlagKey,
value: boolean,
) => {
setError(null);
const previousState = userLookupResult;
if (isDefined(userLookupResult)) {
setUserLookupResult({
...userLookupResult,
workspaces: userLookupResult.workspaces.map((workspace) =>
workspace.id === workspaceId
? {
...workspace,
featureFlags: workspace.featureFlags.map((flag) =>
flag.key === featureFlag ? { ...flag, value } : flag,
),
}
: workspace,
),
});
}
const response = await updateFeatureFlag({
variables: {
workspaceId,
featureFlag,
value,
},
onError: (error) => {
if (isDefined(previousState)) {
setUserLookupResult(previousState);
}
setError(error.message);
},
});
return !!response.data;
};
return {
handleFeatureFlagUpdate,
};
};

View File

@ -0,0 +1,36 @@
import { userLookupResultState } from '@/settings/admin-panel/states/userLookupResultState';
import { useRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared';
import { FeatureFlagKey } from '~/generated/graphql';
export const useFeatureFlagState = () => {
const [userLookupResult, setUserLookupResult] = useRecoilState(
userLookupResultState,
);
const updateFeatureFlagState = (
workspaceId: string,
featureFlag: FeatureFlagKey,
value: boolean,
) => {
if (!isDefined(userLookupResult)) return;
setUserLookupResult({
...userLookupResult,
workspaces: userLookupResult.workspaces.map((workspace) =>
workspace.id === workspaceId
? {
...workspace,
featureFlags: workspace.featureFlags.map((flag) =>
flag.key === featureFlag ? { ...flag, value } : flag,
),
}
: workspace,
),
});
};
return {
updateFeatureFlagState,
};
};

View File

@ -1,78 +0,0 @@
import { useAuth } from '@/auth/hooks/useAuth';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
import { AppPath } from '@/types/AppPath';
import { useState } from 'react';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared';
import { useImpersonateMutation } from '~/generated/graphql';
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
export const useImpersonate = () => {
const [currentUser] = useRecoilState(currentUserState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const setIsAppWaitingForFreshObjectMetadata = useSetRecoilState(
isAppWaitingForFreshObjectMetadataState,
);
const { getAuthTokensFromLoginToken } = useAuth();
const [impersonate] = useImpersonateMutation();
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleImpersonate = async (userId: string, workspaceId: string) => {
if (!userId.trim()) {
setError('Please enter a user ID');
return;
}
setIsLoading(true);
setError(null);
try {
const impersonateResult = await impersonate({
variables: { userId, workspaceId },
});
if (isDefined(impersonateResult.errors)) {
throw impersonateResult.errors;
}
if (!impersonateResult.data?.impersonate) {
throw new Error('No impersonate result');
}
const { loginToken, workspace } = impersonateResult.data.impersonate;
if (workspace.id === currentWorkspace?.id) {
setIsAppWaitingForFreshObjectMetadata(true);
await getAuthTokensFromLoginToken(loginToken.token);
setIsAppWaitingForFreshObjectMetadata(false);
return;
}
return redirectToWorkspaceDomain(
getWorkspaceUrl(workspace.workspaceUrls),
AppPath.Verify,
{
loginToken: loginToken.token,
},
);
} catch (error) {
setError('Failed to impersonate user. Please try again.');
setIsLoading(false);
}
};
return {
handleImpersonate,
isLoading,
error,
canImpersonate: currentUser?.canImpersonate,
};
};

View File

@ -0,0 +1,18 @@
import { useAuth } from '@/auth/hooks/useAuth';
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
import { useSetRecoilState } from 'recoil';
export const useImpersonationAuth = () => {
const { getAuthTokensFromLoginToken } = useAuth();
const setIsAppWaitingForFreshObjectMetadata = useSetRecoilState(
isAppWaitingForFreshObjectMetadataState,
);
const executeImpersonationAuth = async (loginToken: string) => {
setIsAppWaitingForFreshObjectMetadata(true);
await getAuthTokensFromLoginToken(loginToken);
setIsAppWaitingForFreshObjectMetadata(false);
};
return { executeImpersonationAuth };
};

View File

@ -0,0 +1,21 @@
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
import { AppPath } from '@/types/AppPath';
import { WorkspaceUrls } from '~/generated/graphql';
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
export const useImpersonationRedirect = () => {
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
const executeImpersonationRedirect = (
workspaceUrls: WorkspaceUrls,
loginToken: string,
) => {
return redirectToWorkspaceDomain(
getWorkspaceUrl(workspaceUrls),
AppPath.Verify,
{ loginToken },
);
};
return { executeImpersonationRedirect };
};

View File

@ -1,42 +0,0 @@
import { adminPanelErrorState } from '@/settings/admin-panel/states/adminPanelErrorState';
import { userLookupResultState } from '@/settings/admin-panel/states/userLookupResultState';
import { useState } from 'react';
import { useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared';
import { useUserLookupAdminPanelMutation } from '~/generated/graphql';
export const useUserLookup = () => {
const setUserLookupResult = useSetRecoilState(userLookupResultState);
const setError = useSetRecoilState(adminPanelErrorState);
const [isLoading, setIsLoading] = useState(false);
const [userLookup] = useUserLookupAdminPanelMutation({
onCompleted: (data) => {
setIsLoading(false);
if (isDefined(data?.userLookupAdminPanel)) {
setUserLookupResult(data.userLookupAdminPanel);
}
},
onError: (error) => {
setIsLoading(false);
setError(error.message);
},
});
const handleUserLookup = async (userIdentifier: string) => {
setError(null);
setIsLoading(true);
setUserLookupResult(null);
const response = await userLookup({
variables: { userIdentifier },
});
return response.data?.userLookupAdminPanel;
};
return {
handleUserLookup,
isLoading,
};
};

View File

@ -1,6 +0,0 @@
import { atom } from 'recoil';
export const adminPanelErrorState = atom<string | null>({
key: 'adminPanelErrorState',
default: null,
});

View File

@ -19,7 +19,7 @@ export const ENVIRONMENT_VARIABLES_GROUP_METADATA: Record<
position: 200, position: 200,
description: description:
'We use this to limit the number of requests to the server. This is useful to prevent abuse.', 'We use this to limit the number of requests to the server. This is useful to prevent abuse.',
isHiddenOnLoad: false, isHiddenOnLoad: true,
}, },
[EnvironmentVariablesGroup.StorageConfig]: { [EnvironmentVariablesGroup.StorageConfig]: {
position: 300, position: 300,
@ -46,13 +46,13 @@ export const ENVIRONMENT_VARIABLES_GROUP_METADATA: Record<
[EnvironmentVariablesGroup.Logging]: { [EnvironmentVariablesGroup.Logging]: {
position: 700, position: 700,
description: '', description: '',
isHiddenOnLoad: false, isHiddenOnLoad: true,
}, },
[EnvironmentVariablesGroup.ExceptionHandler]: { [EnvironmentVariablesGroup.ExceptionHandler]: {
position: 800, position: 800,
description: description:
'By default, exceptions are sent to the logs. This should be enough for most self-hosting use-cases. For our cloud app we use Sentry.', 'By default, exceptions are sent to the logs. This should be enough for most self-hosting use-cases. For our cloud app we use Sentry.',
isHiddenOnLoad: false, isHiddenOnLoad: true,
}, },
[EnvironmentVariablesGroup.Other]: { [EnvironmentVariablesGroup.Other]: {
position: 900, position: 900,