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:
@ -15,6 +15,7 @@ import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
||||
import { useUpdateEffect } from '~/hooks/useUpdateEffect';
|
||||
|
||||
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { ApolloFactory, Options } from '../services/apollo.factory';
|
||||
|
||||
@ -34,6 +35,7 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
|
||||
const setCurrentWorkspaceMember = useSetRecoilState(
|
||||
currentWorkspaceMemberState,
|
||||
);
|
||||
const setCurrentUserWorkspace = useSetRecoilState(currentUserWorkspaceState);
|
||||
|
||||
const setWorkspaces = useSetRecoilState(workspacesState);
|
||||
const [, setPreviousUrl] = useRecoilState(previousUrlState);
|
||||
@ -71,6 +73,7 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
|
||||
setCurrentUser(null);
|
||||
setCurrentWorkspaceMember(null);
|
||||
setCurrentWorkspace(null);
|
||||
setCurrentUserWorkspace(null);
|
||||
setWorkspaces([]);
|
||||
if (
|
||||
!isMatchingLocation(AppPath.Verify) &&
|
||||
|
||||
@ -1,35 +1,19 @@
|
||||
import { useCreateAppRouter } from '@/app/hooks/useCreateAppRouter';
|
||||
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 { useRecoilValue } from 'recoil';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
export const AppRouter = () => {
|
||||
const billing = useRecoilValue(billingState);
|
||||
|
||||
// We want to disable serverless function settings but keep the code for now
|
||||
const isFunctionSettingsEnabled = false;
|
||||
|
||||
const isBillingPageEnabled = billing?.isBillingEnabled;
|
||||
|
||||
const currentUser = useRecoilValue(currentUserState);
|
||||
|
||||
const isAdminPageEnabled = currentUser?.canImpersonate;
|
||||
|
||||
const isPermissionsEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IsPermissionsEnabled,
|
||||
);
|
||||
|
||||
return (
|
||||
<RouterProvider
|
||||
router={useCreateAppRouter(
|
||||
isBillingPageEnabled,
|
||||
isFunctionSettingsEnabled,
|
||||
isAdminPageEnabled,
|
||||
isPermissionsEnabled,
|
||||
)}
|
||||
router={useCreateAppRouter(isFunctionSettingsEnabled, isAdminPageEnabled)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
|
||||
import { SettingsProtectedRouteWrapper } from '@/settings/components/SettingsProtectedRouteWrapper';
|
||||
import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { SettingsFeatures } from 'twenty-shared';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
const SettingsAccountsCalendars = lazy(() =>
|
||||
import('~/pages/settings/accounts/SettingsAccountsCalendars').then(
|
||||
@ -264,17 +267,13 @@ const SettingsRoleEdit = lazy(() =>
|
||||
);
|
||||
|
||||
type SettingsRoutesProps = {
|
||||
isBillingEnabled?: boolean;
|
||||
isFunctionSettingsEnabled?: boolean;
|
||||
isAdminPageEnabled?: boolean;
|
||||
isPermissionsEnabled?: boolean;
|
||||
};
|
||||
|
||||
export const SettingsRoutes = ({
|
||||
isBillingEnabled,
|
||||
isFunctionSettingsEnabled,
|
||||
isAdminPageEnabled,
|
||||
isPermissionsEnabled,
|
||||
}: SettingsRoutesProps) => (
|
||||
<Suspense fallback={<SettingsSkeletonLoader />}>
|
||||
<Routes>
|
||||
@ -290,35 +289,50 @@ export const SettingsRoutes = ({
|
||||
path={SettingsPath.AccountsEmails}
|
||||
element={<SettingsAccountsEmails />}
|
||||
/>
|
||||
{isBillingEnabled && (
|
||||
<Route
|
||||
element={
|
||||
<SettingsProtectedRouteWrapper
|
||||
requiredFeatureFlag={FeatureFlagKey.IsBillingPlansEnabled}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Route path={SettingsPath.Billing} element={<SettingsBilling />} />
|
||||
)}
|
||||
</Route>
|
||||
<Route path={SettingsPath.Workspace} element={<SettingsWorkspace />} />
|
||||
<Route path={SettingsPath.Domain} element={<SettingsDomain />} />
|
||||
<Route
|
||||
path={SettingsPath.WorkspaceMembersPage}
|
||||
element={<SettingsWorkspaceMembers />}
|
||||
/>
|
||||
<Route path={SettingsPath.Workspace} element={<SettingsWorkspace />} />
|
||||
<Route path={SettingsPath.Objects} element={<SettingsObjects />} />
|
||||
<Route
|
||||
path={SettingsPath.ObjectOverview}
|
||||
element={<SettingsObjectOverview />}
|
||||
/>
|
||||
<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 />}
|
||||
element={
|
||||
<SettingsProtectedRouteWrapper
|
||||
settingsPermission={SettingsFeatures.DATA_MODEL}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
}
|
||||
>
|
||||
<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.DevelopersNewApiKey}
|
||||
|
||||
@ -26,10 +26,8 @@ import { PaymentSuccess } from '~/pages/onboarding/PaymentSuccess';
|
||||
import { SyncEmails } from '~/pages/onboarding/SyncEmails';
|
||||
|
||||
export const useCreateAppRouter = (
|
||||
isBillingEnabled?: boolean,
|
||||
isFunctionSettingsEnabled?: boolean,
|
||||
isAdminPageEnabled?: boolean,
|
||||
isPermissionsEnabled?: boolean,
|
||||
) =>
|
||||
createBrowserRouter(
|
||||
createRoutesFromElements(
|
||||
@ -61,10 +59,8 @@ export const useCreateAppRouter = (
|
||||
path={AppPath.SettingsCatchAll}
|
||||
element={
|
||||
<SettingsRoutes
|
||||
isBillingEnabled={isBillingEnabled}
|
||||
isFunctionSettingsEnabled={isFunctionSettingsEnabled}
|
||||
isAdminPageEnabled={isAdminPageEnabled}
|
||||
isPermissionsEnabled={isPermissionsEnabled}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@ -44,6 +44,7 @@ import { getTimeFormatFromWorkspaceTimeFormat } from '@/localization/utils/getTi
|
||||
import { currentUserState } from '../states/currentUserState';
|
||||
import { tokenPairState } from '../states/tokenPairState';
|
||||
|
||||
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
|
||||
import {
|
||||
SignInUpStep,
|
||||
signInUpStepState,
|
||||
@ -71,6 +72,7 @@ export const useAuth = () => {
|
||||
const setCurrentWorkspaceMember = useSetRecoilState(
|
||||
currentWorkspaceMemberState,
|
||||
);
|
||||
const setCurrentUserWorkspace = useSetRecoilState(currentUserWorkspaceState);
|
||||
const setIsAppWaitingForFreshObjectMetadataState = useSetRecoilState(
|
||||
isAppWaitingForFreshObjectMetadataState,
|
||||
);
|
||||
@ -255,6 +257,10 @@ export const useAuth = () => {
|
||||
setCurrentWorkspaceMembers(workspaceMembers);
|
||||
}
|
||||
|
||||
if (isDefined(user.currentUserWorkspace)) {
|
||||
setCurrentUserWorkspace(user.currentUserWorkspace);
|
||||
}
|
||||
|
||||
if (isDefined(user.workspaceMember)) {
|
||||
workspaceMember = {
|
||||
...user.workspaceMember,
|
||||
@ -318,6 +324,7 @@ export const useAuth = () => {
|
||||
getCurrentUser,
|
||||
isOnAWorkspace,
|
||||
setCurrentUser,
|
||||
setCurrentUserWorkspace,
|
||||
setCurrentWorkspace,
|
||||
setCurrentWorkspaceMember,
|
||||
setCurrentWorkspaceMembers,
|
||||
|
||||
@ -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,
|
||||
});
|
||||
@ -1,48 +1,57 @@
|
||||
import { useMatch, useResolvedPath } from 'react-router-dom';
|
||||
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import {
|
||||
NavigationDrawerItem,
|
||||
NavigationDrawerItemProps,
|
||||
} from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||
import { AdvancedSettingsWrapper } from '@/settings/components/AdvancedSettingsWrapper';
|
||||
import { SettingsNavigationItem } from '@/settings/hooks/useSettingsNavigationItems';
|
||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||
import { NavigationDrawerSubItemState } from '@/ui/navigation/navigation-drawer/types/NavigationDrawerSubItemState';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||
|
||||
type SettingsNavigationDrawerItemProps = Pick<
|
||||
NavigationDrawerItemProps,
|
||||
'Icon' | 'label' | 'indentationLevel' | 'soon'
|
||||
> & {
|
||||
matchSubPages?: boolean;
|
||||
path: SettingsPath;
|
||||
type SettingsNavigationDrawerItemProps = {
|
||||
item: SettingsNavigationItem;
|
||||
subItemState?: NavigationDrawerSubItemState;
|
||||
};
|
||||
|
||||
export const SettingsNavigationDrawerItem = ({
|
||||
Icon,
|
||||
label,
|
||||
indentationLevel,
|
||||
matchSubPages = true,
|
||||
path,
|
||||
soon,
|
||||
item,
|
||||
subItemState,
|
||||
}: SettingsNavigationDrawerItemProps) => {
|
||||
const href = getSettingsPath(path);
|
||||
const href = getSettingsPath(item.path);
|
||||
const pathName = useResolvedPath(href).pathname;
|
||||
|
||||
const isActive = !!useMatch({
|
||||
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 (
|
||||
<NavigationDrawerItem
|
||||
indentationLevel={indentationLevel}
|
||||
indentationLevel={item.indentationLevel}
|
||||
subItemState={subItemState}
|
||||
label={label}
|
||||
label={item.label}
|
||||
to={href}
|
||||
Icon={Icon}
|
||||
Icon={item.Icon}
|
||||
active={isActive}
|
||||
soon={soon}
|
||||
soon={item.soon}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,229 +1,121 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import {
|
||||
IconApps,
|
||||
IconAt,
|
||||
IconCalendarEvent,
|
||||
IconCode,
|
||||
IconColorSwatch,
|
||||
IconComponent,
|
||||
IconCurrencyDollar,
|
||||
IconDoorEnter,
|
||||
IconFlask,
|
||||
IconFunction,
|
||||
IconHierarchy2,
|
||||
IconKey,
|
||||
IconLock,
|
||||
IconMail,
|
||||
IconRocket,
|
||||
IconServer,
|
||||
IconSettings,
|
||||
IconUserCircle,
|
||||
IconUsers,
|
||||
} from 'twenty-ui';
|
||||
import { IconDoorEnter } from 'twenty-ui';
|
||||
|
||||
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 { SettingsNavigationDrawerItem } from '@/settings/components/SettingsNavigationDrawerItem';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import {
|
||||
NavigationDrawerItem,
|
||||
NavigationDrawerItemIndentationLevel,
|
||||
} from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||
SettingsNavigationItem,
|
||||
SettingsNavigationSection,
|
||||
useSettingsNavigationItems,
|
||||
} from '@/settings/hooks/useSettingsNavigationItems';
|
||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||
import { NavigationDrawerItemGroup } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemGroup';
|
||||
import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
|
||||
import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle';
|
||||
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { matchPath, resolvePath, useLocation } from 'react-router-dom';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||
|
||||
type SettingsNavigationItem = {
|
||||
label: string;
|
||||
path: SettingsPath;
|
||||
Icon: IconComponent;
|
||||
indentationLevel?: NavigationDrawerItemIndentationLevel;
|
||||
matchSubPages?: boolean;
|
||||
};
|
||||
|
||||
export const SettingsNavigationDrawerItems = () => {
|
||||
const { signOut } = useAuth();
|
||||
|
||||
const { t } = useLingui();
|
||||
|
||||
const billing = useRecoilValue(billingState);
|
||||
const isPermissionsEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IsPermissionsEnabled,
|
||||
);
|
||||
const settingsNavigationItems: SettingsNavigationSection[] =
|
||||
useSettingsNavigationItems();
|
||||
|
||||
// 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 accountSubSettings: SettingsNavigationItem[] = [
|
||||
{
|
||||
label: t`Emails`,
|
||||
path: SettingsPath.AccountsEmails,
|
||||
Icon: IconMail,
|
||||
indentationLevel: 2,
|
||||
},
|
||||
{
|
||||
label: t`Calendars`,
|
||||
path: SettingsPath.AccountsCalendars,
|
||||
Icon: IconCalendarEvent,
|
||||
indentationLevel: 2,
|
||||
},
|
||||
];
|
||||
const getSelectedIndexForSubItems = (subItems: SettingsNavigationItem[]) => {
|
||||
return subItems.findIndex((subItem) => {
|
||||
const href = getSettingsPath(subItem.path);
|
||||
const pathName = resolvePath(href).pathname;
|
||||
|
||||
const selectedIndex = accountSubSettings.findIndex((accountSubSetting) => {
|
||||
const href = getSettingsPath(accountSubSetting.path);
|
||||
const pathName = resolvePath(href).pathname;
|
||||
|
||||
return matchPath(
|
||||
{
|
||||
path: pathName,
|
||||
end: accountSubSetting.matchSubPages === false,
|
||||
},
|
||||
currentPathName,
|
||||
);
|
||||
});
|
||||
return matchPath(
|
||||
{
|
||||
path: pathName,
|
||||
end: subItem.matchSubPages === false,
|
||||
},
|
||||
currentPathName,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<NavigationDrawerSection>
|
||||
<NavigationDrawerSectionTitle label={t`User`} />
|
||||
<SettingsNavigationDrawerItem
|
||||
label={t`Profile`}
|
||||
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>
|
||||
{settingsNavigationItems.map((section) => {
|
||||
const allItemsHidden = section.items.every((item) => item.isHidden);
|
||||
if (allItemsHidden) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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>
|
||||
<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
|
||||
label={t`Logout`}
|
||||
onClick={signOut}
|
||||
|
||||
@ -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 />;
|
||||
};
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
};
|
||||
@ -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);
|
||||
};
|
||||
@ -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);
|
||||
};
|
||||
@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
|
||||
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
@ -29,6 +30,7 @@ export const UserProviderEffect = () => {
|
||||
);
|
||||
const setCurrentUser = useSetRecoilState(currentUserState);
|
||||
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
|
||||
const setCurrentUserWorkspace = useSetRecoilState(currentUserWorkspaceState);
|
||||
const setWorkspaces = useSetRecoilState(workspacesState);
|
||||
|
||||
const setDateTimeFormat = useSetRecoilState(dateTimeFormatState);
|
||||
@ -58,6 +60,10 @@ export const UserProviderEffect = () => {
|
||||
setCurrentWorkspace(queryData.currentUser.currentWorkspace);
|
||||
}
|
||||
|
||||
if (isDefined(queryData.currentUser.currentUserWorkspace)) {
|
||||
setCurrentUserWorkspace(queryData.currentUser.currentUserWorkspace);
|
||||
}
|
||||
|
||||
const {
|
||||
workspaceMember,
|
||||
workspaceMembers,
|
||||
@ -115,6 +121,7 @@ export const UserProviderEffect = () => {
|
||||
}
|
||||
}, [
|
||||
setCurrentUser,
|
||||
setCurrentUserWorkspace,
|
||||
setCurrentWorkspaceMembers,
|
||||
isLoading,
|
||||
queryLoading,
|
||||
|
||||
@ -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);
|
||||
};
|
||||
@ -103,39 +103,37 @@ export const SettingsRoleEdit = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SubMenuTopBarContainer
|
||||
title={
|
||||
<StyledTitleContainer>
|
||||
<StyledIconUser size={16} />
|
||||
<H3Title title={role.label} />
|
||||
</StyledTitleContainer>
|
||||
}
|
||||
links={[
|
||||
{
|
||||
children: 'Workspace',
|
||||
href: getSettingsPath(SettingsPath.Workspace),
|
||||
},
|
||||
{
|
||||
children: 'Roles',
|
||||
href: getSettingsPath(SettingsPath.Roles),
|
||||
},
|
||||
{
|
||||
children: role.label,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<TabList
|
||||
tabListInstanceId={SETTINGS_ROLE_DETAIL_TABS.COMPONENT_INSTANCE_ID}
|
||||
tabs={tabs}
|
||||
className="tab-list"
|
||||
/>
|
||||
<StyledContentContainer>
|
||||
{renderActiveTabContent()}
|
||||
</StyledContentContainer>
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
</>
|
||||
<SubMenuTopBarContainer
|
||||
title={
|
||||
<StyledTitleContainer>
|
||||
<StyledIconUser size={16} />
|
||||
<H3Title title={role.label} />
|
||||
</StyledTitleContainer>
|
||||
}
|
||||
links={[
|
||||
{
|
||||
children: 'Workspace',
|
||||
href: getSettingsPath(SettingsPath.Workspace),
|
||||
},
|
||||
{
|
||||
children: 'Roles',
|
||||
href: getSettingsPath(SettingsPath.Roles),
|
||||
},
|
||||
{
|
||||
children: role.label,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<TabList
|
||||
tabListInstanceId={SETTINGS_ROLE_DETAIL_TABS.COMPONENT_INSTANCE_ID}
|
||||
tabs={tabs}
|
||||
className="tab-list"
|
||||
/>
|
||||
<StyledContentContainer>
|
||||
{renderActiveTabContent()}
|
||||
</StyledContentContainer>
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -20,9 +20,9 @@ import { Table } from '@/ui/layout/table/components/Table';
|
||||
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 { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
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 { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||
|
||||
@ -90,19 +90,13 @@ const StyledAssignedText = styled.div`
|
||||
|
||||
export const SettingsRoles = () => {
|
||||
const { t } = useLingui();
|
||||
const isPermissionsEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IsPermissionsEnabled,
|
||||
);
|
||||
|
||||
const theme = useTheme();
|
||||
const navigateSettings = useNavigateSettings();
|
||||
const { data: rolesData, loading: rolesLoading } = useGetRolesQuery({
|
||||
fetchPolicy: 'network-only',
|
||||
});
|
||||
|
||||
if (!isPermissionsEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleRoleClick = (roleId: string) => {
|
||||
navigateSettings(SettingsPath.RoleDetail, { roleId });
|
||||
};
|
||||
@ -157,9 +151,8 @@ export const SettingsRoles = () => {
|
||||
{role.workspaceMembers
|
||||
.slice(0, 5)
|
||||
.map((workspaceMember) => (
|
||||
<>
|
||||
<React.Fragment key={workspaceMember.id}>
|
||||
<StyledAvatarContainer
|
||||
key={workspaceMember.id}
|
||||
id={`avatar-${workspaceMember.id}`}
|
||||
>
|
||||
<Avatar
|
||||
@ -180,7 +173,7 @@ export const SettingsRoles = () => {
|
||||
positionStrategy="fixed"
|
||||
delay={TooltipDelay.shortDelay}
|
||||
/>
|
||||
</>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</StyledAvatarGroup>
|
||||
<StyledAssignedText>
|
||||
|
||||
@ -19,8 +19,8 @@ export const RoleWorkspaceMemberPickerDropdownContent = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!filteredWorkspaceMembers?.length && searchFilter?.length > 0) {
|
||||
return <MenuItem disabled text={t`No Result`} />;
|
||||
if (!filteredWorkspaceMembers.length && searchFilter.length > 0) {
|
||||
return <MenuItem disabled text={t`No Results`} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@ -3,6 +3,7 @@ import { useEffect } from 'react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { ObjectMetadataItemsLoadEffect } from '@/object-metadata/components/ObjectMetadataItemsLoadEffect';
|
||||
import { PreComputedChipGeneratorsProvider } from '@/object-metadata/components/PreComputedChipGeneratorsProvider';
|
||||
@ -16,11 +17,13 @@ export const ObjectMetadataItemsDecorator: Decorator = (Story) => {
|
||||
currentWorkspaceMemberState,
|
||||
);
|
||||
const setCurrentUser = useSetRecoilState(currentUserState);
|
||||
const setCurrentUserWorkspace = useSetRecoilState(currentUserWorkspaceState);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentWorkspaceMember(mockWorkspaceMembers[0]);
|
||||
setCurrentUser(mockedUserData);
|
||||
}, [setCurrentUser, setCurrentWorkspaceMember]);
|
||||
setCurrentUserWorkspace(mockedUserData.currentUserWorkspace);
|
||||
}, [setCurrentUser, setCurrentWorkspaceMember, setCurrentUserWorkspace]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { CurrentUserWorkspace } from '@/auth/states/currentUserWorkspaceState';
|
||||
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
||||
import {
|
||||
FeatureFlagKey,
|
||||
OnboardingStatus,
|
||||
SettingsFeatures,
|
||||
SubscriptionInterval,
|
||||
SubscriptionStatus,
|
||||
User,
|
||||
@ -29,6 +31,7 @@ type MockedUser = Pick<
|
||||
currentWorkspace: Workspace;
|
||||
workspaces: Array<{ workspace: Workspace }>;
|
||||
workspaceMembers: WorkspaceMember[];
|
||||
currentUserWorkspace: CurrentUserWorkspace;
|
||||
};
|
||||
|
||||
export const avatarUrl =
|
||||
@ -125,6 +128,9 @@ export const mockedUserData: MockedUser = {
|
||||
'a95afad9ff6f0b364e2a3fd3e246a1a852c22b6e55a3ca33745a86c201f9c10d',
|
||||
workspaceMember: mockedWorkspaceMemberData,
|
||||
currentWorkspace: mockCurrentWorkspace,
|
||||
currentUserWorkspace: {
|
||||
settingsPermissions: [SettingsFeatures.WORKSPACE_USERS],
|
||||
},
|
||||
locale: 'en',
|
||||
workspaces: [{ workspace: mockCurrentWorkspace }],
|
||||
workspaceMembers: [mockedWorkspaceMemberData],
|
||||
|
||||
Reference in New Issue
Block a user