@ -253,6 +253,12 @@ const SettingsAdminContent = lazy(() =>
|
||||
),
|
||||
);
|
||||
|
||||
const SettingsLab = lazy(() =>
|
||||
import('~/pages/settings/lab/SettingsLab').then((module) => ({
|
||||
default: module.SettingsLab,
|
||||
})),
|
||||
);
|
||||
|
||||
type SettingsRoutesProps = {
|
||||
isBillingEnabled?: boolean;
|
||||
isServerlessFunctionSettingsEnabled?: boolean;
|
||||
@ -379,6 +385,7 @@ export const SettingsRoutes = ({
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Route path={SettingsPath.Lab} element={<SettingsLab />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
@ -10,6 +10,7 @@ import { isDebugModeState } from '@/client-config/states/isDebugModeState';
|
||||
import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState';
|
||||
import { isEmailVerificationRequiredState } from '@/client-config/states/isEmailVerificationRequiredState';
|
||||
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
|
||||
import { labPublicFeatureFlagsState } from '@/client-config/states/labPublicFeatureFlagsState';
|
||||
import { sentryConfigState } from '@/client-config/states/sentryConfigState';
|
||||
import { supportChatState } from '@/client-config/states/supportChatState';
|
||||
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
|
||||
@ -52,6 +53,10 @@ export const ClientConfigProviderEffect = () => {
|
||||
canManageFeatureFlagsState,
|
||||
);
|
||||
|
||||
const setLabPublicFeatureFlags = useSetRecoilState(
|
||||
labPublicFeatureFlagsState,
|
||||
);
|
||||
|
||||
const { data, loading, error } = useGetClientConfigQuery({
|
||||
skip: clientConfigApiStatus.isLoaded,
|
||||
});
|
||||
@ -117,6 +122,7 @@ export const ClientConfigProviderEffect = () => {
|
||||
frontDomain: data?.clientConfig?.frontDomain,
|
||||
});
|
||||
setCanManageFeatureFlags(data?.clientConfig?.canManageFeatureFlags);
|
||||
setLabPublicFeatureFlags(data?.clientConfig?.publicFeatureFlags);
|
||||
}, [
|
||||
data,
|
||||
setIsDebugMode,
|
||||
@ -136,6 +142,7 @@ export const ClientConfigProviderEffect = () => {
|
||||
setDomainConfiguration,
|
||||
setAuthProviders,
|
||||
setCanManageFeatureFlags,
|
||||
setLabPublicFeatureFlags,
|
||||
]);
|
||||
|
||||
return <></>;
|
||||
|
||||
@ -48,6 +48,14 @@ export const GET_CLIENT_CONFIG = gql`
|
||||
}
|
||||
chromeExtensionId
|
||||
canManageFeatureFlags
|
||||
publicFeatureFlags {
|
||||
key
|
||||
metadata {
|
||||
label
|
||||
description
|
||||
imagePath
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
import { atom } from 'recoil';
|
||||
import { PublicFeatureFlag } from '~/generated/graphql';
|
||||
|
||||
export const labPublicFeatureFlagsState = atom<PublicFeatureFlag[]>({
|
||||
key: 'labPublicFeatureFlagsState',
|
||||
default: [],
|
||||
});
|
||||
@ -8,6 +8,7 @@ import {
|
||||
IconComponent,
|
||||
IconCurrencyDollar,
|
||||
IconDoorEnter,
|
||||
IconFlask,
|
||||
IconFunction,
|
||||
IconHierarchy2,
|
||||
IconKey,
|
||||
@ -22,6 +23,7 @@ import {
|
||||
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';
|
||||
@ -64,6 +66,7 @@ export const SettingsNavigationDrawerItems = () => {
|
||||
|
||||
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;
|
||||
|
||||
@ -200,6 +203,13 @@ export const SettingsNavigationDrawerItems = () => {
|
||||
Icon={IconServer}
|
||||
/>
|
||||
)}
|
||||
{labPublicFeatureFlags?.length > 0 && (
|
||||
<SettingsNavigationDrawerItem
|
||||
label={t`Lab`}
|
||||
path={SettingsPath.Lab}
|
||||
Icon={IconFlask}
|
||||
/>
|
||||
)}
|
||||
<SettingsNavigationDrawerItem
|
||||
label={t`Releases`}
|
||||
path={SettingsPath.Releases}
|
||||
|
||||
@ -23,7 +23,11 @@ const StyledSettingsOptionCardToggleContent = styled(
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledSettingsOptionCardToggleButton = styled(Toggle)`
|
||||
const StyledSettingsOptionCardToggleButton = styled(Toggle)<{
|
||||
toggleCentered?: boolean;
|
||||
}>`
|
||||
align-self: ${({ toggleCentered }) =>
|
||||
toggleCentered ? 'center' : 'flex-start'};
|
||||
margin-left: auto;
|
||||
`;
|
||||
|
||||
@ -40,6 +44,7 @@ type SettingsOptionCardContentToggleProps = {
|
||||
divider?: boolean;
|
||||
disabled?: boolean;
|
||||
advancedMode?: boolean;
|
||||
toggleCentered?: boolean;
|
||||
checked: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
};
|
||||
@ -51,6 +56,7 @@ export const SettingsOptionCardContentToggle = ({
|
||||
divider,
|
||||
disabled = false,
|
||||
advancedMode = false,
|
||||
toggleCentered = true,
|
||||
checked,
|
||||
onChange,
|
||||
}: SettingsOptionCardContentToggleProps) => {
|
||||
@ -83,6 +89,7 @@ export const SettingsOptionCardContentToggle = ({
|
||||
disabled={disabled}
|
||||
toggleSize="small"
|
||||
color={advancedMode ? theme.color.yellow : theme.color.blue}
|
||||
toggleCentered={toggleCentered}
|
||||
/>
|
||||
</StyledSettingsOptionCardToggleContent>
|
||||
{divider && <Separator />}
|
||||
|
||||
@ -0,0 +1,84 @@
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
|
||||
import { useLabPublicFeatureFlags } from '@/settings/lab/hooks/useLabPublicFeatureFlags';
|
||||
import styled from '@emotion/styled';
|
||||
import { useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Card, MOBILE_VIEWPORT } from 'twenty-ui';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
|
||||
const StyledCardGrid = styled.div`
|
||||
display: grid;
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
& > *:not(:first-child) {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
@media (min-width: ${MOBILE_VIEWPORT}px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
|
||||
& > *:first-child {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledImage = styled.img<{ isFirstCard: boolean }>`
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
height: ${({ isFirstCard }) => (isFirstCard ? '240px' : '120px')};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledFallbackDiv = styled.div<{ isFirstCard: boolean }>`
|
||||
background-color: ${({ theme }) => theme.background.tertiary};
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
height: ${({ isFirstCard }) => (isFirstCard ? '240px' : '120px')};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const SettingsLabContent = () => {
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
const { labPublicFeatureFlags, handleLabPublicFeatureFlagUpdate } =
|
||||
useLabPublicFeatureFlags();
|
||||
const [hasImageLoadingError, setHasImageLoadingError] = useState<
|
||||
Record<string, boolean>
|
||||
>({});
|
||||
|
||||
const handleToggle = async (key: FeatureFlagKey, value: boolean) => {
|
||||
await handleLabPublicFeatureFlagUpdate(key, value);
|
||||
};
|
||||
|
||||
const handleImageError = (key: string) => {
|
||||
setHasImageLoadingError((prev) => ({ ...prev, [key]: true }));
|
||||
};
|
||||
|
||||
return (
|
||||
currentWorkspace?.id && (
|
||||
<StyledCardGrid>
|
||||
{labPublicFeatureFlags.map((flag, index) => (
|
||||
<Card key={flag.key} rounded>
|
||||
{flag.metadata.imagePath && !hasImageLoadingError[flag.key] ? (
|
||||
<StyledImage
|
||||
src={flag.metadata.imagePath}
|
||||
alt={flag.metadata.label}
|
||||
isFirstCard={index === 0}
|
||||
onError={() => handleImageError(flag.key)}
|
||||
/>
|
||||
) : (
|
||||
<StyledFallbackDiv isFirstCard={index === 0} />
|
||||
)}
|
||||
<SettingsOptionCardContentToggle
|
||||
title={flag.metadata.label}
|
||||
description={flag.metadata.description}
|
||||
checked={flag.value}
|
||||
onChange={(value) => handleToggle(flag.key, value)}
|
||||
toggleCentered={false}
|
||||
/>
|
||||
</Card>
|
||||
))}
|
||||
</StyledCardGrid>
|
||||
)
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,9 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const UPDATE_LAB_PUBLIC_FEATURE_FLAG = gql`
|
||||
mutation UpdateLabPublicFeatureFlag(
|
||||
$input: UpdateLabPublicFeatureFlagInput!
|
||||
) {
|
||||
updateLabPublicFeatureFlag(input: $input)
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,66 @@
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { labPublicFeatureFlagsState } from '@/client-config/states/labPublicFeatureFlagsState';
|
||||
import { useState } from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
import {
|
||||
FeatureFlagKey,
|
||||
useUpdateLabPublicFeatureFlagMutation,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
export const useLabPublicFeatureFlags = () => {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
|
||||
currentWorkspaceState,
|
||||
);
|
||||
const labPublicFeatureFlags = useRecoilValue(labPublicFeatureFlagsState);
|
||||
|
||||
const [updateLabPublicFeatureFlag] = useUpdateLabPublicFeatureFlagMutation();
|
||||
|
||||
const handleLabPublicFeatureFlagUpdate = async (
|
||||
publicFeatureFlag: FeatureFlagKey,
|
||||
value: boolean,
|
||||
) => {
|
||||
if (!isDefined(currentWorkspace)) {
|
||||
setError('No workspace selected');
|
||||
return false;
|
||||
}
|
||||
|
||||
setError(null);
|
||||
|
||||
const response = await updateLabPublicFeatureFlag({
|
||||
variables: {
|
||||
input: {
|
||||
publicFeatureFlag,
|
||||
value,
|
||||
},
|
||||
},
|
||||
onError: (error) => {
|
||||
setError(error.message);
|
||||
},
|
||||
});
|
||||
|
||||
if (isDefined(response.data)) {
|
||||
setCurrentWorkspace({
|
||||
...currentWorkspace,
|
||||
featureFlags: currentWorkspace.featureFlags?.map((flag) =>
|
||||
flag.key === publicFeatureFlag ? { ...flag, value } : flag,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return !!response.data;
|
||||
};
|
||||
|
||||
return {
|
||||
labPublicFeatureFlags: labPublicFeatureFlags.map((flag) => ({
|
||||
...flag,
|
||||
value:
|
||||
currentWorkspace?.featureFlags?.find(
|
||||
(workspaceFlag) => workspaceFlag.key === flag.key,
|
||||
)?.value ?? false,
|
||||
})),
|
||||
handleLabPublicFeatureFlagUpdate,
|
||||
error,
|
||||
};
|
||||
};
|
||||
@ -35,4 +35,5 @@ export enum SettingsPath {
|
||||
Releases = 'releases',
|
||||
AdminPanel = 'admin-panel',
|
||||
FeatureFlags = 'admin-panel/feature-flags',
|
||||
Lab = 'lab',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user