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

@ -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,
};
};