feature: add user project count to Admin component

This commit is contained in:
Jordan Knott
2020-07-17 19:40:05 -05:00
parent ccaa97e2bb
commit 68fa7aef94
23 changed files with 1140 additions and 140 deletions

View File

@ -12,7 +12,7 @@ import {
import Input from 'shared/components/Input';
import styled from 'styled-components';
import Button from 'shared/components/Button';
import { useForm } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form';
import { usePopup, Popup } from 'shared/components/PopupMenu';
import produce from 'immer';
import updateApolloCache from 'shared/utils/cache';
@ -45,14 +45,19 @@ const DeleteUserPopup: React.FC<DeleteUserPopupProps> = ({ onDeleteUser }) => {
</DeleteUserWrapper>
);
};
type RoleCodeOption = {
label: string;
value: string;
};
type CreateUserData = {
email: string;
username: string;
fullName: string;
initials: string;
password: string;
roleCode: string;
roleCode: RoleCodeOption;
};
const CreateUserForm = styled.form`
display: flex;
flex-direction: column;
@ -78,11 +83,11 @@ type AddUserPopupProps = {
};
const AddUserPopup: React.FC<AddUserPopupProps> = ({ onAddUser }) => {
const { register, handleSubmit, errors, setValue } = useForm<CreateUserData>();
const [role, setRole] = useState<string | null>(null);
register({ name: 'roleCode' }, { required: true });
const { register, handleSubmit, errors, setValue, control } = useForm<CreateUserData>();
console.log(errors);
const createUser = (data: CreateUserData) => {
console.log(data);
onAddUser(data);
};
return (
@ -106,19 +111,23 @@ const AddUserPopup: React.FC<AddUserPopupProps> = ({ onAddUser }) => {
variant="alternate"
ref={register({ required: 'Email is required' })}
/>
<Select
label="Role"
value={role}
options={[
{ label: 'Admin', value: 'admin' },
{ label: 'Member', value: 'member' },
]}
onChange={newRole => {
setRole(newRole);
setValue('roleCode', newRole.value);
}}
<Controller
control={control}
name="roleCode"
rules={{ required: 'Role is required' }}
render={({ onChange, onBlur, value }) => (
<Select
label="Role"
value={value}
onChange={onChange}
options={[
{ label: 'Admin', value: 'admin' },
{ label: 'Member', value: 'member' },
]}
/>
)}
/>
{errors.email && <InputError>{errors.email.message}</InputError>}
{errors.roleCode && errors.roleCode.value && <InputError>{errors.roleCode.value.message}</InputError>}
<AddUserInput
floatingLabel
width="100%"
@ -146,6 +155,7 @@ const AddUserPopup: React.FC<AddUserPopupProps> = ({ onAddUser }) => {
id="password"
name="password"
variant="alternate"
type="password"
ref={register({ required: 'Password is required' })}
/>
{errors.password && <InputError>{errors.password.message}</InputError>}
@ -196,7 +206,7 @@ const AdminRoute = () => {
<>
<GlobalTopNavbar projectID={null} onSaveProjectName={() => {}} name={null} />
<Admin
initialTab={1}
initialTab={0}
users={data.users}
onInviteUser={() => {}}
onUpdateUserPassword={(user, password) => {
@ -223,7 +233,8 @@ const AdminRoute = () => {
<Popup tab={0} title="Add member" onClose={() => hidePopup()}>
<AddUserPopup
onAddUser={user => {
createUser({ variables: { ...user } });
const { roleCode, ...userData } = user;
createUser({ variables: { ...userData, roleCode: roleCode.value } });
hidePopup();
}}
/>

View File

@ -87,11 +87,12 @@ const CreateChecklistPopup: React.FC<CreateChecklistPopupProps> = ({ onCreateChe
const createUser = (data: CreateChecklistData) => {
onCreateChecklist(data);
};
console.log(errors);
return (
<CreateChecklistForm onSubmit={handleSubmit(createUser)}>
<CreateChecklistInput
floatingLabel
autoFocus
autoSelect
defaultValue="Checklist"
width="100%"
label="Name"
@ -437,7 +438,7 @@ const Details: React.FC<DetailsProps> = ({
showPopup(
$target,
<Popup
title={'Add checklist'}
title="Add checklist"
tab={0}
onClose={() => {
hidePopup();

View File

@ -178,6 +178,8 @@ const Project = () => {
FindProjectDocument,
cache =>
produce(cache, draftCache => {
console.log(cache);
console.log(response);
draftCache.findProject.members = cache.findProject.members.filter(
m => m.id !== response.data.deleteProjectMember.member.id,
);

View File

@ -159,8 +159,8 @@ export const RemoveMemberButton = styled(Button)`
`;
type TeamRoleManagerPopupProps = {
currentUserID: string;
subject: TaskUser;
members: Array<TaskUser>;
subject: User;
members: Array<User>;
warning?: string | null;
canChangeRole: boolean;
onChangeRole: (roleCode: RoleCode) => void;

View File

@ -22,13 +22,19 @@ type Role = {
name: string;
};
type User = {
type UserProject = {
id: string;
fullName: string;
username: string;
email: string;
role: Role;
profileIcon: ProfileIcon;
name: string;
};
type UserTeam = {
id: string;
name: string;
};
type RelatedList = {
teams: Array<UserTeam>;
projects: Array<UserProject>;
};
type OwnedList = {
@ -42,7 +48,12 @@ type TaskUser = {
profileIcon: ProfileIcon;
username?: string;
role?: Role;
owned?: OwnedList | null;
};
type User = TaskUser & {
email?: string;
member: RelatedList;
owned: RelatedList;
};
type RefreshTokenResponse = {

View File

@ -1,18 +1,18 @@
import React, {useRef} from 'react';
import React, { useRef } from 'react';
import Admin from '.';
import {theme} from 'App/ThemeStyles';
import { theme } from 'App/ThemeStyles';
import NormalizeStyles from 'App/NormalizeStyles';
import BaseStyles from 'App/BaseStyles';
import {ThemeProvider} from 'styled-components';
import {action} from '@storybook/addon-actions';
import { ThemeProvider } from 'styled-components';
import { action } from '@storybook/addon-actions';
export default {
component: Admin,
title: 'Admin',
parameters: {
backgrounds: [
{name: 'gray', value: '#f8f8f8', default: true},
{name: 'white', value: '#ffffff'},
{ name: 'gray', value: '#f8f8f8', default: true },
{ name: 'white', value: '#ffffff' },
],
},
};
@ -33,13 +33,21 @@ export const Default = () => {
id: '1',
username: 'jordanthedev',
email: 'jordan@jordanthedev.com',
role: {code: 'admin', name: 'Admin'},
role: { code: 'admin', name: 'Admin' },
fullName: 'Jordan Knott',
profileIcon: {
bgColor: '#fff',
initials: 'JK',
url: null,
},
owned: {
teams: [{ id: '1', name: 'Team' }],
projects: [{ id: '2', name: 'Project' }],
},
member: {
teams: [],
projects: [],
},
},
]}
onAddUser={action('add user')}

View File

@ -509,7 +509,7 @@ const ListTable: React.FC<ListTableProps> = ({ users, onDeleteUser }) => {
rowSelection="multiple"
defaultColDef={data.defaultColDef}
columnDefs={data.columnDefs}
rowData={users.map(u => ({ ...u, roleName: u.role.name }))}
rowData={users.map(u => ({ ...u, roleName: 'member' }))}
frameworkComponents={data.frameworkComponents}
onFirstDataRendered={params => {
params.api.sizeColumnsToFit();
@ -717,46 +717,49 @@ const Admin: React.FC<AdminProps> = ({
</ListActions>
</MemberListHeader>
<MemberList>
{users.map(member => (
<MemberListItem>
<MemberProfile showRoleIcons size={32} onMemberProfile={() => {}} member={member} />
<MemberListItemDetails>
<MemberItemName>{member.fullName}</MemberItemName>
<MemberItemUsername>{`@${member.username}`}</MemberItemUsername>
</MemberListItemDetails>
<MemberItemOptions>
<MemberItemOption variant="flat">On 6 projects</MemberItemOption>
<MemberItemOption
variant="outline"
onClick={$target => {
showPopup(
$target,
<TeamRoleManagerPopup
user={member}
warning={member.role && member.role.code === 'owner' ? warning : null}
updateUserPassword={(user, password) => {
onUpdateUserPassword(user, password);
}}
canChangeRole={member.role && member.role.code !== 'owner'}
onChangeRole={roleCode => {
updateUserRole({ variables: { userID: member.id, roleCode } });
}}
onRemoveFromTeam={
member.role && member.role.code === 'owner'
? undefined
: () => {
hidePopup();
}
}
/>,
);
}}
>
Manage
</MemberItemOption>
</MemberItemOptions>
</MemberListItem>
))}
{users.map(member => {
const projectTotal = member.owned.projects.length + member.member.projects.length;
return (
<MemberListItem>
<MemberProfile showRoleIcons size={32} onMemberProfile={() => {}} member={member} />
<MemberListItemDetails>
<MemberItemName>{member.fullName}</MemberItemName>
<MemberItemUsername>{`@${member.username}`}</MemberItemUsername>
</MemberListItemDetails>
<MemberItemOptions>
<MemberItemOption variant="flat">{`On ${projectTotal} projects`}</MemberItemOption>
<MemberItemOption
variant="outline"
onClick={$target => {
showPopup(
$target,
<TeamRoleManagerPopup
user={member}
warning={member.role && member.role.code === 'owner' ? warning : null}
updateUserPassword={(user, password) => {
onUpdateUserPassword(user, password);
}}
canChangeRole={(member.role && member.role.code !== 'owner') ?? false}
onChangeRole={roleCode => {
updateUserRole({ variables: { userID: member.id, roleCode } });
}}
onRemoveFromTeam={
member.role && member.role.code === 'owner'
? undefined
: () => {
hidePopup();
}
}
/>,
);
}}
>
Manage
</MemberItemOption>
</MemberItemOptions>
</MemberListItem>
);
})}
</MemberList>
</MemberListWrapper>
</TabContent>

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import styled, { css } from 'styled-components/macro';
const InputWrapper = styled.div<{ width: string }>`
@ -85,6 +85,8 @@ type InputProps = {
icon?: JSX.Element;
type?: string;
autocomplete?: boolean;
autoFocus?: boolean;
autoSelect?: boolean;
id?: string;
name?: string;
className?: string;
@ -92,12 +94,32 @@ type InputProps = {
onClick?: (e: React.MouseEvent<HTMLInputElement>) => void;
};
function useCombinedRefs(...refs: any) {
const targetRef = React.useRef();
React.useEffect(() => {
refs.forEach((ref: any) => {
if (!ref) return;
if (typeof ref === 'function') {
ref(targetRef.current);
} else {
ref.current = targetRef.current;
}
});
}, [refs]);
return targetRef;
}
const Input = React.forwardRef(
(
{
width = 'auto',
variant = 'normal',
type = 'text',
autoFocus = false,
autoSelect = false,
autocomplete,
label,
placeholder,
@ -111,9 +133,25 @@ const Input = React.forwardRef(
}: InputProps,
$ref: any,
) => {
const [hasValue, setHasValue] = useState(false);
const [hasValue, setHasValue] = useState(defaultValue !== '');
const borderColor = variant === 'normal' ? 'rgba(0, 0, 0, 0.2)' : '#414561';
const focusBg = variant === 'normal' ? 'rgba(38, 44, 73, )' : 'rgba(16, 22, 58, 1)';
// Merge forwarded ref and internal ref in order to be able to access the ref in the useEffect
// The forwarded ref is not accessible by itself, which is what the innerRef & combined ref is for
// TODO(jordanknott): This is super ugly, find a better approach?
const $innerRef = React.useRef<HTMLInputElement>(null);
const combinedRef: any = useCombinedRefs($ref, $innerRef);
useEffect(() => {
if (combinedRef && combinedRef.current) {
if (autoFocus) {
combinedRef.current.focus();
}
if (autoSelect) {
combinedRef.current.select();
}
}
}, []);
return (
<InputWrapper className={className} width={width}>
<InputInput
@ -121,7 +159,7 @@ const Input = React.forwardRef(
setHasValue((e.currentTarget.value !== '' || floatingLabel) ?? false);
}}
hasValue={hasValue}
ref={$ref}
ref={combinedRef}
id={id}
type={type}
name={name}

View File

@ -67,7 +67,8 @@ export type Member = {
fullName: Scalars['String'];
username: Scalars['String'];
profileIcon: ProfileIcon;
owned?: Maybe<OwnersList>;
owned: OwnedList;
member: MemberList;
};
export type RefreshToken = {
@ -84,6 +85,18 @@ export type Role = {
name: Scalars['String'];
};
export type OwnedList = {
__typename?: 'OwnedList';
teams: Array<Team>;
projects: Array<Project>;
};
export type MemberList = {
__typename?: 'MemberList';
teams: Array<Team>;
projects: Array<Project>;
};
export type UserAccount = {
__typename?: 'UserAccount';
id: Scalars['ID'];
@ -94,6 +107,8 @@ export type UserAccount = {
role: Role;
username: Scalars['String'];
profileIcon: ProfileIcon;
owned: OwnedList;
member: MemberList;
};
export type Team = {
@ -1096,6 +1111,24 @@ export type FindProjectQuery = (
), profileIcon: (
{ __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
), owned: (
{ __typename?: 'OwnedList' }
& { teams: Array<(
{ __typename?: 'Team' }
& Pick<Team, 'id' | 'name'>
)>, projects: Array<(
{ __typename?: 'Project' }
& Pick<Project, 'id' | 'name'>
)> }
), member: (
{ __typename?: 'MemberList' }
& { teams: Array<(
{ __typename?: 'Team' }
& Pick<Team, 'id' | 'name'>
)>, projects: Array<(
{ __typename?: 'Project' }
& Pick<Project, 'id' | 'name'>
)> }
) }
)> }
);
@ -1611,12 +1644,27 @@ export type GetTeamQuery = (
& { role: (
{ __typename?: 'Role' }
& Pick<Role, 'code' | 'name'>
), owned?: Maybe<(
{ __typename?: 'OwnersList' }
& Pick<OwnersList, 'projects' | 'teams'>
)>, profileIcon: (
), profileIcon: (
{ __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
), owned: (
{ __typename?: 'OwnedList' }
& { teams: Array<(
{ __typename?: 'Team' }
& Pick<Team, 'id' | 'name'>
)>, projects: Array<(
{ __typename?: 'Project' }
& Pick<Project, 'id' | 'name'>
)> }
), member: (
{ __typename?: 'MemberList' }
& { teams: Array<(
{ __typename?: 'Team' }
& Pick<Team, 'id' | 'name'>
)>, projects: Array<(
{ __typename?: 'Project' }
& Pick<Project, 'id' | 'name'>
)> }
) }
)> }
), projects: Array<(
@ -1635,6 +1683,24 @@ export type GetTeamQuery = (
), profileIcon: (
{ __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
), owned: (
{ __typename?: 'OwnedList' }
& { teams: Array<(
{ __typename?: 'Team' }
& Pick<Team, 'id' | 'name'>
)>, projects: Array<(
{ __typename?: 'Project' }
& Pick<Project, 'id' | 'name'>
)> }
), member: (
{ __typename?: 'MemberList' }
& { teams: Array<(
{ __typename?: 'Team' }
& Pick<Team, 'id' | 'name'>
)>, projects: Array<(
{ __typename?: 'Project' }
& Pick<Project, 'id' | 'name'>
)> }
) }
)> }
);
@ -1876,6 +1942,24 @@ export type UsersQuery = (
), profileIcon: (
{ __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
), owned: (
{ __typename?: 'OwnedList' }
& { teams: Array<(
{ __typename?: 'Team' }
& Pick<Team, 'id' | 'name'>
)>, projects: Array<(
{ __typename?: 'Project' }
& Pick<Project, 'id' | 'name'>
)> }
), member: (
{ __typename?: 'MemberList' }
& { teams: Array<(
{ __typename?: 'Team' }
& Pick<Team, 'id' | 'name'>
)>, projects: Array<(
{ __typename?: 'Project' }
& Pick<Project, 'id' | 'name'>
)> }
) }
)> }
);
@ -2278,6 +2362,26 @@ export const FindProjectDocument = gql`
initials
bgColor
}
owned {
teams {
id
name
}
projects {
id
name
}
}
member {
teams {
id
name
}
projects {
id
name
}
}
}
}
${TaskFieldsFragmentDoc}`;
@ -3288,15 +3392,31 @@ export const GetTeamDocument = gql`
code
name
}
owned {
projects
teams
}
profileIcon {
url
initials
bgColor
}
owned {
teams {
id
name
}
projects {
id
name
}
}
member {
teams {
id
name
}
projects {
id
name
}
}
}
}
projects(input: {teamID: $teamID}) {
@ -3321,6 +3441,26 @@ export const GetTeamDocument = gql`
initials
bgColor
}
owned {
teams {
id
name
}
projects {
id
name
}
}
member {
teams {
id
name
}
projects {
id
name
}
}
}
}
`;
@ -3834,6 +3974,26 @@ export const UsersDocument = gql`
initials
bgColor
}
owned {
teams {
id
name
}
projects {
id
name
}
}
member {
teams {
id
name
}
projects {
id
name
}
}
}
}
`;

View File

@ -59,6 +59,26 @@ query findProject($projectId: String!) {
initials
bgColor
}
owned {
teams {
id
name
}
projects {
id
name
}
}
member {
teams {
id
name
}
projects {
id
name
}
}
}
${TASK_FRAGMENT}
}

View File

@ -14,15 +14,31 @@ export const GET_TEAM_QUERY = gql`
code
name
}
owned {
projects
teams
}
profileIcon {
url
initials
bgColor
}
owned {
teams {
id
name
}
projects {
id
name
}
}
member {
teams {
id
name
}
projects {
id
name
}
}
}
}
projects(input: { teamID: $teamID }) {
@ -47,6 +63,26 @@ export const GET_TEAM_QUERY = gql`
initials
bgColor
}
owned {
teams {
id
name
}
projects {
id
name
}
}
member {
teams {
id
name
}
projects {
id
name
}
}
}
}
`;

View File

@ -13,6 +13,26 @@ query users {
initials
bgColor
}
owned {
teams {
id
name
}
projects {
id
name
}
}
member {
teams {
id
name
}
projects {
id
name
}
}
}
}