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:
gitstart-twenty
2023-08-10 06:00:07 +08:00
committed by GitHub
parent b49c857dc5
commit 7dcbc56e69
8 changed files with 163 additions and 12 deletions

View File

@ -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) {

View File

@ -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>
);

View File

@ -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 && (
<>

View File

@ -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,
};
}

View File

@ -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
}
}
`;

View File

@ -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,

View File

@ -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: {},

View File

@ -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);