Add settingsPermission gate on the frontend (#10179)

## Context
With the new permissions system, we now need to hide some items from the
settings navigation and gate some routes so they can't be accessed
directly.
To avoid having to set permission gates in all the component pages, I'm
introducing wrapper at the route level and in the Navigation. This is
not required and is mostly for pages that are strictly mapped to a
single permission, for the rest we still need to use the different hooks
manually but it should avoid a bit of boilerplate for most of the cases.

- currentUserWorkspaceState to access settingsPermissions
- SettingsProtectedRouteWrapper in the router that can take a
settingFeature or a featureFlag as a gate logic, if the currentUser does
not have access to the settingFeature or the featureFlag is not enabled
they will be redirected to the profile page.
- SettingsNavigationItemWrapper & SettingsNavigationSectionWrapper. The
former will check the same logic as SettingsProtectedRouteWrapper and
not display the item if needed. The later will check if all
SettingsNavigationItemWrapper are not visible and hide itself if that's
the case.
- useHasSettingsPermission to get a specific permission state for the
current user
- useSettingsPermissionMap to get a map of all permissions with their
values for the current user
- useFeatureFlagsMap same but for featureFlags
This commit is contained in:
Weiko
2025-02-18 15:50:23 +01:00
committed by GitHub
parent 755d1786fc
commit 2fca60436b
20 changed files with 660 additions and 318 deletions

View File

@ -15,6 +15,7 @@ import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation'; import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
import { useUpdateEffect } from '~/hooks/useUpdateEffect'; import { useUpdateEffect } from '~/hooks/useUpdateEffect';
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { ApolloFactory, Options } from '../services/apollo.factory'; import { ApolloFactory, Options } from '../services/apollo.factory';
@ -34,6 +35,7 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
const setCurrentWorkspaceMember = useSetRecoilState( const setCurrentWorkspaceMember = useSetRecoilState(
currentWorkspaceMemberState, currentWorkspaceMemberState,
); );
const setCurrentUserWorkspace = useSetRecoilState(currentUserWorkspaceState);
const setWorkspaces = useSetRecoilState(workspacesState); const setWorkspaces = useSetRecoilState(workspacesState);
const [, setPreviousUrl] = useRecoilState(previousUrlState); const [, setPreviousUrl] = useRecoilState(previousUrlState);
@ -71,6 +73,7 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
setCurrentUser(null); setCurrentUser(null);
setCurrentWorkspaceMember(null); setCurrentWorkspaceMember(null);
setCurrentWorkspace(null); setCurrentWorkspace(null);
setCurrentUserWorkspace(null);
setWorkspaces([]); setWorkspaces([]);
if ( if (
!isMatchingLocation(AppPath.Verify) && !isMatchingLocation(AppPath.Verify) &&

View File

@ -1,35 +1,19 @@
import { useCreateAppRouter } from '@/app/hooks/useCreateAppRouter'; import { useCreateAppRouter } from '@/app/hooks/useCreateAppRouter';
import { currentUserState } from '@/auth/states/currentUserState'; import { currentUserState } from '@/auth/states/currentUserState';
import { billingState } from '@/client-config/states/billingState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { RouterProvider } from 'react-router-dom'; import { RouterProvider } from 'react-router-dom';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
export const AppRouter = () => { export const AppRouter = () => {
const billing = useRecoilValue(billingState);
// We want to disable serverless function settings but keep the code for now // We want to disable serverless function settings but keep the code for now
const isFunctionSettingsEnabled = false; const isFunctionSettingsEnabled = false;
const isBillingPageEnabled = billing?.isBillingEnabled;
const currentUser = useRecoilValue(currentUserState); const currentUser = useRecoilValue(currentUserState);
const isAdminPageEnabled = currentUser?.canImpersonate; const isAdminPageEnabled = currentUser?.canImpersonate;
const isPermissionsEnabled = useIsFeatureEnabled(
FeatureFlagKey.IsPermissionsEnabled,
);
return ( return (
<RouterProvider <RouterProvider
router={useCreateAppRouter( router={useCreateAppRouter(isFunctionSettingsEnabled, isAdminPageEnabled)}
isBillingPageEnabled,
isFunctionSettingsEnabled,
isAdminPageEnabled,
isPermissionsEnabled,
)}
/> />
); );
}; };

View File

@ -1,8 +1,11 @@
import { lazy, Suspense } from 'react'; import { lazy, Suspense } from 'react';
import { Route, Routes } from 'react-router-dom'; import { Route, Routes } from 'react-router-dom';
import { SettingsProtectedRouteWrapper } from '@/settings/components/SettingsProtectedRouteWrapper';
import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader'; import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader';
import { SettingsPath } from '@/types/SettingsPath'; import { SettingsPath } from '@/types/SettingsPath';
import { SettingsFeatures } from 'twenty-shared';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
const SettingsAccountsCalendars = lazy(() => const SettingsAccountsCalendars = lazy(() =>
import('~/pages/settings/accounts/SettingsAccountsCalendars').then( import('~/pages/settings/accounts/SettingsAccountsCalendars').then(
@ -264,17 +267,13 @@ const SettingsRoleEdit = lazy(() =>
); );
type SettingsRoutesProps = { type SettingsRoutesProps = {
isBillingEnabled?: boolean;
isFunctionSettingsEnabled?: boolean; isFunctionSettingsEnabled?: boolean;
isAdminPageEnabled?: boolean; isAdminPageEnabled?: boolean;
isPermissionsEnabled?: boolean;
}; };
export const SettingsRoutes = ({ export const SettingsRoutes = ({
isBillingEnabled,
isFunctionSettingsEnabled, isFunctionSettingsEnabled,
isAdminPageEnabled, isAdminPageEnabled,
isPermissionsEnabled,
}: SettingsRoutesProps) => ( }: SettingsRoutesProps) => (
<Suspense fallback={<SettingsSkeletonLoader />}> <Suspense fallback={<SettingsSkeletonLoader />}>
<Routes> <Routes>
@ -290,35 +289,50 @@ export const SettingsRoutes = ({
path={SettingsPath.AccountsEmails} path={SettingsPath.AccountsEmails}
element={<SettingsAccountsEmails />} element={<SettingsAccountsEmails />}
/> />
{isBillingEnabled && ( <Route
element={
<SettingsProtectedRouteWrapper
requiredFeatureFlag={FeatureFlagKey.IsBillingPlansEnabled}
/>
}
>
<Route path={SettingsPath.Billing} element={<SettingsBilling />} /> <Route path={SettingsPath.Billing} element={<SettingsBilling />} />
)} </Route>
<Route path={SettingsPath.Workspace} element={<SettingsWorkspace />} /> <Route path={SettingsPath.Workspace} element={<SettingsWorkspace />} />
<Route path={SettingsPath.Domain} element={<SettingsDomain />} /> <Route path={SettingsPath.Domain} element={<SettingsDomain />} />
<Route <Route
path={SettingsPath.WorkspaceMembersPage} path={SettingsPath.WorkspaceMembersPage}
element={<SettingsWorkspaceMembers />} element={<SettingsWorkspaceMembers />}
/> />
<Route path={SettingsPath.Workspace} element={<SettingsWorkspace />} />
<Route path={SettingsPath.Objects} element={<SettingsObjects />} />
<Route <Route
path={SettingsPath.ObjectOverview} element={
element={<SettingsObjectOverview />} <SettingsProtectedRouteWrapper
/> settingsPermission={SettingsFeatures.DATA_MODEL}
<Route
path={SettingsPath.ObjectDetail}
element={<SettingsObjectDetailPage />}
/>
<Route path={SettingsPath.NewObject} element={<SettingsNewObject />} />
{isPermissionsEnabled && (
<>
<Route path={SettingsPath.Roles} element={<SettingsRoles />} />
<Route
path={SettingsPath.RoleDetail}
element={<SettingsRoleEdit />}
/> />
</> }
)} >
<Route path={SettingsPath.Objects} element={<SettingsObjects />} />
<Route
path={SettingsPath.ObjectOverview}
element={<SettingsObjectOverview />}
/>
<Route
path={SettingsPath.ObjectDetail}
element={<SettingsObjectDetailPage />}
/>
<Route path={SettingsPath.NewObject} element={<SettingsNewObject />} />
</Route>
<Route
element={
<SettingsProtectedRouteWrapper
settingsPermission={SettingsFeatures.ROLES}
requiredFeatureFlag={FeatureFlagKey.IsPermissionsEnabled}
/>
}
>
<Route path={SettingsPath.Roles} element={<SettingsRoles />} />
<Route path={SettingsPath.RoleDetail} element={<SettingsRoleEdit />} />
</Route>
<Route path={SettingsPath.Developers} element={<SettingsDevelopers />} /> <Route path={SettingsPath.Developers} element={<SettingsDevelopers />} />
<Route <Route
path={SettingsPath.DevelopersNewApiKey} path={SettingsPath.DevelopersNewApiKey}

View File

@ -26,10 +26,8 @@ import { PaymentSuccess } from '~/pages/onboarding/PaymentSuccess';
import { SyncEmails } from '~/pages/onboarding/SyncEmails'; import { SyncEmails } from '~/pages/onboarding/SyncEmails';
export const useCreateAppRouter = ( export const useCreateAppRouter = (
isBillingEnabled?: boolean,
isFunctionSettingsEnabled?: boolean, isFunctionSettingsEnabled?: boolean,
isAdminPageEnabled?: boolean, isAdminPageEnabled?: boolean,
isPermissionsEnabled?: boolean,
) => ) =>
createBrowserRouter( createBrowserRouter(
createRoutesFromElements( createRoutesFromElements(
@ -61,10 +59,8 @@ export const useCreateAppRouter = (
path={AppPath.SettingsCatchAll} path={AppPath.SettingsCatchAll}
element={ element={
<SettingsRoutes <SettingsRoutes
isBillingEnabled={isBillingEnabled}
isFunctionSettingsEnabled={isFunctionSettingsEnabled} isFunctionSettingsEnabled={isFunctionSettingsEnabled}
isAdminPageEnabled={isAdminPageEnabled} isAdminPageEnabled={isAdminPageEnabled}
isPermissionsEnabled={isPermissionsEnabled}
/> />
} }
/> />

View File

@ -44,6 +44,7 @@ import { getTimeFormatFromWorkspaceTimeFormat } from '@/localization/utils/getTi
import { currentUserState } from '../states/currentUserState'; import { currentUserState } from '../states/currentUserState';
import { tokenPairState } from '../states/tokenPairState'; import { tokenPairState } from '../states/tokenPairState';
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { import {
SignInUpStep, SignInUpStep,
signInUpStepState, signInUpStepState,
@ -71,6 +72,7 @@ export const useAuth = () => {
const setCurrentWorkspaceMember = useSetRecoilState( const setCurrentWorkspaceMember = useSetRecoilState(
currentWorkspaceMemberState, currentWorkspaceMemberState,
); );
const setCurrentUserWorkspace = useSetRecoilState(currentUserWorkspaceState);
const setIsAppWaitingForFreshObjectMetadataState = useSetRecoilState( const setIsAppWaitingForFreshObjectMetadataState = useSetRecoilState(
isAppWaitingForFreshObjectMetadataState, isAppWaitingForFreshObjectMetadataState,
); );
@ -255,6 +257,10 @@ export const useAuth = () => {
setCurrentWorkspaceMembers(workspaceMembers); setCurrentWorkspaceMembers(workspaceMembers);
} }
if (isDefined(user.currentUserWorkspace)) {
setCurrentUserWorkspace(user.currentUserWorkspace);
}
if (isDefined(user.workspaceMember)) { if (isDefined(user.workspaceMember)) {
workspaceMember = { workspaceMember = {
...user.workspaceMember, ...user.workspaceMember,
@ -318,6 +324,7 @@ export const useAuth = () => {
getCurrentUser, getCurrentUser,
isOnAWorkspace, isOnAWorkspace,
setCurrentUser, setCurrentUser,
setCurrentUserWorkspace,
setCurrentWorkspace, setCurrentWorkspace,
setCurrentWorkspaceMember, setCurrentWorkspaceMember,
setCurrentWorkspaceMembers, setCurrentWorkspaceMembers,

View File

@ -0,0 +1,10 @@
import { createState } from '@ui/utilities/state/utils/createState';
import { UserWorkspace } from '~/generated/graphql';
export type CurrentUserWorkspace = Pick<UserWorkspace, 'settingsPermissions'>;
export const currentUserWorkspaceState =
createState<CurrentUserWorkspace | null>({
key: 'currentUserWorkspaceState',
defaultValue: null,
});

View File

@ -1,48 +1,57 @@
import { useMatch, useResolvedPath } from 'react-router-dom'; import { useMatch, useResolvedPath } from 'react-router-dom';
import { SettingsPath } from '@/types/SettingsPath'; import { AdvancedSettingsWrapper } from '@/settings/components/AdvancedSettingsWrapper';
import { import { SettingsNavigationItem } from '@/settings/hooks/useSettingsNavigationItems';
NavigationDrawerItem, import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
NavigationDrawerItemProps,
} from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { NavigationDrawerSubItemState } from '@/ui/navigation/navigation-drawer/types/NavigationDrawerSubItemState'; import { NavigationDrawerSubItemState } from '@/ui/navigation/navigation-drawer/types/NavigationDrawerSubItemState';
import { isDefined } from 'twenty-shared';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
type SettingsNavigationDrawerItemProps = Pick< type SettingsNavigationDrawerItemProps = {
NavigationDrawerItemProps, item: SettingsNavigationItem;
'Icon' | 'label' | 'indentationLevel' | 'soon'
> & {
matchSubPages?: boolean;
path: SettingsPath;
subItemState?: NavigationDrawerSubItemState; subItemState?: NavigationDrawerSubItemState;
}; };
export const SettingsNavigationDrawerItem = ({ export const SettingsNavigationDrawerItem = ({
Icon, item,
label,
indentationLevel,
matchSubPages = true,
path,
soon,
subItemState, subItemState,
}: SettingsNavigationDrawerItemProps) => { }: SettingsNavigationDrawerItemProps) => {
const href = getSettingsPath(path); const href = getSettingsPath(item.path);
const pathName = useResolvedPath(href).pathname; const pathName = useResolvedPath(href).pathname;
const isActive = !!useMatch({ const isActive = !!useMatch({
path: pathName, path: pathName,
end: !matchSubPages, end: !item.matchSubPages,
}); });
if (isDefined(item.isHidden) && item.isHidden) {
return null;
}
if (isDefined(item.isAdvanced) && item.isAdvanced) {
return (
<AdvancedSettingsWrapper navigationDrawerItem>
<NavigationDrawerItem
indentationLevel={item.indentationLevel}
subItemState={subItemState}
label={item.label}
to={href}
Icon={item.Icon}
active={isActive}
soon={item.soon}
/>
</AdvancedSettingsWrapper>
);
}
return ( return (
<NavigationDrawerItem <NavigationDrawerItem
indentationLevel={indentationLevel} indentationLevel={item.indentationLevel}
subItemState={subItemState} subItemState={subItemState}
label={label} label={item.label}
to={href} to={href}
Icon={Icon} Icon={item.Icon}
active={isActive} active={isActive}
soon={soon} soon={item.soon}
/> />
); );
}; };

View File

@ -1,229 +1,121 @@
import { useRecoilValue } from 'recoil'; import { IconDoorEnter } from 'twenty-ui';
import {
IconApps,
IconAt,
IconCalendarEvent,
IconCode,
IconColorSwatch,
IconComponent,
IconCurrencyDollar,
IconDoorEnter,
IconFlask,
IconFunction,
IconHierarchy2,
IconKey,
IconLock,
IconMail,
IconRocket,
IconServer,
IconSettings,
IconUserCircle,
IconUsers,
} from 'twenty-ui';
import { useAuth } from '@/auth/hooks/useAuth'; import { useAuth } from '@/auth/hooks/useAuth';
import { currentUserState } from '@/auth/states/currentUserState';
import { billingState } from '@/client-config/states/billingState';
import { labPublicFeatureFlagsState } from '@/client-config/states/labPublicFeatureFlagsState';
import { AdvancedSettingsWrapper } from '@/settings/components/AdvancedSettingsWrapper'; import { AdvancedSettingsWrapper } from '@/settings/components/AdvancedSettingsWrapper';
import { SettingsNavigationDrawerItem } from '@/settings/components/SettingsNavigationDrawerItem'; import { SettingsNavigationDrawerItem } from '@/settings/components/SettingsNavigationDrawerItem';
import { SettingsPath } from '@/types/SettingsPath';
import { import {
NavigationDrawerItem, SettingsNavigationItem,
NavigationDrawerItemIndentationLevel, SettingsNavigationSection,
} from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem'; useSettingsNavigationItems,
} from '@/settings/hooks/useSettingsNavigationItems';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { NavigationDrawerItemGroup } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemGroup'; import { NavigationDrawerItemGroup } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemGroup';
import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection'; import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle'; import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle';
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment'; import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useLingui } from '@lingui/react/macro'; import { useLingui } from '@lingui/react/macro';
import { matchPath, resolvePath, useLocation } from 'react-router-dom'; import { matchPath, resolvePath, useLocation } from 'react-router-dom';
import { FeatureFlagKey } from '~/generated/graphql';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
type SettingsNavigationItem = {
label: string;
path: SettingsPath;
Icon: IconComponent;
indentationLevel?: NavigationDrawerItemIndentationLevel;
matchSubPages?: boolean;
};
export const SettingsNavigationDrawerItems = () => { export const SettingsNavigationDrawerItems = () => {
const { signOut } = useAuth(); const { signOut } = useAuth();
const { t } = useLingui(); const { t } = useLingui();
const billing = useRecoilValue(billingState); const settingsNavigationItems: SettingsNavigationSection[] =
const isPermissionsEnabled = useIsFeatureEnabled( useSettingsNavigationItems();
FeatureFlagKey.IsPermissionsEnabled,
);
// We want to disable this serverless function setting menu but keep the code
// for now
const isFunctionSettingsEnabled = false;
const isBillingPageEnabled = billing?.isBillingEnabled;
const currentUser = useRecoilValue(currentUserState);
const isAdminPageEnabled = currentUser?.canImpersonate;
const labPublicFeatureFlags = useRecoilValue(labPublicFeatureFlagsState);
// TODO: Refactor this part to only have arrays of navigation items
const currentPathName = useLocation().pathname; const currentPathName = useLocation().pathname;
const accountSubSettings: SettingsNavigationItem[] = [ const getSelectedIndexForSubItems = (subItems: SettingsNavigationItem[]) => {
{ return subItems.findIndex((subItem) => {
label: t`Emails`, const href = getSettingsPath(subItem.path);
path: SettingsPath.AccountsEmails, const pathName = resolvePath(href).pathname;
Icon: IconMail,
indentationLevel: 2,
},
{
label: t`Calendars`,
path: SettingsPath.AccountsCalendars,
Icon: IconCalendarEvent,
indentationLevel: 2,
},
];
const selectedIndex = accountSubSettings.findIndex((accountSubSetting) => { return matchPath(
const href = getSettingsPath(accountSubSetting.path); {
const pathName = resolvePath(href).pathname; path: pathName,
end: subItem.matchSubPages === false,
return matchPath( },
{ currentPathName,
path: pathName, );
end: accountSubSetting.matchSubPages === false, });
}, };
currentPathName,
);
});
return ( return (
<> <>
<NavigationDrawerSection> {settingsNavigationItems.map((section) => {
<NavigationDrawerSectionTitle label={t`User`} /> const allItemsHidden = section.items.every((item) => item.isHidden);
<SettingsNavigationDrawerItem if (allItemsHidden) {
label={t`Profile`} return null;
path={SettingsPath.ProfilePage} }
Icon={IconUserCircle}
/>
<SettingsNavigationDrawerItem
label={t`Experience`}
path={SettingsPath.Experience}
Icon={IconColorSwatch}
/>
<NavigationDrawerItemGroup>
<SettingsNavigationDrawerItem
label={t`Accounts`}
path={SettingsPath.Accounts}
Icon={IconAt}
matchSubPages={false}
/>
{accountSubSettings.map((navigationItem, index) => (
<SettingsNavigationDrawerItem
key={index}
label={navigationItem.label}
path={navigationItem.path}
Icon={navigationItem.Icon}
indentationLevel={navigationItem.indentationLevel}
subItemState={getNavigationSubItemLeftAdornment({
arrayLength: accountSubSettings.length,
index,
selectedIndex,
})}
/>
))}
</NavigationDrawerItemGroup>
</NavigationDrawerSection>
<NavigationDrawerSection>
<NavigationDrawerSectionTitle label={t`Workspace`} />
<SettingsNavigationDrawerItem
label={t`General`}
path={SettingsPath.Workspace}
Icon={IconSettings}
/>
<SettingsNavigationDrawerItem
label={t`Members`}
path={SettingsPath.WorkspaceMembersPage}
Icon={IconUsers}
/>
{isBillingPageEnabled && (
<SettingsNavigationDrawerItem
label={t`Billing`}
path={SettingsPath.Billing}
Icon={IconCurrencyDollar}
/>
)}
{isPermissionsEnabled && (
<SettingsNavigationDrawerItem
label={t`Roles`}
path={SettingsPath.Roles}
Icon={IconLock}
/>
)}
<SettingsNavigationDrawerItem
label={t`Data model`}
path={SettingsPath.Objects}
Icon={IconHierarchy2}
/>
<SettingsNavigationDrawerItem
label={t`Integrations`}
path={SettingsPath.Integrations}
Icon={IconApps}
/>
<AdvancedSettingsWrapper navigationDrawerItem={true}>
<SettingsNavigationDrawerItem
label={t`Security`}
path={SettingsPath.Security}
Icon={IconKey}
/>
</AdvancedSettingsWrapper>
</NavigationDrawerSection>
return (
<NavigationDrawerSection key={section.label}>
{section.isAdvanced ? (
<AdvancedSettingsWrapper hideIcon>
<NavigationDrawerSectionTitle label={section.label} />
</AdvancedSettingsWrapper>
) : (
<NavigationDrawerSectionTitle label={section.label} />
)}
{section.items.map((item, index) => {
const subItems = item.subItems;
if (Array.isArray(subItems) && subItems.length > 0) {
const selectedSubItemIndex =
getSelectedIndexForSubItems(subItems);
return (
<NavigationDrawerItemGroup key={item.path}>
<SettingsNavigationDrawerItem
item={item}
subItemState={
item.indentationLevel
? getNavigationSubItemLeftAdornment({
arrayLength: section.items.length,
index,
selectedIndex: selectedSubItemIndex,
})
: undefined
}
/>
{subItems.map((subItem, subIndex) => (
<SettingsNavigationDrawerItem
key={subItem.path}
item={subItem}
subItemState={
subItem.indentationLevel
? getNavigationSubItemLeftAdornment({
arrayLength: subItems.length,
index: subIndex,
selectedIndex: selectedSubItemIndex,
})
: undefined
}
/>
))}
</NavigationDrawerItemGroup>
);
}
return (
<SettingsNavigationDrawerItem
key={item.path}
item={item}
subItemState={
item.indentationLevel
? getNavigationSubItemLeftAdornment({
arrayLength: section.items.length,
index,
selectedIndex: index,
})
: undefined
}
/>
);
})}
</NavigationDrawerSection>
);
})}
<NavigationDrawerSection> <NavigationDrawerSection>
<AdvancedSettingsWrapper hideIcon>
<NavigationDrawerSectionTitle label={t`Developers`} />
</AdvancedSettingsWrapper>
<AdvancedSettingsWrapper navigationDrawerItem={true}>
<SettingsNavigationDrawerItem
label={t`API & Webhooks`}
path={SettingsPath.Developers}
Icon={IconCode}
/>
</AdvancedSettingsWrapper>
{isFunctionSettingsEnabled && (
<AdvancedSettingsWrapper navigationDrawerItem={true}>
<SettingsNavigationDrawerItem
label={t`Functions`}
path={SettingsPath.ServerlessFunctions}
Icon={IconFunction}
/>
</AdvancedSettingsWrapper>
)}
</NavigationDrawerSection>
<NavigationDrawerSection>
<NavigationDrawerSectionTitle label={t`Other`} />
{isAdminPageEnabled && (
<SettingsNavigationDrawerItem
label={t`Server Admin`}
path={SettingsPath.AdminPanel}
Icon={IconServer}
/>
)}
{labPublicFeatureFlags?.length > 0 && (
<SettingsNavigationDrawerItem
label={t`Lab`}
path={SettingsPath.Lab}
Icon={IconFlask}
/>
)}
<SettingsNavigationDrawerItem
label={t`Releases`}
path={SettingsPath.Releases}
Icon={IconRocket}
/>
<NavigationDrawerItem <NavigationDrawerItem
label={t`Logout`} label={t`Logout`}
onClick={signOut} onClick={signOut}

View File

@ -0,0 +1,36 @@
import { useHasSettingsPermission } from '@/settings/roles/hooks/useHasSettingsPermission';
import { SettingsPath } from '@/types/SettingsPath';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { ReactNode } from 'react';
import { Navigate, Outlet } from 'react-router-dom';
import { FeatureFlagKey, SettingsFeatures } from '~/generated/graphql';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
type SettingsProtectedRouteWrapperProps = {
children?: ReactNode;
settingsPermission?: SettingsFeatures;
requiredFeatureFlag?: FeatureFlagKey;
};
export const SettingsProtectedRouteWrapper = ({
children,
settingsPermission,
requiredFeatureFlag,
}: SettingsProtectedRouteWrapperProps) => {
const isPermissionsEnabled = useIsFeatureEnabled(
FeatureFlagKey.IsPermissionsEnabled,
);
const hasPermission = useHasSettingsPermission(settingsPermission);
const requiredFeatureFlagEnabled = useIsFeatureEnabled(
requiredFeatureFlag || null,
);
if (
(requiredFeatureFlag && !requiredFeatureFlagEnabled) ||
(!hasPermission && isPermissionsEnabled)
) {
return <Navigate to={getSettingsPath(SettingsPath.ProfilePage)} replace />;
}
return children ?? <Outlet />;
};

View File

@ -0,0 +1,110 @@
import { useSettingsNavigationItems } from '@/settings/hooks/useSettingsNavigationItems';
import { MockedProvider } from '@apollo/client/testing';
import { renderHook } from '@testing-library/react';
import { ReactNode } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { MutableSnapshot, RecoilRoot } from 'recoil';
import { SettingsFeatures } from 'twenty-shared';
import { Billing, FeatureFlagKey, OnboardingStatus } from '~/generated/graphql';
import { currentUserState } from '@/auth/states/currentUserState';
import { billingState } from '@/client-config/states/billingState';
import { labPublicFeatureFlagsState } from '@/client-config/states/labPublicFeatureFlagsState';
import { useSettingsPermissionMap } from '@/settings/roles/hooks/useSettingsPermissionMap';
const mockCurrentUser = {
id: 'fake-user-id',
email: 'fake@email.com',
supportUserHash: null,
analyticsTinybirdJwts: null,
canImpersonate: false,
onboardingStatus: OnboardingStatus.COMPLETED,
userVars: {},
};
const mockBilling: Billing = {
isBillingEnabled: false,
billingUrl: '',
trialPeriods: [],
__typename: 'Billing',
};
const initializeState = ({ set }: MutableSnapshot) => {
set(currentUserState, mockCurrentUser);
set(billingState, mockBilling);
set(labPublicFeatureFlagsState, []);
};
const Wrapper = ({ children }: { children: ReactNode }) => (
<MockedProvider>
<RecoilRoot initializeState={initializeState}>
<MemoryRouter>{children}</MemoryRouter>
</RecoilRoot>
</MockedProvider>
);
jest.mock('@/settings/roles/hooks/useSettingsPermissionMap', () => ({
useSettingsPermissionMap: jest.fn(),
}));
jest.mock('@/workspace/hooks/useFeatureFlagsMap', () => ({
useFeatureFlagsMap: () => ({
[FeatureFlagKey.IsPermissionsEnabled]: true,
}),
}));
describe('useSettingsNavigationItems', () => {
it('should hide workspace settings when no permissions', () => {
(useSettingsPermissionMap as jest.Mock).mockImplementation(() => ({
[SettingsFeatures.WORKSPACE]: false,
[SettingsFeatures.WORKSPACE_USERS]: false,
[SettingsFeatures.DATA_MODEL]: false,
[SettingsFeatures.API_KEYS_AND_WEBHOOKS]: false,
[SettingsFeatures.ROLES]: false,
[SettingsFeatures.SECURITY]: false,
}));
const { result } = renderHook(() => useSettingsNavigationItems(), {
wrapper: Wrapper,
});
const workspaceSection = result.current.find(
(section) => section.label === 'Workspace',
);
expect(workspaceSection?.items.every((item) => item.isHidden)).toBe(true);
});
it('should show workspace settings when has permissions', () => {
(useSettingsPermissionMap as jest.Mock).mockImplementation(() => ({
[SettingsFeatures.WORKSPACE]: true,
[SettingsFeatures.WORKSPACE_USERS]: true,
[SettingsFeatures.DATA_MODEL]: true,
[SettingsFeatures.API_KEYS_AND_WEBHOOKS]: true,
[SettingsFeatures.ROLES]: true,
[SettingsFeatures.SECURITY]: true,
}));
const { result } = renderHook(() => useSettingsNavigationItems(), {
wrapper: Wrapper,
});
const workspaceSection = result.current.find(
(section) => section.label === 'Workspace',
);
expect(workspaceSection?.items.some((item) => !item.isHidden)).toBe(true);
});
it('should show user section items regardless of permissions', () => {
const { result } = renderHook(() => useSettingsNavigationItems(), {
wrapper: Wrapper,
});
const userSection = result.current.find(
(section) => section.label === 'User',
);
expect(userSection?.items.length).toBeGreaterThan(0);
expect(userSection?.items.every((item) => !item.isHidden)).toBe(true);
});
});

View File

@ -0,0 +1,194 @@
import {
IconApps,
IconAt,
IconCalendarEvent,
IconCode,
IconColorSwatch,
IconComponent,
IconCurrencyDollar,
IconFlask,
IconFunction,
IconHierarchy2,
IconKey,
IconLock,
IconMail,
IconRocket,
IconServer,
IconSettings,
IconUserCircle,
IconUsers,
} from 'twenty-ui';
import { SettingsPath } from '@/types/SettingsPath';
import { SettingsFeatures } from 'twenty-shared';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
import { currentUserState } from '@/auth/states/currentUserState';
import { billingState } from '@/client-config/states/billingState';
import { labPublicFeatureFlagsState } from '@/client-config/states/labPublicFeatureFlagsState';
import { useSettingsPermissionMap } from '@/settings/roles/hooks/useSettingsPermissionMap';
import { NavigationDrawerItemIndentationLevel } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { t } from '@lingui/core/macro';
import { useRecoilValue } from 'recoil';
export type SettingsNavigationSection = {
label: string;
items: SettingsNavigationItem[];
isAdvanced?: boolean;
};
export type SettingsNavigationItem = {
label: string;
path: SettingsPath;
Icon: IconComponent;
indentationLevel?: NavigationDrawerItemIndentationLevel;
matchSubPages?: boolean;
isHidden?: boolean;
subItems?: SettingsNavigationItem[];
isAdvanced?: boolean;
soon?: boolean;
};
export const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
const billing = useRecoilValue(billingState);
const isFunctionSettingsEnabled = false;
const isBillingEnabled = billing?.isBillingEnabled ?? false;
const currentUser = useRecoilValue(currentUserState);
const isAdminEnabled = currentUser?.canImpersonate ?? false;
const labPublicFeatureFlags = useRecoilValue(labPublicFeatureFlagsState);
const featureFlags = useFeatureFlagsMap();
const permissionMap = useSettingsPermissionMap();
return [
{
label: t`User`,
items: [
{
label: t`Profile`,
path: SettingsPath.ProfilePage,
Icon: IconUserCircle,
},
{
label: t`Experience`,
path: SettingsPath.Experience,
Icon: IconColorSwatch,
},
{
label: t`Accounts`,
path: SettingsPath.Accounts,
Icon: IconAt,
matchSubPages: false,
subItems: [
{
label: t`Emails`,
path: SettingsPath.AccountsEmails,
Icon: IconMail,
indentationLevel: 2,
},
{
label: t`Calendars`,
path: SettingsPath.AccountsCalendars,
Icon: IconCalendarEvent,
indentationLevel: 2,
},
],
},
],
},
{
label: t`Workspace`,
items: [
{
label: t`General`,
path: SettingsPath.Workspace,
Icon: IconSettings,
isHidden: !permissionMap[SettingsFeatures.WORKSPACE],
},
{
label: t`Members`,
path: SettingsPath.WorkspaceMembersPage,
Icon: IconUsers,
isHidden: !permissionMap[SettingsFeatures.WORKSPACE_USERS],
},
{
label: t`Billing`,
path: SettingsPath.Billing,
Icon: IconCurrencyDollar,
isHidden: !isBillingEnabled,
},
{
label: t`Roles`,
path: SettingsPath.Roles,
Icon: IconLock,
isHidden:
!featureFlags[FeatureFlagKey.IsPermissionsEnabled] ||
!permissionMap[SettingsFeatures.ROLES],
},
{
label: t`Data model`,
path: SettingsPath.Objects,
Icon: IconHierarchy2,
isHidden: !permissionMap[SettingsFeatures.DATA_MODEL],
},
{
label: t`Integrations`,
path: SettingsPath.Integrations,
Icon: IconApps,
isHidden: !permissionMap[SettingsFeatures.API_KEYS_AND_WEBHOOKS],
},
{
label: t`Security`,
path: SettingsPath.Security,
Icon: IconKey,
isAdvanced: true,
isHidden: !permissionMap[SettingsFeatures.SECURITY],
},
],
},
{
label: t`Developers`,
isAdvanced: true,
items: [
{
label: t`API & Webhooks`,
path: SettingsPath.Developers,
Icon: IconCode,
isAdvanced: true,
isHidden: !permissionMap[SettingsFeatures.API_KEYS_AND_WEBHOOKS],
},
{
label: t`Functions`,
path: SettingsPath.ServerlessFunctions,
Icon: IconFunction,
isHidden: !isFunctionSettingsEnabled,
isAdvanced: true,
},
],
},
{
label: t`Other`,
items: [
{
label: t`Server Admin`,
path: SettingsPath.AdminPanel,
Icon: IconServer,
isHidden: !isAdminEnabled,
},
{
label: t`Lab`,
path: SettingsPath.Lab,
Icon: IconFlask,
isHidden: !labPublicFeatureFlags.length,
},
{
label: t`Releases`,
path: SettingsPath.Releases,
Icon: IconRocket,
},
],
},
];
};

View File

@ -0,0 +1,22 @@
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { useRecoilValue } from 'recoil';
import { SettingsFeatures } from 'twenty-shared';
export const useHasSettingsPermission = (
settingsPermission?: SettingsFeatures,
) => {
const currentUserWorkspace = useRecoilValue(currentUserWorkspaceState);
if (!settingsPermission) {
return true;
}
const currentUserWorkspaceSettingsPermissions =
currentUserWorkspace?.settingsPermissions;
if (!currentUserWorkspaceSettingsPermissions) {
return false;
}
return currentUserWorkspaceSettingsPermissions.includes(settingsPermission);
};

View File

@ -0,0 +1,34 @@
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useRecoilValue } from 'recoil';
import { SettingsFeatures } from 'twenty-shared';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
import { buildRecordFromKeysWithSameValue } from '~/utils/array/buildRecordFromKeysWithSameValue';
export const useSettingsPermissionMap = (): Record<
SettingsFeatures,
boolean
> => {
const currentUserWorkspace = useRecoilValue(currentUserWorkspaceState);
const isPermissionEnabled = useIsFeatureEnabled(
FeatureFlagKey.IsPermissionsEnabled,
);
const currentUserWorkspaceSettingsPermissions =
currentUserWorkspace?.settingsPermissions;
const initialPermissions = buildRecordFromKeysWithSameValue(
Object.values(SettingsFeatures),
!isPermissionEnabled,
);
if (!currentUserWorkspaceSettingsPermissions) {
return initialPermissions;
}
return currentUserWorkspaceSettingsPermissions.reduce((acc, permission) => {
acc[permission] = true;
return acc;
}, initialPermissions);
};

View File

@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
import { useRecoilState, useSetRecoilState } from 'recoil'; import { useRecoilState, useSetRecoilState } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState'; import { currentUserState } from '@/auth/states/currentUserState';
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates'; import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
@ -29,6 +30,7 @@ export const UserProviderEffect = () => {
); );
const setCurrentUser = useSetRecoilState(currentUserState); const setCurrentUser = useSetRecoilState(currentUserState);
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState); const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
const setCurrentUserWorkspace = useSetRecoilState(currentUserWorkspaceState);
const setWorkspaces = useSetRecoilState(workspacesState); const setWorkspaces = useSetRecoilState(workspacesState);
const setDateTimeFormat = useSetRecoilState(dateTimeFormatState); const setDateTimeFormat = useSetRecoilState(dateTimeFormatState);
@ -58,6 +60,10 @@ export const UserProviderEffect = () => {
setCurrentWorkspace(queryData.currentUser.currentWorkspace); setCurrentWorkspace(queryData.currentUser.currentWorkspace);
} }
if (isDefined(queryData.currentUser.currentUserWorkspace)) {
setCurrentUserWorkspace(queryData.currentUser.currentUserWorkspace);
}
const { const {
workspaceMember, workspaceMember,
workspaceMembers, workspaceMembers,
@ -115,6 +121,7 @@ export const UserProviderEffect = () => {
} }
}, [ }, [
setCurrentUser, setCurrentUser,
setCurrentUserWorkspace,
setCurrentWorkspaceMembers, setCurrentWorkspaceMembers,
isLoading, isLoading,
queryLoading, queryLoading,

View File

@ -0,0 +1,24 @@
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useRecoilValue } from 'recoil';
import { FeatureFlagKey } from '~/generated/graphql';
import { buildRecordFromKeysWithSameValue } from '~/utils/array/buildRecordFromKeysWithSameValue';
export const useFeatureFlagsMap = (): Record<FeatureFlagKey, boolean> => {
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const currentWorkspaceFeatureFlags = currentWorkspace?.featureFlags;
const initialFeatureFlags = buildRecordFromKeysWithSameValue(
Object.values(FeatureFlagKey),
false,
);
if (!currentWorkspaceFeatureFlags) {
return initialFeatureFlags;
}
return currentWorkspaceFeatureFlags.reduce((acc, featureFlag) => {
acc[featureFlag.key] = featureFlag.value;
return acc;
}, initialFeatureFlags);
};

View File

@ -103,39 +103,37 @@ export const SettingsRoleEdit = () => {
}; };
return ( return (
<> <SubMenuTopBarContainer
<SubMenuTopBarContainer title={
title={ <StyledTitleContainer>
<StyledTitleContainer> <StyledIconUser size={16} />
<StyledIconUser size={16} /> <H3Title title={role.label} />
<H3Title title={role.label} /> </StyledTitleContainer>
</StyledTitleContainer> }
} links={[
links={[ {
{ children: 'Workspace',
children: 'Workspace', href: getSettingsPath(SettingsPath.Workspace),
href: getSettingsPath(SettingsPath.Workspace), },
}, {
{ children: 'Roles',
children: 'Roles', href: getSettingsPath(SettingsPath.Roles),
href: getSettingsPath(SettingsPath.Roles), },
}, {
{ children: role.label,
children: role.label, },
}, ]}
]} >
> <SettingsPageContainer>
<SettingsPageContainer> <TabList
<TabList tabListInstanceId={SETTINGS_ROLE_DETAIL_TABS.COMPONENT_INSTANCE_ID}
tabListInstanceId={SETTINGS_ROLE_DETAIL_TABS.COMPONENT_INSTANCE_ID} tabs={tabs}
tabs={tabs} className="tab-list"
className="tab-list" />
/> <StyledContentContainer>
<StyledContentContainer> {renderActiveTabContent()}
{renderActiveTabContent()} </StyledContentContainer>
</StyledContentContainer> </SettingsPageContainer>
</SettingsPageContainer> </SubMenuTopBarContainer>
</SubMenuTopBarContainer>
</>
); );
}; };

View File

@ -20,9 +20,9 @@ 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 { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import { FeatureFlagKey, useGetRolesQuery } from '~/generated/graphql'; import React from 'react';
import { useGetRolesQuery } from '~/generated/graphql';
import { useNavigateSettings } from '~/hooks/useNavigateSettings'; import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
@ -90,19 +90,13 @@ const StyledAssignedText = styled.div`
export const SettingsRoles = () => { export const SettingsRoles = () => {
const { t } = useLingui(); const { t } = useLingui();
const isPermissionsEnabled = useIsFeatureEnabled(
FeatureFlagKey.IsPermissionsEnabled,
);
const theme = useTheme(); const theme = useTheme();
const navigateSettings = useNavigateSettings(); const navigateSettings = useNavigateSettings();
const { data: rolesData, loading: rolesLoading } = useGetRolesQuery({ const { data: rolesData, loading: rolesLoading } = useGetRolesQuery({
fetchPolicy: 'network-only', fetchPolicy: 'network-only',
}); });
if (!isPermissionsEnabled) {
return null;
}
const handleRoleClick = (roleId: string) => { const handleRoleClick = (roleId: string) => {
navigateSettings(SettingsPath.RoleDetail, { roleId }); navigateSettings(SettingsPath.RoleDetail, { roleId });
}; };
@ -157,9 +151,8 @@ export const SettingsRoles = () => {
{role.workspaceMembers {role.workspaceMembers
.slice(0, 5) .slice(0, 5)
.map((workspaceMember) => ( .map((workspaceMember) => (
<> <React.Fragment key={workspaceMember.id}>
<StyledAvatarContainer <StyledAvatarContainer
key={workspaceMember.id}
id={`avatar-${workspaceMember.id}`} id={`avatar-${workspaceMember.id}`}
> >
<Avatar <Avatar
@ -180,7 +173,7 @@ export const SettingsRoles = () => {
positionStrategy="fixed" positionStrategy="fixed"
delay={TooltipDelay.shortDelay} delay={TooltipDelay.shortDelay}
/> />
</> </React.Fragment>
))} ))}
</StyledAvatarGroup> </StyledAvatarGroup>
<StyledAssignedText> <StyledAssignedText>

View File

@ -19,8 +19,8 @@ export const RoleWorkspaceMemberPickerDropdownContent = ({
return null; return null;
} }
if (!filteredWorkspaceMembers?.length && searchFilter?.length > 0) { if (!filteredWorkspaceMembers.length && searchFilter.length > 0) {
return <MenuItem disabled text={t`No Result`} />; return <MenuItem disabled text={t`No Results`} />;
} }
return ( return (

View File

@ -3,6 +3,7 @@ import { useEffect } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil'; import { useRecoilValue, useSetRecoilState } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState'; import { currentUserState } from '@/auth/states/currentUserState';
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { ObjectMetadataItemsLoadEffect } from '@/object-metadata/components/ObjectMetadataItemsLoadEffect'; import { ObjectMetadataItemsLoadEffect } from '@/object-metadata/components/ObjectMetadataItemsLoadEffect';
import { PreComputedChipGeneratorsProvider } from '@/object-metadata/components/PreComputedChipGeneratorsProvider'; import { PreComputedChipGeneratorsProvider } from '@/object-metadata/components/PreComputedChipGeneratorsProvider';
@ -16,11 +17,13 @@ export const ObjectMetadataItemsDecorator: Decorator = (Story) => {
currentWorkspaceMemberState, currentWorkspaceMemberState,
); );
const setCurrentUser = useSetRecoilState(currentUserState); const setCurrentUser = useSetRecoilState(currentUserState);
const setCurrentUserWorkspace = useSetRecoilState(currentUserWorkspaceState);
useEffect(() => { useEffect(() => {
setCurrentWorkspaceMember(mockWorkspaceMembers[0]); setCurrentWorkspaceMember(mockWorkspaceMembers[0]);
setCurrentUser(mockedUserData); setCurrentUser(mockedUserData);
}, [setCurrentUser, setCurrentWorkspaceMember]); setCurrentUserWorkspace(mockedUserData.currentUserWorkspace);
}, [setCurrentUser, setCurrentWorkspaceMember, setCurrentUserWorkspace]);
return ( return (
<> <>

View File

@ -1,7 +1,9 @@
import { CurrentUserWorkspace } from '@/auth/states/currentUserWorkspaceState';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember'; import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { import {
FeatureFlagKey, FeatureFlagKey,
OnboardingStatus, OnboardingStatus,
SettingsFeatures,
SubscriptionInterval, SubscriptionInterval,
SubscriptionStatus, SubscriptionStatus,
User, User,
@ -29,6 +31,7 @@ type MockedUser = Pick<
currentWorkspace: Workspace; currentWorkspace: Workspace;
workspaces: Array<{ workspace: Workspace }>; workspaces: Array<{ workspace: Workspace }>;
workspaceMembers: WorkspaceMember[]; workspaceMembers: WorkspaceMember[];
currentUserWorkspace: CurrentUserWorkspace;
}; };
export const avatarUrl = export const avatarUrl =
@ -125,6 +128,9 @@ export const mockedUserData: MockedUser = {
'a95afad9ff6f0b364e2a3fd3e246a1a852c22b6e55a3ca33745a86c201f9c10d', 'a95afad9ff6f0b364e2a3fd3e246a1a852c22b6e55a3ca33745a86c201f9c10d',
workspaceMember: mockedWorkspaceMemberData, workspaceMember: mockedWorkspaceMemberData,
currentWorkspace: mockCurrentWorkspace, currentWorkspace: mockCurrentWorkspace,
currentUserWorkspace: {
settingsPermissions: [SettingsFeatures.WORKSPACE_USERS],
},
locale: 'en', locale: 'en',
workspaces: [{ workspace: mockCurrentWorkspace }], workspaces: [{ workspace: mockCurrentWorkspace }],
workspaceMembers: [mockedWorkspaceMemberData], workspaceMembers: [mockedWorkspaceMemberData],