feat: redesign project sharing & initial registration
redesigned the project sharing popup to be a multi select dropdown that populates the options by using the input as a fuzzy search filter on the current users & invited users. users can now also be directly invited by email from the project share window. if invited this way, then the user will receive an email that sends them to a registration page, then a confirmation page. the initial registration was always redone so that it uses a similar system to the above in that it now will accept the first registered user if there are no other accounts (besides 'system').
This commit is contained in:
@ -51,7 +51,9 @@ export const Default = () => {
|
||||
},
|
||||
},
|
||||
]}
|
||||
invitedUsers={[]}
|
||||
onAddUser={action('add user')}
|
||||
onDeleteInvitedUser={action('delete invited user')}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
</>
|
||||
|
@ -104,8 +104,8 @@ type TeamRoleManagerPopupProps = {
|
||||
user: User;
|
||||
users: Array<User>;
|
||||
warning?: string | null;
|
||||
canChangeRole: boolean;
|
||||
onChangeRole: (roleCode: RoleCode) => void;
|
||||
canChangeRole?: boolean;
|
||||
onChangeRole?: (roleCode: RoleCode) => void;
|
||||
updateUserPassword?: (user: TaskUser, password: string) => void;
|
||||
onDeleteUser?: (userID: string, newOwnerID: string | null) => void;
|
||||
};
|
||||
@ -530,8 +530,10 @@ type AdminProps = {
|
||||
onDeleteUser: (userID: string, newOwnerID: string | null) => void;
|
||||
onInviteUser: ($target: React.RefObject<HTMLElement>) => void;
|
||||
users: Array<User>;
|
||||
invitedUsers: Array<InvitedUserAccount>;
|
||||
canInviteUser: boolean;
|
||||
onUpdateUserPassword: (user: TaskUser, password: string) => void;
|
||||
onDeleteInvitedUser: (invitedUserID: string) => void;
|
||||
};
|
||||
|
||||
const Admin: React.FC<AdminProps> = ({
|
||||
@ -540,7 +542,9 @@ const Admin: React.FC<AdminProps> = ({
|
||||
onUpdateUserPassword,
|
||||
canInviteUser,
|
||||
onDeleteUser,
|
||||
onDeleteInvitedUser,
|
||||
onInviteUser,
|
||||
invitedUsers,
|
||||
users,
|
||||
}) => {
|
||||
const warning =
|
||||
@ -577,7 +581,7 @@ const Admin: React.FC<AdminProps> = ({
|
||||
<TabContent>
|
||||
<MemberListWrapper>
|
||||
<MemberListHeader>
|
||||
<ListTitle>{`Members (${users.length})`}</ListTitle>
|
||||
<ListTitle>{`Members (${users.length + invitedUsers.length})`}</ListTitle>
|
||||
<ListDesc>
|
||||
Organization admins can create / manage / delete all projects & teams. Members only have access to teams
|
||||
or projects they have been added to.
|
||||
@ -635,6 +639,65 @@ const Admin: React.FC<AdminProps> = ({
|
||||
</MemberListItem>
|
||||
);
|
||||
})}
|
||||
{invitedUsers.map(member => {
|
||||
return (
|
||||
<MemberListItem>
|
||||
<MemberProfile
|
||||
showRoleIcons
|
||||
size={32}
|
||||
onMemberProfile={NOOP}
|
||||
member={{
|
||||
id: member.id,
|
||||
fullName: member.email,
|
||||
profileIcon: {
|
||||
bgColor: '#fff',
|
||||
url: null,
|
||||
initials: member.email.charAt(0),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<MemberListItemDetails>
|
||||
<MemberItemName>{member.email}</MemberItemName>
|
||||
<MemberItemUsername>Invited</MemberItemUsername>
|
||||
</MemberListItemDetails>
|
||||
<MemberItemOptions>
|
||||
<MemberItemOption
|
||||
variant="outline"
|
||||
onClick={$target => {
|
||||
showPopup(
|
||||
$target,
|
||||
<TeamRoleManagerPopup
|
||||
user={{
|
||||
id: member.id,
|
||||
fullName: member.email,
|
||||
profileIcon: {
|
||||
bgColor: '#fff',
|
||||
url: null,
|
||||
initials: member.email.charAt(0),
|
||||
},
|
||||
member: {
|
||||
teams: [],
|
||||
projects: [],
|
||||
},
|
||||
owned: {
|
||||
teams: [],
|
||||
projects: [],
|
||||
},
|
||||
}}
|
||||
users={users}
|
||||
onDeleteUser={() => {
|
||||
onDeleteInvitedUser(member.id);
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
}}
|
||||
>
|
||||
Manage
|
||||
</MemberItemOption>
|
||||
</MemberItemOptions>
|
||||
</MemberListItem>
|
||||
);
|
||||
})}
|
||||
</MemberList>
|
||||
</MemberListWrapper>
|
||||
</TabContent>
|
||||
|
@ -35,11 +35,15 @@ const Base = styled.button<{ color: string; disabled: boolean }>`
|
||||
`}
|
||||
`;
|
||||
|
||||
const Filled = styled(Base)`
|
||||
const Filled = styled(Base)<{ hoverVariant: HoverVariant }>`
|
||||
background: rgba(${props => props.theme.colors[props.color]});
|
||||
&:hover {
|
||||
box-shadow: 0 8px 25px -8px rgba(${props => props.theme.colors[props.color]});
|
||||
}
|
||||
${props =>
|
||||
props.hoverVariant === 'boxShadow' &&
|
||||
css`
|
||||
&:hover {
|
||||
box-shadow: 0 8px 25px -8px rgba(${props.theme.colors[props.color]});
|
||||
}
|
||||
`}
|
||||
`;
|
||||
const Outline = styled(Base)<{ invert: boolean }>`
|
||||
border: 1px solid rgba(${props => props.theme.colors[props.color]});
|
||||
@ -123,9 +127,11 @@ const Relief = styled(Base)`
|
||||
}
|
||||
`;
|
||||
|
||||
type HoverVariant = 'boxShadow' | 'none';
|
||||
type ButtonProps = {
|
||||
fontSize?: string;
|
||||
variant?: 'filled' | 'outline' | 'flat' | 'lineDown' | 'gradient' | 'relief';
|
||||
hoverVariant?: HoverVariant;
|
||||
color?: 'primary' | 'danger' | 'success' | 'warning' | 'dark';
|
||||
disabled?: boolean;
|
||||
type?: 'button' | 'submit';
|
||||
@ -142,6 +148,7 @@ const Button: React.FC<ButtonProps> = ({
|
||||
invert = false,
|
||||
color = 'primary',
|
||||
variant = 'filled',
|
||||
hoverVariant = 'boxShadow',
|
||||
type = 'button',
|
||||
justifyTextContent = 'center',
|
||||
icon,
|
||||
@ -158,7 +165,15 @@ const Button: React.FC<ButtonProps> = ({
|
||||
switch (variant) {
|
||||
case 'filled':
|
||||
return (
|
||||
<Filled ref={$button} type={type} onClick={handleClick} className={className} disabled={disabled} color={color}>
|
||||
<Filled
|
||||
ref={$button}
|
||||
hoverVariant={hoverVariant}
|
||||
type={type}
|
||||
onClick={handleClick}
|
||||
className={className}
|
||||
disabled={disabled}
|
||||
color={color}
|
||||
>
|
||||
{icon && icon}
|
||||
<Text hasIcon={typeof icon !== 'undefined'} justifyTextContent={justifyTextContent} fontSize={fontSize}>
|
||||
{children}
|
||||
|
103
frontend/src/shared/components/Confirm/Styles.ts
Normal file
103
frontend/src/shared/components/Confirm/Styles.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import styled from 'styled-components';
|
||||
import Button from 'shared/components/Button';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
background: #eff2f7;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
`;
|
||||
|
||||
export const Column = styled.div`
|
||||
width: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const LoginFormWrapper = styled.div`
|
||||
background: #10163a;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const LoginFormContainer = styled.div`
|
||||
min-height: 505px;
|
||||
padding: 2rem;
|
||||
`;
|
||||
|
||||
export const Title = styled.h1`
|
||||
color: #ebeefd;
|
||||
font-size: 18px;
|
||||
margin-bottom: 14px;
|
||||
`;
|
||||
|
||||
export const SubTitle = styled.h2`
|
||||
color: #c2c6dc;
|
||||
font-size: 14px;
|
||||
margin-bottom: 14px;
|
||||
`;
|
||||
export const Form = styled.form`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
export const FormLabel = styled.label`
|
||||
color: #c2c6dc;
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
margin-top: 14px;
|
||||
`;
|
||||
|
||||
export const FormTextInput = styled.input`
|
||||
width: 100%;
|
||||
background: #262c49;
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
margin-top: 4px;
|
||||
padding: 0.7rem 1rem 0.7rem 3rem;
|
||||
font-size: 1rem;
|
||||
color: #c2c6dc;
|
||||
border-radius: 5px;
|
||||
`;
|
||||
|
||||
export const FormIcon = styled.div`
|
||||
top: 30px;
|
||||
left: 16px;
|
||||
position: absolute;
|
||||
`;
|
||||
|
||||
export const FormError = styled.span`
|
||||
font-size: 0.875rem;
|
||||
color: rgb(234, 84, 85);
|
||||
`;
|
||||
|
||||
export const LoginButton = styled(Button)``;
|
||||
|
||||
export const ActionButtons = styled.div`
|
||||
margin-top: 17.5px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
export const RegisterButton = styled(Button)``;
|
||||
|
||||
export const LogoTitle = styled.div`
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-left: 12px;
|
||||
transition: visibility, opacity, transform 0.25s ease;
|
||||
color: #7367f0;
|
||||
`;
|
||||
|
||||
export const LogoWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: 24px;
|
||||
color: rgb(222, 235, 255);
|
||||
border-bottom: 1px solid rgba(65, 69, 97, 0.65);
|
||||
`;
|
62
frontend/src/shared/components/Confirm/index.tsx
Normal file
62
frontend/src/shared/components/Confirm/index.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import AccessAccount from 'shared/undraw/AccessAccount';
|
||||
import { User, Lock, Taskcafe } from 'shared/icons';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import LoadingSpinner from 'shared/components/LoadingSpinner';
|
||||
import {
|
||||
Form,
|
||||
LogoWrapper,
|
||||
LogoTitle,
|
||||
ActionButtons,
|
||||
RegisterButton,
|
||||
FormError,
|
||||
FormIcon,
|
||||
FormLabel,
|
||||
FormTextInput,
|
||||
Wrapper,
|
||||
Column,
|
||||
LoginFormWrapper,
|
||||
LoginFormContainer,
|
||||
Title,
|
||||
SubTitle,
|
||||
} from './Styles';
|
||||
|
||||
const Confirm = ({ onConfirmUser, hasConfirmToken }: ConfirmProps) => {
|
||||
const [hasFailed, setFailed] = useState(false);
|
||||
const setHasFailed = () => {
|
||||
setFailed(true);
|
||||
};
|
||||
useEffect(() => {
|
||||
onConfirmUser(setHasFailed);
|
||||
});
|
||||
return (
|
||||
<Wrapper>
|
||||
<Column>
|
||||
<AccessAccount width={275} height={250} />
|
||||
</Column>
|
||||
<Column>
|
||||
<LoginFormWrapper>
|
||||
<LoginFormContainer>
|
||||
<LogoWrapper>
|
||||
<Taskcafe width={42} height={42} />
|
||||
<LogoTitle>Taskcafé</LogoTitle>
|
||||
</LogoWrapper>
|
||||
{hasConfirmToken ? (
|
||||
<>
|
||||
<Title>Confirming user...</Title>
|
||||
{hasFailed ? <SubTitle>There was an error while confirming your user</SubTitle> : <LoadingSpinner />}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Title>There is no confirmation token</Title>
|
||||
<SubTitle>There seems to have been an error.</SubTitle>
|
||||
</>
|
||||
)}
|
||||
</LoginFormContainer>
|
||||
</LoginFormWrapper>
|
||||
</Column>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Confirm;
|
@ -73,7 +73,6 @@ export const HeaderName = styled(TextareaAutosize)`
|
||||
box-shadow: none;
|
||||
font-weight: 600;
|
||||
margin: -4px 0;
|
||||
padding: 4px 8px;
|
||||
|
||||
letter-spacing: normal;
|
||||
word-spacing: normal;
|
||||
|
@ -47,6 +47,7 @@ const permissions = [
|
||||
type MiniProfileProps = {
|
||||
bio: string;
|
||||
user: TaskUser;
|
||||
invited?: boolean;
|
||||
onRemoveFromTask?: () => void;
|
||||
onChangeRole?: (roleCode: RoleCode) => void;
|
||||
onRemoveFromBoard?: () => void;
|
||||
@ -56,6 +57,7 @@ type MiniProfileProps = {
|
||||
const MiniProfile: React.FC<MiniProfileProps> = ({
|
||||
user,
|
||||
bio,
|
||||
invited,
|
||||
canChangeRole,
|
||||
onRemoveFromTask,
|
||||
onChangeRole,
|
||||
@ -74,7 +76,7 @@ const MiniProfile: React.FC<MiniProfileProps> = ({
|
||||
)}
|
||||
<ProfileInfo>
|
||||
<InfoTitle>{user.fullName}</InfoTitle>
|
||||
<InfoUsername>{`@${user.username}`}</InfoUsername>
|
||||
{invited ? <InfoUsername>Invited</InfoUsername> : <InfoUsername>{`@${user.username}`}</InfoUsername>}
|
||||
<InfoBio>{bio}</InfoBio>
|
||||
</ProfileInfo>
|
||||
</Profile>
|
||||
|
@ -24,7 +24,7 @@ import {
|
||||
const EMAIL_PATTERN = /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/i;
|
||||
const INITIALS_PATTERN = /[a-zA-Z]{2,3}/i;
|
||||
|
||||
const Register = ({ onSubmit }: RegisterProps) => {
|
||||
const Register = ({ onSubmit, registered = false }: RegisterProps) => {
|
||||
const [isComplete, setComplete] = useState(true);
|
||||
const { register, handleSubmit, errors, setError } = useForm<RegisterFormData>();
|
||||
const loginSubmit = (data: RegisterFormData) => {
|
||||
@ -43,103 +43,112 @@ const Register = ({ onSubmit }: RegisterProps) => {
|
||||
<Taskcafe width={42} height={42} />
|
||||
<LogoTitle>Taskcafé</LogoTitle>
|
||||
</LogoWrapper>
|
||||
<Title>Register</Title>
|
||||
<SubTitle>Please create the system admin user</SubTitle>
|
||||
<Form onSubmit={handleSubmit(loginSubmit)}>
|
||||
<FormLabel htmlFor="fullname">
|
||||
Full name
|
||||
<FormTextInput
|
||||
type="text"
|
||||
id="fullname"
|
||||
name="fullname"
|
||||
ref={register({ required: 'Full name is required' })}
|
||||
/>
|
||||
<FormIcon>
|
||||
<User width={20} height={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.username && <FormError>{errors.username.message}</FormError>}
|
||||
<FormLabel htmlFor="username">
|
||||
Username
|
||||
<FormTextInput
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
ref={register({ required: 'Username is required' })}
|
||||
/>
|
||||
<FormIcon>
|
||||
<User width={20} height={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.username && <FormError>{errors.username.message}</FormError>}
|
||||
<FormLabel htmlFor="email">
|
||||
Email
|
||||
<FormTextInput
|
||||
type="text"
|
||||
id="email"
|
||||
name="email"
|
||||
ref={register({
|
||||
required: 'Email is required',
|
||||
pattern: { value: EMAIL_PATTERN, message: 'Must be a valid email' },
|
||||
})}
|
||||
/>
|
||||
<FormIcon>
|
||||
<User width={20} height={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.email && <FormError>{errors.email.message}</FormError>}
|
||||
<FormLabel htmlFor="initials">
|
||||
Initials
|
||||
<FormTextInput
|
||||
type="text"
|
||||
id="initials"
|
||||
name="initials"
|
||||
ref={register({
|
||||
required: 'Initials is required',
|
||||
pattern: {
|
||||
value: INITIALS_PATTERN,
|
||||
message: 'Initials must be between 2 to 3 characters.',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
<FormIcon>
|
||||
<User width={20} height={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.initials && <FormError>{errors.initials.message}</FormError>}
|
||||
<FormLabel htmlFor="password">
|
||||
Password
|
||||
<FormTextInput
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
ref={register({ required: 'Password is required' })}
|
||||
/>
|
||||
<FormIcon>
|
||||
<Lock width={20} height={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.password && <FormError>{errors.password.message}</FormError>}
|
||||
<FormLabel htmlFor="password_confirm">
|
||||
Password (Confirm)
|
||||
<FormTextInput
|
||||
type="password"
|
||||
id="password_confirm"
|
||||
name="password_confirm"
|
||||
ref={register({ required: 'Password (confirm) is required' })}
|
||||
/>
|
||||
<FormIcon>
|
||||
<Lock width={20} height={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.password_confirm && <FormError>{errors.password_confirm.message}</FormError>}
|
||||
{registered ? (
|
||||
<>
|
||||
<Title>Thanks for registering</Title>
|
||||
<SubTitle>Please check your inbox for a confirmation email.</SubTitle>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Title>Register</Title>
|
||||
<SubTitle>Please create your user</SubTitle>
|
||||
<Form onSubmit={handleSubmit(loginSubmit)}>
|
||||
<FormLabel htmlFor="fullname">
|
||||
Full name
|
||||
<FormTextInput
|
||||
type="text"
|
||||
id="fullname"
|
||||
name="fullname"
|
||||
ref={register({ required: 'Full name is required' })}
|
||||
/>
|
||||
<FormIcon>
|
||||
<User width={20} height={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.username && <FormError>{errors.username.message}</FormError>}
|
||||
<FormLabel htmlFor="username">
|
||||
Username
|
||||
<FormTextInput
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
ref={register({ required: 'Username is required' })}
|
||||
/>
|
||||
<FormIcon>
|
||||
<User width={20} height={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.username && <FormError>{errors.username.message}</FormError>}
|
||||
<FormLabel htmlFor="email">
|
||||
Email
|
||||
<FormTextInput
|
||||
type="text"
|
||||
id="email"
|
||||
name="email"
|
||||
ref={register({
|
||||
required: 'Email is required',
|
||||
pattern: { value: EMAIL_PATTERN, message: 'Must be a valid email' },
|
||||
})}
|
||||
/>
|
||||
<FormIcon>
|
||||
<User width={20} height={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.email && <FormError>{errors.email.message}</FormError>}
|
||||
<FormLabel htmlFor="initials">
|
||||
Initials
|
||||
<FormTextInput
|
||||
type="text"
|
||||
id="initials"
|
||||
name="initials"
|
||||
ref={register({
|
||||
required: 'Initials is required',
|
||||
pattern: {
|
||||
value: INITIALS_PATTERN,
|
||||
message: 'Initials must be between 2 to 3 characters.',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
<FormIcon>
|
||||
<User width={20} height={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.initials && <FormError>{errors.initials.message}</FormError>}
|
||||
<FormLabel htmlFor="password">
|
||||
Password
|
||||
<FormTextInput
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
ref={register({ required: 'Password is required' })}
|
||||
/>
|
||||
<FormIcon>
|
||||
<Lock width={20} height={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.password && <FormError>{errors.password.message}</FormError>}
|
||||
<FormLabel htmlFor="password_confirm">
|
||||
Password (Confirm)
|
||||
<FormTextInput
|
||||
type="password"
|
||||
id="password_confirm"
|
||||
name="password_confirm"
|
||||
ref={register({ required: 'Password (confirm) is required' })}
|
||||
/>
|
||||
<FormIcon>
|
||||
<Lock width={20} height={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.password_confirm && <FormError>{errors.password_confirm.message}</FormError>}
|
||||
|
||||
<ActionButtons>
|
||||
<RegisterButton type="submit" disabled={!isComplete}>
|
||||
Register
|
||||
</RegisterButton>
|
||||
</ActionButtons>
|
||||
</Form>
|
||||
<ActionButtons>
|
||||
<RegisterButton type="submit" disabled={!isComplete}>
|
||||
Register
|
||||
</RegisterButton>
|
||||
</ActionButtons>
|
||||
</Form>
|
||||
</>
|
||||
)}
|
||||
</LoginFormContainer>
|
||||
</LoginFormWrapper>
|
||||
</Column>
|
||||
|
@ -16,7 +16,7 @@ function getBackgroundColor(isDisabled: boolean, isSelected: boolean, isFocused:
|
||||
return null;
|
||||
}
|
||||
|
||||
const colourStyles = {
|
||||
export const colourStyles = {
|
||||
control: (styles: any, data: any) => {
|
||||
return {
|
||||
...styles,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import styled, { css } from 'styled-components';
|
||||
import { DoubleChevronUp, Crown } from 'shared/icons';
|
||||
|
||||
export const AdminIcon = styled(DoubleChevronUp)`
|
||||
@ -24,7 +24,12 @@ const TaskDetailAssignee = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export const Wrapper = styled.div<{ size: number | string; bgColor: string | null; backgroundURL: string | null }>`
|
||||
export const Wrapper = styled.div<{
|
||||
size: number | string;
|
||||
bgColor: string | null;
|
||||
backgroundURL: string | null;
|
||||
hasClick: boolean;
|
||||
}>`
|
||||
width: ${props => props.size}px;
|
||||
height: ${props => props.size}px;
|
||||
border-radius: 9999px;
|
||||
@ -37,33 +42,60 @@ export const Wrapper = styled.div<{ size: number | string; bgColor: string | nul
|
||||
background-size: contain;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
${props =>
|
||||
props.hasClick &&
|
||||
css`
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
type TaskAssigneeProps = {
|
||||
size: number | string;
|
||||
showRoleIcons?: boolean;
|
||||
member: TaskUser;
|
||||
onMemberProfile: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
|
||||
invited?: boolean;
|
||||
onMemberProfile?: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const TaskAssignee: React.FC<TaskAssigneeProps> = ({ showRoleIcons, member, onMemberProfile, size, className }) => {
|
||||
const TaskAssignee: React.FC<TaskAssigneeProps> = ({
|
||||
showRoleIcons,
|
||||
member,
|
||||
invited,
|
||||
onMemberProfile,
|
||||
size,
|
||||
className,
|
||||
}) => {
|
||||
const $memberRef = useRef<HTMLDivElement>(null);
|
||||
let profileIcon: ProfileIcon = {
|
||||
url: null,
|
||||
bgColor: null,
|
||||
initials: null,
|
||||
};
|
||||
if (member.profileIcon) {
|
||||
profileIcon = member.profileIcon;
|
||||
}
|
||||
return (
|
||||
<TaskDetailAssignee
|
||||
className={className}
|
||||
ref={$memberRef}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onMemberProfile($memberRef, member.id);
|
||||
if (onMemberProfile) {
|
||||
onMemberProfile($memberRef, member.id);
|
||||
}
|
||||
}}
|
||||
key={member.id}
|
||||
>
|
||||
<Wrapper backgroundURL={member.profileIcon.url ?? null} bgColor={member.profileIcon.bgColor ?? null} size={size}>
|
||||
{(!member.profileIcon.url && member.profileIcon.initials) ?? ''}
|
||||
<Wrapper
|
||||
hasClick={typeof onMemberProfile !== undefined}
|
||||
backgroundURL={profileIcon.url ?? null}
|
||||
bgColor={profileIcon.bgColor ?? null}
|
||||
size={size}
|
||||
>
|
||||
{(!profileIcon.url && profileIcon.initials) ?? ''}
|
||||
</Wrapper>
|
||||
{showRoleIcons && member.role && member.role.code === 'admin' && <AdminIcon width={10} height={10} />}
|
||||
{showRoleIcons && member.role && member.role.code === 'owner' && <OwnerIcon width={10} height={10} />}
|
||||
|
@ -1,10 +1,11 @@
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import { Home, Star, Bell, AngleDown, BarChart, CheckCircle } from 'shared/icons';
|
||||
import { Home, Star, Bell, AngleDown, BarChart, CheckCircle, ListUnordered } from 'shared/icons';
|
||||
import styled from 'styled-components';
|
||||
import ProfileIcon from 'shared/components/ProfileIcon';
|
||||
import { usePopup } from 'shared/components/PopupMenu';
|
||||
import { RoleCode } from 'shared/generated/graphql';
|
||||
import NOOP from 'shared/utils/noop';
|
||||
import { useHistory } from 'react-router';
|
||||
import {
|
||||
TaskcafeLogo,
|
||||
TaskcafeTitle,
|
||||
@ -173,8 +174,11 @@ type NavBarProps = {
|
||||
user: TaskUser | null;
|
||||
onOpenSettings: ($target: React.RefObject<HTMLElement>) => void;
|
||||
projectMembers?: Array<TaskUser> | null;
|
||||
projectInvitedMembers?: Array<InvitedUser> | null;
|
||||
|
||||
onRemoveFromBoard?: (userID: string) => void;
|
||||
onMemberProfile?: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
|
||||
onInvitedMemberProfile?: ($targetRef: React.RefObject<HTMLElement>, email: string) => void;
|
||||
};
|
||||
|
||||
const NavBar: React.FC<NavBarProps> = ({
|
||||
@ -184,10 +188,12 @@ const NavBar: React.FC<NavBarProps> = ({
|
||||
onChangeProjectOwner,
|
||||
currentTab,
|
||||
onMemberProfile,
|
||||
onInvitedMemberProfile,
|
||||
canEditProjectName = false,
|
||||
onOpenProjectFinder,
|
||||
onFavorite,
|
||||
onSetTab,
|
||||
projectInvitedMembers,
|
||||
onChangeRole,
|
||||
name,
|
||||
onRemoveFromBoard,
|
||||
@ -204,6 +210,7 @@ const NavBar: React.FC<NavBarProps> = ({
|
||||
onProfileClick($target);
|
||||
}
|
||||
};
|
||||
const history = useHistory();
|
||||
const { showPopup } = usePopup();
|
||||
return (
|
||||
<NavbarWrapper>
|
||||
@ -245,19 +252,38 @@ const NavBar: React.FC<NavBarProps> = ({
|
||||
<TaskcafeTitle>Taskcafé</TaskcafeTitle>
|
||||
</LogoContainer>
|
||||
<GlobalActions>
|
||||
{projectMembers && onMemberProfile && (
|
||||
{projectMembers && projectInvitedMembers && onMemberProfile && onInvitedMemberProfile && (
|
||||
<>
|
||||
<ProjectMembers>
|
||||
{projectMembers.map((member, idx) => (
|
||||
<ProjectMember
|
||||
showRoleIcons
|
||||
zIndex={projectMembers.length - idx}
|
||||
zIndex={projectMembers.length - idx + projectInvitedMembers.length}
|
||||
key={member.id}
|
||||
size={28}
|
||||
member={member}
|
||||
onMemberProfile={onMemberProfile}
|
||||
/>
|
||||
))}
|
||||
{projectInvitedMembers.map((member, idx) => (
|
||||
<ProjectMember
|
||||
showRoleIcons
|
||||
zIndex={projectInvitedMembers.length - idx}
|
||||
key={member.email}
|
||||
size={28}
|
||||
invited
|
||||
member={{
|
||||
id: member.email,
|
||||
fullName: member.email,
|
||||
profileIcon: {
|
||||
url: null,
|
||||
initials: member.email.charAt(0),
|
||||
bgColor: '#fff',
|
||||
},
|
||||
}}
|
||||
onMemberProfile={onInvitedMemberProfile}
|
||||
/>
|
||||
))}
|
||||
{canInviteUser && (
|
||||
<InviteButton
|
||||
onClick={$target => {
|
||||
@ -283,6 +309,9 @@ const NavBar: React.FC<NavBarProps> = ({
|
||||
<IconContainer disabled onClick={NOOP}>
|
||||
<CheckCircle width={20} height={20} />
|
||||
</IconContainer>
|
||||
<IconContainer disabled onClick={NOOP}>
|
||||
<ListUnordered width={20} height={20} />
|
||||
</IconContainer>
|
||||
<IconContainer disabled onClick={onNotificationClick}>
|
||||
<Bell color="#c2c6dc" size={20} />
|
||||
</IconContainer>
|
||||
|
Reference in New Issue
Block a user