feat(*): allow to select auth providers + add multiworkspace with subdomain management (#8656)
## Summary Add support for multi-workspace feature and adjust configurations and states accordingly. - Introduced new state isMultiWorkspaceEnabledState. - Updated ClientConfigProviderEffect component to handle multi-workspace. - Modified GraphQL schema and queries to include multi-workspace related configurations. - Adjusted server environment variables and their respective documentation to support multi-workspace toggle. - Updated server-side logic to handle new multi-workspace configurations and conditions.
This commit is contained in:
@ -1,7 +1,7 @@
|
||||
import { Logo } from '@/auth/components/Logo';
|
||||
import { Title } from '@/auth/components/Title';
|
||||
import { FooterNote } from '@/auth/sign-in-up/components/FooterNote';
|
||||
import { SignInUpForm } from '@/auth/sign-in-up/components/SignInUpForm';
|
||||
import { SignInUpWorkspaceScopeForm } from '@/auth/sign-in-up/components/SignInUpWorkspaceScopeForm';
|
||||
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
|
||||
import { useWorkspaceFromInviteHash } from '@/auth/sign-in-up/hooks/useWorkspaceFromInviteHash';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
@ -16,6 +16,7 @@ import {
|
||||
useAddUserToWorkspaceMutation,
|
||||
} from '~/generated/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
|
||||
const StyledContentContainer = styled.div`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(8)};
|
||||
@ -28,6 +29,7 @@ export const Invite = () => {
|
||||
|
||||
const { form } = useSignInUpForm();
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
const currentUser = useRecoilValue(currentUserState);
|
||||
const [addUserToWorkspace] = useAddUserToWorkspaceMutation();
|
||||
const [addUserToWorkspaceByInviteToken] =
|
||||
useAddUserToWorkspaceByInviteTokenMutation();
|
||||
@ -77,7 +79,7 @@ export const Invite = () => {
|
||||
<Logo secondaryLogo={workspaceFromInviteHash?.logo} />
|
||||
</AnimatedEaseIn>
|
||||
<Title animate>{title}</Title>
|
||||
{isDefined(currentWorkspace) ? (
|
||||
{isDefined(currentUser) ? (
|
||||
<>
|
||||
<StyledContentContainer>
|
||||
<MainButton
|
||||
@ -91,7 +93,7 @@ export const Invite = () => {
|
||||
<FooterNote />
|
||||
</>
|
||||
) : (
|
||||
<SignInUpForm />
|
||||
<SignInUpWorkspaceScopeForm />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@ -19,7 +19,7 @@ import { useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { useSetRecoilState, useRecoilValue } from 'recoil';
|
||||
import { AnimatedEaseIn, MainButton } from 'twenty-ui';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
@ -27,6 +27,7 @@ import {
|
||||
useValidatePasswordResetTokenQuery,
|
||||
} from '~/generated/graphql';
|
||||
import { logError } from '~/utils/logError';
|
||||
import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState';
|
||||
|
||||
const validationSchema = z
|
||||
.object({
|
||||
@ -71,6 +72,8 @@ const StyledInputContainer = styled.div`
|
||||
export const PasswordReset = () => {
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
|
||||
const workspacePublicData = useRecoilValue(workspacePublicDataState);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [email, setEmail] = useState('');
|
||||
@ -163,7 +166,7 @@ export const PasswordReset = () => {
|
||||
isTokenValid && (
|
||||
<StyledMainContainer>
|
||||
<AnimatedEaseIn>
|
||||
<Logo />
|
||||
<Logo secondaryLogo={workspacePublicData?.logo} />
|
||||
</AnimatedEaseIn>
|
||||
<Title animate>Reset Password</Title>
|
||||
<StyledContentContainer>
|
||||
|
||||
@ -1,68 +0,0 @@
|
||||
/* @license Enterprise */
|
||||
|
||||
import { FooterNote } from '@/auth/sign-in-up/components/FooterNote';
|
||||
import { HorizontalSeparator, MainButton } from 'twenty-ui';
|
||||
import { useSSO } from '@/auth/sign-in-up/hooks/useSSO';
|
||||
import { availableSSOIdentityProvidersState } from '@/auth/states/availableWorkspacesForSSO';
|
||||
import { guessSSOIdentityProviderIconByUrl } from '@/settings/security/utils/guessSSOIdentityProviderIconByUrl';
|
||||
import { DEFAULT_WORKSPACE_NAME } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceName';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
const StyledContentContainer = styled.div`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(8)};
|
||||
margin-top: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledTitle = styled.h2`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
margin: 0;
|
||||
`;
|
||||
|
||||
export const SSOWorkspaceSelection = () => {
|
||||
const availableSSOIdentityProviders = useRecoilValue(
|
||||
availableSSOIdentityProvidersState,
|
||||
);
|
||||
|
||||
const { redirectToSSOLoginPage } = useSSO();
|
||||
|
||||
const availableWorkspacesForSSOGroupByWorkspace =
|
||||
availableSSOIdentityProviders.reduce(
|
||||
(acc, idp) => {
|
||||
acc[idp.workspace.id] = [...(acc[idp.workspace.id] ?? []), idp];
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, typeof availableSSOIdentityProviders>,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledContentContainer>
|
||||
{Object.values(availableWorkspacesForSSOGroupByWorkspace).map(
|
||||
(idps) => (
|
||||
<>
|
||||
<StyledTitle>
|
||||
{idps[0].workspace.displayName ?? DEFAULT_WORKSPACE_NAME}
|
||||
</StyledTitle>
|
||||
<HorizontalSeparator visible={false} />
|
||||
{idps.map((idp) => (
|
||||
<>
|
||||
<MainButton
|
||||
title={idp.name}
|
||||
onClick={() => redirectToSSOLoginPage(idp.id)}
|
||||
Icon={guessSSOIdentityProviderIconByUrl(idp.issuer)}
|
||||
fullWidth
|
||||
/>
|
||||
<HorizontalSeparator visible={false} />
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
),
|
||||
)}
|
||||
</StyledContentContainer>
|
||||
<FooterNote />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,58 +1,70 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp';
|
||||
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
|
||||
import { SignInUpStep } from '@/auth/states/signInUpStepState';
|
||||
import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState';
|
||||
|
||||
import { SignInUpGlobalScopeForm } from '@/auth/sign-in-up/components/SignInUpGlobalScopeForm';
|
||||
import { FooterNote } from '@/auth/sign-in-up/components/FooterNote';
|
||||
import { AnimatedEaseIn } from 'twenty-ui';
|
||||
import { Logo } from '@/auth/components/Logo';
|
||||
import { Title } from '@/auth/components/Title';
|
||||
import { SignInUpForm } from '@/auth/sign-in-up/components/SignInUpForm';
|
||||
import { SignInUpMode, useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp';
|
||||
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { SignInUpStep } from '@/auth/states/signInUpStepState';
|
||||
import { IconLockCustom } from '@ui/display/icon/components/IconLock';
|
||||
import { AnimatedEaseIn } from 'twenty-ui';
|
||||
import { SignInUpWorkspaceScopeForm } from '@/auth/sign-in-up/components/SignInUpWorkspaceScopeForm';
|
||||
import { DEFAULT_WORKSPACE_NAME } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceName';
|
||||
import { SignInUpSSOIdentityProviderSelection } from '@/auth/sign-in-up/components/SignInUpSSOIdentityProviderSelection';
|
||||
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
|
||||
import { useUrlManager } from '@/url-manager/hooks/useUrlManager';
|
||||
import { useMemo } from 'react';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { SSOWorkspaceSelection } from './SSOWorkspaceSelection';
|
||||
|
||||
export const SignInUp = () => {
|
||||
const { form } = useSignInUpForm();
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
const { signInUpStep } = useSignInUp(form);
|
||||
const { isTwentyHomePage, isTwentyWorkspaceSubdomain } = useUrlManager();
|
||||
|
||||
const { signInUpStep, signInUpMode } = useSignInUp(form);
|
||||
const workspacePublicData = useRecoilValue(workspacePublicDataState);
|
||||
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
|
||||
|
||||
const signInUpForm = useMemo(() => {
|
||||
if (isTwentyHomePage && isMultiWorkspaceEnabled) {
|
||||
return <SignInUpGlobalScopeForm />;
|
||||
}
|
||||
|
||||
const title = useMemo(() => {
|
||||
if (
|
||||
signInUpStep === SignInUpStep.Init ||
|
||||
signInUpStep === SignInUpStep.Email
|
||||
(!isMultiWorkspaceEnabled ||
|
||||
(isMultiWorkspaceEnabled && isTwentyWorkspaceSubdomain)) &&
|
||||
signInUpStep === SignInUpStep.SSOIdentityProviderSelection
|
||||
) {
|
||||
return 'Welcome to Twenty';
|
||||
return <SignInUpSSOIdentityProviderSelection />;
|
||||
}
|
||||
if (signInUpStep === SignInUpStep.SSOWorkspaceSelection) {
|
||||
return 'Choose SSO connection';
|
||||
}
|
||||
return signInUpMode === SignInUpMode.SignIn
|
||||
? 'Sign in to Twenty'
|
||||
: 'Sign up to Twenty';
|
||||
}, [signInUpMode, signInUpStep]);
|
||||
|
||||
if (isDefined(currentWorkspace)) {
|
||||
return <></>;
|
||||
}
|
||||
if (
|
||||
isDefined(workspacePublicData) &&
|
||||
(!isMultiWorkspaceEnabled || isTwentyWorkspaceSubdomain)
|
||||
) {
|
||||
return <SignInUpWorkspaceScopeForm />;
|
||||
}
|
||||
|
||||
return <SignInUpGlobalScopeForm />;
|
||||
}, [
|
||||
isTwentyHomePage,
|
||||
isMultiWorkspaceEnabled,
|
||||
isTwentyWorkspaceSubdomain,
|
||||
signInUpStep,
|
||||
workspacePublicData,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AnimatedEaseIn>
|
||||
{signInUpStep === SignInUpStep.SSOWorkspaceSelection ? (
|
||||
<IconLockCustom size={40} />
|
||||
) : (
|
||||
<Logo />
|
||||
)}
|
||||
<Logo secondaryLogo={workspacePublicData?.logo} />
|
||||
</AnimatedEaseIn>
|
||||
<Title animate>{title}</Title>
|
||||
{signInUpStep === SignInUpStep.SSOWorkspaceSelection ? (
|
||||
<SSOWorkspaceSelection />
|
||||
) : (
|
||||
<SignInUpForm />
|
||||
)}
|
||||
<Title animate>
|
||||
{`Welcome to ${workspacePublicData?.displayName ?? DEFAULT_WORKSPACE_NAME}`}
|
||||
</Title>
|
||||
{signInUpForm}
|
||||
{signInUpStep !== SignInUpStep.Password && <FooterNote />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -2,7 +2,7 @@ import styled from '@emotion/styled';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useCallback } from 'react';
|
||||
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { useSetRecoilState, useRecoilValue } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { H2Title, Loader, MainButton } from 'twenty-ui';
|
||||
import { z } from 'zod';
|
||||
@ -22,6 +22,9 @@ import {
|
||||
useActivateWorkspaceMutation,
|
||||
} from '~/generated/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { useUrlManager } from '@/url-manager/hooks/useUrlManager';
|
||||
|
||||
const StyledContentContainer = styled.div`
|
||||
width: 100%;
|
||||
@ -47,6 +50,8 @@ type Form = z.infer<typeof validationSchema>;
|
||||
export const CreateWorkspace = () => {
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const onboardingStatus = useOnboardingStatus();
|
||||
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
|
||||
const { redirectToWorkspace } = useUrlManager();
|
||||
|
||||
const [activateWorkspace] = useActivateWorkspaceMutation();
|
||||
const apolloMetadataClient = useApolloMetadataClient();
|
||||
@ -75,8 +80,19 @@ export const CreateWorkspace = () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
setIsCurrentUserLoaded(false);
|
||||
|
||||
if (isDefined(result.data) && isMultiWorkspaceEnabled) {
|
||||
return redirectToWorkspace(
|
||||
result.data.activateWorkspace.workspace.subdomain,
|
||||
AppPath.Verify,
|
||||
{
|
||||
loginToken: result.data.activateWorkspace.loginToken.token,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
await apolloMetadataClient?.refetchQueries({
|
||||
include: [FIND_MANY_OBJECT_METADATA_ITEMS],
|
||||
});
|
||||
@ -93,7 +109,9 @@ export const CreateWorkspace = () => {
|
||||
[
|
||||
activateWorkspace,
|
||||
setIsCurrentUserLoaded,
|
||||
isMultiWorkspaceEnabled,
|
||||
apolloMetadataClient,
|
||||
redirectToWorkspace,
|
||||
enqueueSnackBar,
|
||||
],
|
||||
);
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import { GithubVersionLink, H2Title, Section } from 'twenty-ui';
|
||||
import { GithubVersionLink, H2Title, Section, IconWorld } from 'twenty-ui';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { DeleteWorkspace } from '@/settings/profile/components/DeleteWorkspace';
|
||||
@ -9,39 +13,61 @@ import { WorkspaceLogoUploader } from '@/settings/workspace/components/Workspace
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import packageJson from '../../../package.json';
|
||||
export const SettingsWorkspace = () => (
|
||||
<SubMenuTopBarContainer
|
||||
title="General"
|
||||
links={[
|
||||
{
|
||||
children: 'Workspace',
|
||||
href: getSettingsPagePath(SettingsPath.Workspace),
|
||||
},
|
||||
{ children: 'General' },
|
||||
]}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<Section>
|
||||
<H2Title title="Picture" />
|
||||
<WorkspaceLogoUploader />
|
||||
</Section>
|
||||
<Section>
|
||||
<H2Title title="Name" description="Name of your workspace" />
|
||||
<NameField />
|
||||
</Section>
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Support"
|
||||
adornment={<ToggleImpersonate />}
|
||||
description="Grant Twenty support temporary access to your workspace so we can troubleshoot problems or recover content on your behalf. You can revoke access at any time."
|
||||
/>
|
||||
</Section>
|
||||
<Section>
|
||||
<DeleteWorkspace />
|
||||
</Section>
|
||||
<Section>
|
||||
<GithubVersionLink version={packageJson.version} />
|
||||
</Section>
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
import { SettingsCard } from '@/settings/components/SettingsCard';
|
||||
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
|
||||
|
||||
const StyledLink = styled(Link)`
|
||||
text-decoration: none;
|
||||
`;
|
||||
|
||||
export const SettingsWorkspace = () => {
|
||||
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
title="General"
|
||||
links={[
|
||||
{
|
||||
children: 'Workspace',
|
||||
href: getSettingsPagePath(SettingsPath.Workspace),
|
||||
},
|
||||
{ children: 'General' },
|
||||
]}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<Section>
|
||||
<H2Title title="Picture" />
|
||||
<WorkspaceLogoUploader />
|
||||
</Section>
|
||||
<Section>
|
||||
<H2Title title="Name" description="Name of your workspace" />
|
||||
<NameField />
|
||||
</Section>
|
||||
{isMultiWorkspaceEnabled && (
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Domain"
|
||||
description="Edit your subdomain name or set a custom domain."
|
||||
/>
|
||||
<StyledLink to={getSettingsPagePath(SettingsPath.Domain)}>
|
||||
<SettingsCard title="Customize Domain" Icon={<IconWorld />} />
|
||||
</StyledLink>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Support"
|
||||
adornment={<ToggleImpersonate />}
|
||||
description="Grant Twenty support temporary access to your workspace so we can troubleshoot problems or recover content on your behalf. You can revoke access at any time."
|
||||
/>
|
||||
</Section>
|
||||
<Section>
|
||||
<DeleteWorkspace />
|
||||
</Section>
|
||||
<Section>
|
||||
<GithubVersionLink version={packageJson.version} />
|
||||
</Section>
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -9,6 +9,9 @@ import { SettingsSecurityOptionsList } from '@/settings/security/components/Sett
|
||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { isSSOEnabledState } from '@/client-config/states/isSSOEnabledState';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
width: 100%;
|
||||
@ -26,6 +29,10 @@ const StyledSSOSection = styled(Section)`
|
||||
`;
|
||||
|
||||
export const SettingsSecurity = () => {
|
||||
const isSSOEnabled = useRecoilValue(isSSOEnabledState);
|
||||
const isSSOSectionDisplay =
|
||||
useIsFeatureEnabled('IS_SSO_ENABLED') && isSSOEnabled;
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
title="Security"
|
||||
@ -40,26 +47,28 @@ export const SettingsSecurity = () => {
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<StyledMainContent>
|
||||
<StyledSSOSection>
|
||||
<H2Title
|
||||
title="SSO"
|
||||
description="Configure an SSO connection"
|
||||
adornment={
|
||||
<Tag
|
||||
text={'Enterprise'}
|
||||
color={'transparent'}
|
||||
Icon={IconLock}
|
||||
variant={'border'}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<SettingsSSOIdentitiesProvidersListCard />
|
||||
</StyledSSOSection>
|
||||
{isSSOSectionDisplay && (
|
||||
<StyledSSOSection>
|
||||
<H2Title
|
||||
title="SSO"
|
||||
description="Configure an SSO connection"
|
||||
adornment={
|
||||
<Tag
|
||||
text={'Enterprise'}
|
||||
color={'transparent'}
|
||||
Icon={IconLock}
|
||||
variant={'border'}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<SettingsSSOIdentitiesProvidersListCard />
|
||||
</StyledSSOSection>
|
||||
)}
|
||||
<Section>
|
||||
<AdvancedSettingsWrapper>
|
||||
<StyledContainer>
|
||||
<H2Title
|
||||
title="Other"
|
||||
title="Authentication"
|
||||
description="Customize your workspace security"
|
||||
/>
|
||||
<SettingsSecurityOptionsList />
|
||||
|
||||
@ -0,0 +1,154 @@
|
||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { H2Title, Section } from 'twenty-ui';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import styled from '@emotion/styled';
|
||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useUpdateWorkspaceMutation } from '~/generated/graphql';
|
||||
import { useUrlManager } from '@/url-manager/hooks/useUrlManager';
|
||||
import { urlManagerState } from '@/url-manager/states/url-manager.state';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
const validationSchema = z
|
||||
.object({
|
||||
subdomain: z
|
||||
.string()
|
||||
.min(1, { message: 'Subdomain can not be empty' })
|
||||
.max(63, { message: 'Subdomain can not be longer than 63 characters' }),
|
||||
})
|
||||
.required();
|
||||
|
||||
type Form = z.infer<typeof validationSchema>;
|
||||
|
||||
const StyledDomainFromWrapper = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const StyledDomain = styled.h2`
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
margin-left: 8px;
|
||||
`;
|
||||
|
||||
export const SettingsDomain = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const urlManager = useRecoilValue(urlManagerState);
|
||||
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const [updateWorkspace] = useUpdateWorkspaceMutation();
|
||||
const { buildWorkspaceUrl } = useUrlManager();
|
||||
|
||||
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
|
||||
currentWorkspaceState,
|
||||
);
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const values = getValues();
|
||||
|
||||
if (!values || !isValid || !currentWorkspace) {
|
||||
throw new Error('Invalid form values');
|
||||
}
|
||||
|
||||
await updateWorkspace({
|
||||
variables: {
|
||||
input: {
|
||||
subdomain: values.subdomain,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
setCurrentWorkspace({
|
||||
...currentWorkspace,
|
||||
subdomain: values.subdomain,
|
||||
});
|
||||
|
||||
window.location.href = buildWorkspaceUrl(values.subdomain);
|
||||
} catch (error) {
|
||||
enqueueSnackBar((error as Error).message, {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
control,
|
||||
getValues,
|
||||
formState: { isValid },
|
||||
} = useForm<Form>({
|
||||
mode: 'onChange',
|
||||
defaultValues: {
|
||||
subdomain: currentWorkspace?.subdomain ?? '',
|
||||
},
|
||||
resolver: zodResolver(validationSchema),
|
||||
});
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
title="General"
|
||||
links={[
|
||||
{
|
||||
children: 'Workspace',
|
||||
href: getSettingsPagePath(SettingsPath.Workspace),
|
||||
},
|
||||
{
|
||||
children: 'General',
|
||||
href: getSettingsPagePath(SettingsPath.Workspace),
|
||||
},
|
||||
{ children: 'Domain' },
|
||||
]}
|
||||
actionButton={
|
||||
<SaveAndCancelButtons
|
||||
isSaveDisabled={!isValid}
|
||||
onCancel={() => navigate(getSettingsPagePath(SettingsPath.Workspace))}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Domain"
|
||||
description="Set the name of your subdomain"
|
||||
/>
|
||||
{currentWorkspace?.subdomain && (
|
||||
<StyledDomainFromWrapper>
|
||||
<Controller
|
||||
name="subdomain"
|
||||
control={control}
|
||||
render={({
|
||||
field: { onChange, value },
|
||||
fieldState: { error },
|
||||
}) => (
|
||||
<TextInputV2
|
||||
value={value}
|
||||
type="text"
|
||||
onChange={onChange}
|
||||
error={error?.message}
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{isDefined(urlManager) && isDefined(urlManager.frontDomain) && (
|
||||
<StyledDomain>.{urlManager.frontDomain}</StyledDomain>
|
||||
)}
|
||||
</StyledDomainFromWrapper>
|
||||
)}
|
||||
</Section>
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user