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:
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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 };
|
||||
};
|
||||
@ -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 };
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user