feat: Add the workspace logo on Twenty logo on the invited route (#1136)
* Add the workspace logo on Twenty logo on the invited route Co-authored-by: v1b3m <vibenjamin6@gmail.com> Co-authored-by: Mael FOSSO <fosso.mael.elvis@gmail.com> * Add minor refactors Co-authored-by: v1b3m <vibenjamin6@gmail.com> Co-authored-by: Mael FOSSO <fosso.mael.elvis@gmail.com> * Refactor the invite logic Co-authored-by: v1b3m <vibenjamin6@gmail.com> Co-authored-by: Mael FOSSO <fosso.mael.elvis@gmail.com> --------- Co-authored-by: v1b3m <vibenjamin6@gmail.com> Co-authored-by: Mael FOSSO <fosso.mael.elvis@gmail.com>
This commit is contained in:
@ -1769,6 +1769,7 @@ export type Query = {
|
||||
findManyWorkspaceMember: Array<WorkspaceMember>;
|
||||
findUniqueCompany: Company;
|
||||
findUniquePerson: Person;
|
||||
findWorkspaceFromInviteHash: Workspace;
|
||||
};
|
||||
|
||||
|
||||
@ -1881,6 +1882,11 @@ export type QueryFindUniquePersonArgs = {
|
||||
id: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryFindWorkspaceFromInviteHashArgs = {
|
||||
inviteHash: Scalars['String'];
|
||||
};
|
||||
|
||||
export enum QueryMode {
|
||||
Default = 'default',
|
||||
Insensitive = 'insensitive'
|
||||
@ -2787,6 +2793,13 @@ export type GetWorkspaceMembersQueryVariables = Exact<{ [key: string]: never; }>
|
||||
|
||||
export type GetWorkspaceMembersQuery = { __typename?: 'Query', workspaceMembers: Array<{ __typename?: 'WorkspaceMember', id: string, user: { __typename?: 'User', id: string, email: string, avatarUrl?: string | null, firstName?: string | null, lastName?: string | null, displayName: string } }> };
|
||||
|
||||
export type GetWorkspaceFromInviteHashQueryVariables = Exact<{
|
||||
inviteHash: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetWorkspaceFromInviteHashQuery = { __typename?: 'Query', findWorkspaceFromInviteHash: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null } };
|
||||
|
||||
export type UpdateWorkspaceMutationVariables = Exact<{
|
||||
data: WorkspaceUpdateInput;
|
||||
}>;
|
||||
@ -5444,6 +5457,43 @@ export function useGetWorkspaceMembersLazyQuery(baseOptions?: Apollo.LazyQueryHo
|
||||
export type GetWorkspaceMembersQueryHookResult = ReturnType<typeof useGetWorkspaceMembersQuery>;
|
||||
export type GetWorkspaceMembersLazyQueryHookResult = ReturnType<typeof useGetWorkspaceMembersLazyQuery>;
|
||||
export type GetWorkspaceMembersQueryResult = Apollo.QueryResult<GetWorkspaceMembersQuery, GetWorkspaceMembersQueryVariables>;
|
||||
export const GetWorkspaceFromInviteHashDocument = gql`
|
||||
query GetWorkspaceFromInviteHash($inviteHash: String!) {
|
||||
findWorkspaceFromInviteHash(inviteHash: $inviteHash) {
|
||||
id
|
||||
displayName
|
||||
logo
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useGetWorkspaceFromInviteHashQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useGetWorkspaceFromInviteHashQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useGetWorkspaceFromInviteHashQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useGetWorkspaceFromInviteHashQuery({
|
||||
* variables: {
|
||||
* inviteHash: // value for 'inviteHash'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useGetWorkspaceFromInviteHashQuery(baseOptions: Apollo.QueryHookOptions<GetWorkspaceFromInviteHashQuery, GetWorkspaceFromInviteHashQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<GetWorkspaceFromInviteHashQuery, GetWorkspaceFromInviteHashQueryVariables>(GetWorkspaceFromInviteHashDocument, options);
|
||||
}
|
||||
export function useGetWorkspaceFromInviteHashLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetWorkspaceFromInviteHashQuery, GetWorkspaceFromInviteHashQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<GetWorkspaceFromInviteHashQuery, GetWorkspaceFromInviteHashQueryVariables>(GetWorkspaceFromInviteHashDocument, options);
|
||||
}
|
||||
export type GetWorkspaceFromInviteHashQueryHookResult = ReturnType<typeof useGetWorkspaceFromInviteHashQuery>;
|
||||
export type GetWorkspaceFromInviteHashLazyQueryHookResult = ReturnType<typeof useGetWorkspaceFromInviteHashLazyQuery>;
|
||||
export type GetWorkspaceFromInviteHashQueryResult = Apollo.QueryResult<GetWorkspaceFromInviteHashQuery, GetWorkspaceFromInviteHashQueryVariables>;
|
||||
export const UpdateWorkspaceDocument = gql`
|
||||
mutation UpdateWorkspace($data: WorkspaceUpdateInput!) {
|
||||
updateWorkspace(data: $data) {
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
type Props = React.ComponentProps<'div'>;
|
||||
import { getImageAbsoluteURIOrBase64 } from '@/users/utils/getProfilePictureAbsoluteURI';
|
||||
|
||||
type Props = React.ComponentProps<'div'> & {
|
||||
workspaceLogo?: string | null;
|
||||
};
|
||||
|
||||
const StyledLogo = styled.div`
|
||||
height: 48px;
|
||||
@ -12,12 +16,29 @@ const StyledLogo = styled.div`
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
position: relative;
|
||||
width: 48px;
|
||||
`;
|
||||
|
||||
export function Logo(props: Props) {
|
||||
type StyledWorkspaceLogoProps = {
|
||||
logo?: string | null;
|
||||
};
|
||||
|
||||
const StyledWorkspaceLogo = styled.div<StyledWorkspaceLogoProps>`
|
||||
background: url(${(props) => props.logo});
|
||||
background-size: cover;
|
||||
border-radius: ${({ theme }) => theme.border.radius.xs};
|
||||
bottom: ${({ theme }) => `-${theme.spacing(3)}`};
|
||||
height: ${({ theme }) => theme.spacing(6)};
|
||||
position: absolute;
|
||||
right: ${({ theme }) => `-${theme.spacing(3)}`};
|
||||
width: ${({ theme }) => theme.spacing(6)};
|
||||
`;
|
||||
|
||||
export function Logo({ workspaceLogo, ...props }: Props) {
|
||||
return (
|
||||
<StyledLogo {...props}>
|
||||
<StyledWorkspaceLogo logo={getImageAbsoluteURIOrBase64(workspaceLogo)} />
|
||||
<img src="/icons/android/android-launchericon-192-192.png" alt="logo" />
|
||||
</StyledLogo>
|
||||
);
|
||||
|
||||
@ -58,6 +58,7 @@ export function SignInUpForm() {
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
},
|
||||
workspace,
|
||||
} = useSignInUp();
|
||||
const theme = useTheme();
|
||||
|
||||
@ -73,16 +74,22 @@ export function SignInUpForm() {
|
||||
return signInUpMode === SignInUpMode.SignIn ? 'Sign in' : 'Sign up';
|
||||
}, [signInUpMode, signInUpStep]);
|
||||
|
||||
const title = useMemo(() => {
|
||||
if (signInUpMode === SignInUpMode.Invite) {
|
||||
return `Join ${workspace?.displayName ?? ''} Team`;
|
||||
}
|
||||
|
||||
return signInUpMode === SignInUpMode.SignIn
|
||||
? 'Sign in to Twenty'
|
||||
: 'Sign up to Twenty';
|
||||
}, [signInUpMode, workspace?.displayName]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AnimatedEaseIn>
|
||||
<Logo />
|
||||
<Logo workspaceLogo={workspace?.logo} />
|
||||
</AnimatedEaseIn>
|
||||
<Title animate>
|
||||
{signInUpMode === SignInUpMode.SignIn
|
||||
? 'Sign in to Twenty'
|
||||
: 'Sign up to Twenty'}
|
||||
</Title>
|
||||
<Title animate>{title}</Title>
|
||||
<StyledContentContainer>
|
||||
{authProviders.google && (
|
||||
<>
|
||||
|
||||
@ -11,6 +11,7 @@ import { AppPath } from '@/types/AppPath';
|
||||
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
||||
import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { useGetWorkspaceFromInviteHashQuery } from '~/generated/graphql';
|
||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
||||
|
||||
import { useAuth } from '../../hooks/useAuth';
|
||||
@ -19,6 +20,7 @@ import { PASSWORD_REGEX } from '../../utils/passwordRegex';
|
||||
export enum SignInUpMode {
|
||||
SignIn = 'sign-in',
|
||||
SignUp = 'sign-up',
|
||||
Invite = 'invite',
|
||||
}
|
||||
|
||||
export enum SignInUpStep {
|
||||
@ -50,13 +52,21 @@ export function useSignInUp() {
|
||||
const [signInUpStep, setSignInUpStep] = useState<SignInUpStep>(
|
||||
SignInUpStep.Init,
|
||||
);
|
||||
const [signInUpMode, setSignInUpMode] = useState<SignInUpMode>(
|
||||
isMatchingLocation(AppPath.SignIn)
|
||||
const [signInUpMode, setSignInUpMode] = useState<SignInUpMode>(() => {
|
||||
if (isMatchingLocation(AppPath.Invite)) {
|
||||
return SignInUpMode.Invite;
|
||||
}
|
||||
|
||||
return isMatchingLocation(AppPath.SignIn)
|
||||
? SignInUpMode.SignIn
|
||||
: SignInUpMode.SignUp,
|
||||
);
|
||||
: SignInUpMode.SignUp;
|
||||
});
|
||||
const [showErrors, setShowErrors] = useState(false);
|
||||
|
||||
const { data: workspace } = useGetWorkspaceFromInviteHashQuery({
|
||||
variables: { inviteHash: workspaceInviteHash || '' },
|
||||
});
|
||||
|
||||
const form = useForm<Form>({
|
||||
mode: 'onChange',
|
||||
defaultValues: {
|
||||
@ -171,5 +181,6 @@ export function useSignInUp() {
|
||||
goBackToEmailStep,
|
||||
submitCredentials,
|
||||
form,
|
||||
workspace: workspace?.findWorkspaceFromInviteHash,
|
||||
};
|
||||
}
|
||||
|
||||
@ -15,3 +15,13 @@ export const GET_WORKSPACE_MEMBERS = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_WORKSPACE_FROM_INVITE_HASH = gql`
|
||||
query GetWorkspaceFromInviteHash($inviteHash: String!) {
|
||||
findWorkspaceFromInviteHash(inviteHash: $inviteHash) {
|
||||
id
|
||||
displayName
|
||||
logo
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -12,8 +12,10 @@ import { AppBasePath } from '@/types/AppBasePath';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar';
|
||||
import { TableHotkeyScope } from '@/ui/table/types/TableHotkeyScope';
|
||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||
import { useGetWorkspaceFromInviteHashLazyQuery } from '~/generated/graphql';
|
||||
import { ActivityType, CommentableType } from '~/generated/graphql';
|
||||
|
||||
import { useIsMatchingLocation } from '../hooks/useIsMatchingLocation';
|
||||
@ -21,6 +23,7 @@ import { useIsMatchingLocation } from '../hooks/useIsMatchingLocation';
|
||||
export function AuthAutoRouter() {
|
||||
const navigate = useNavigate();
|
||||
const isMatchingLocation = useIsMatchingLocation();
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
|
||||
const [previousLocation, setPreviousLocation] = useState('');
|
||||
|
||||
@ -32,6 +35,8 @@ export function AuthAutoRouter() {
|
||||
|
||||
const eventTracker = useEventTracker();
|
||||
|
||||
const [workspaceFromInviteHashQuery] =
|
||||
useGetWorkspaceFromInviteHashLazyQuery();
|
||||
const { addToCommandMenu, setToIntitialCommandMenu } = useCommandMenu();
|
||||
|
||||
const openCreateActivity = useOpenCreateActivityDrawer();
|
||||
@ -57,6 +62,13 @@ export function AuthAutoRouter() {
|
||||
isMatchingLocation(AppPath.CreateWorkspace) ||
|
||||
isMatchingLocation(AppPath.CreateProfile);
|
||||
|
||||
function navigateToSignUp() {
|
||||
enqueueSnackBar('workspace does not exist', {
|
||||
variant: 'error',
|
||||
});
|
||||
navigate(AppPath.SignUp);
|
||||
}
|
||||
|
||||
if (
|
||||
onboardingStatus === OnboardingStatus.OngoingUserCreation &&
|
||||
!isMachinOngoingUserCreationRoute
|
||||
@ -77,6 +89,24 @@ export function AuthAutoRouter() {
|
||||
isMatchingOnboardingRoute
|
||||
) {
|
||||
navigate('/');
|
||||
} else if (isMatchingLocation(AppPath.Invite)) {
|
||||
const inviteHash =
|
||||
matchPath({ path: '/invite/:workspaceInviteHash' }, location.pathname)
|
||||
?.params.workspaceInviteHash || '';
|
||||
|
||||
workspaceFromInviteHashQuery({
|
||||
variables: {
|
||||
inviteHash,
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
if (!data.findWorkspaceFromInviteHash) {
|
||||
navigateToSignUp();
|
||||
}
|
||||
},
|
||||
onError: (_) => {
|
||||
navigateToSignUp();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
switch (true) {
|
||||
@ -222,6 +252,8 @@ export function AuthAutoRouter() {
|
||||
location,
|
||||
previousLocation,
|
||||
eventTracker,
|
||||
workspaceFromInviteHashQuery,
|
||||
enqueueSnackBar,
|
||||
addToCommandMenu,
|
||||
openCreateActivity,
|
||||
setToIntitialCommandMenu,
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { WorkspaceService } from 'src/core/workspace/services/workspace.service';
|
||||
|
||||
import { AuthResolver } from './auth.resolver';
|
||||
|
||||
import { TokenService } from './services/token.service';
|
||||
@ -12,6 +14,10 @@ describe('AuthResolver', () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
AuthResolver,
|
||||
{
|
||||
provide: WorkspaceService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: AuthService,
|
||||
useValue: {},
|
||||
|
||||
@ -15,6 +15,8 @@ import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
|
||||
import { AuthUser } from 'src/decorators/auth-user.decorator';
|
||||
import { assert } from 'src/utils/assert';
|
||||
import { User } from 'src/core/@generated/user/user.model';
|
||||
import { Workspace } from 'src/core/@generated/workspace/workspace.model';
|
||||
import { WorkspaceService } from 'src/core/workspace/services/workspace.service';
|
||||
|
||||
import { AuthTokens } from './dto/token.entity';
|
||||
import { TokenService } from './services/token.service';
|
||||
@ -34,6 +36,7 @@ import { ImpersonateInput } from './dto/impersonate.input';
|
||||
@Resolver()
|
||||
export class AuthResolver {
|
||||
constructor(
|
||||
private workspaceService: WorkspaceService,
|
||||
private authService: AuthService,
|
||||
private tokenService: TokenService,
|
||||
) {}
|
||||
@ -57,6 +60,17 @@ export class AuthResolver {
|
||||
);
|
||||
}
|
||||
|
||||
@Query(() => Workspace)
|
||||
async findWorkspaceFromInviteHash(
|
||||
@Args() workspaceInviteHashValidInput: WorkspaceInviteHashValidInput,
|
||||
) {
|
||||
return await this.workspaceService.findFirst({
|
||||
where: {
|
||||
inviteHash: workspaceInviteHashValidInput.inviteHash,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => LoginToken)
|
||||
async challenge(@Args() challengeInput: ChallengeInput): Promise<LoginToken> {
|
||||
const user = await this.authService.challenge(challengeInput);
|
||||
|
||||
Reference in New Issue
Block a user