feature: add web & migrate commands

This commit is contained in:
Jordan Knott
2020-07-15 18:20:08 -05:00
parent 1e9813601e
commit 90515f6aa4
31 changed files with 1300 additions and 640 deletions

View File

@ -70,6 +70,7 @@
"styled-components": "^5.0.1",
"typescript": "~3.7.2"
},
"proxy": "http://localhost:3333",
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",

View File

@ -2,19 +2,19 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="apple-touch-icon" href="/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="manifest" href="/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.

View File

@ -3,11 +3,11 @@ import Admin from 'shared/components/Admin';
import Select from 'shared/components/Select';
import GlobalTopNavbar from 'App/TopNavbar';
import {
useUsersQuery,
useDeleteUserAccountMutation,
useCreateUserAccountMutation,
UsersDocument,
UsersQuery,
useUsersQuery,
useDeleteUserAccountMutation,
useCreateUserAccountMutation,
UsersDocument,
UsersQuery,
} from 'shared/generated/graphql';
import Input from 'shared/components/Input';
import styled from 'styled-components';
@ -33,25 +33,25 @@ const DeleteUserButton = styled(Button)`
`;
type DeleteUserPopupProps = {
onDeleteUser: () => void;
onDeleteUser: () => void;
};
const DeleteUserPopup: React.FC<DeleteUserPopupProps> = ({ onDeleteUser }) => {
return (
<DeleteUserWrapper>
<DeleteUserDescription>Deleting this user will remove all user related data.</DeleteUserDescription>
<DeleteUserButton onClick={() => onDeleteUser()} color="danger">
Delete user
return (
<DeleteUserWrapper>
<DeleteUserDescription>Deleting this user will remove all user related data.</DeleteUserDescription>
<DeleteUserButton onClick={() => onDeleteUser()} color="danger">
Delete user
</DeleteUserButton>
</DeleteUserWrapper>
);
</DeleteUserWrapper>
);
};
type CreateUserData = {
email: string;
username: string;
fullName: string;
initials: string;
password: string;
roleCode: string;
email: string;
username: string;
fullName: string;
initials: string;
password: string;
roleCode: string;
};
const CreateUserForm = styled.form`
display: flex;
@ -73,162 +73,167 @@ const InputError = styled.span`
`;
type AddUserPopupProps = {
onAddUser: (user: CreateUserData) => void;
onAddUser: (user: CreateUserData) => void;
};
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 } = useForm<CreateUserData>();
const [role, setRole] = useState<string | null>(null);
register({ name: 'roleCode' }, { required: true });
const createUser = (data: CreateUserData) => {
onAddUser(data);
};
return (
<CreateUserForm onSubmit={handleSubmit(createUser)}>
<AddUserInput
floatingLabel
width="100%"
label="Full Name"
id="fullName"
name="fullName"
variant="alternate"
ref={register({ required: 'Full name is required' })}
/>
{errors.fullName && <InputError>{errors.fullName.message}</InputError>}
<AddUserInput
floatingLabel
width="100%"
label="Email"
id="email"
name="email"
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);
}}
/>
{errors.email && <InputError>{errors.email.message}</InputError>}
<AddUserInput
floatingLabel
width="100%"
label="Username"
id="username"
name="username"
variant="alternate"
ref={register({ required: 'Username is required' })}
/>
{errors.username && <InputError>{errors.username.message}</InputError>}
<AddUserInput
floatingLabel
width="100%"
label="Initials"
id="initials"
name="initials"
variant="alternate"
ref={register({ required: 'Initials is required' })}
/>
{errors.initials && <InputError>{errors.initials.message}</InputError>}
<AddUserInput
floatingLabel
width="100%"
label="Password"
id="password"
name="password"
variant="alternate"
ref={register({ required: 'Password is required' })}
/>
{errors.password && <InputError>{errors.password.message}</InputError>}
<CreateUserButton type="submit">Create</CreateUserButton>
</CreateUserForm>
);
const createUser = (data: CreateUserData) => {
onAddUser(data);
};
return (
<CreateUserForm onSubmit={handleSubmit(createUser)}>
<AddUserInput
floatingLabel
width="100%"
label="Full Name"
id="fullName"
name="fullName"
variant="alternate"
ref={register({ required: 'Full name is required' })}
/>
{errors.fullName && <InputError>{errors.fullName.message}</InputError>}
<AddUserInput
floatingLabel
width="100%"
label="Email"
id="email"
name="email"
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);
}}
/>
{errors.email && <InputError>{errors.email.message}</InputError>}
<AddUserInput
floatingLabel
width="100%"
label="Username"
id="username"
name="username"
variant="alternate"
ref={register({ required: 'Username is required' })}
/>
{errors.username && <InputError>{errors.username.message}</InputError>}
<AddUserInput
floatingLabel
width="100%"
label="Initials"
id="initials"
name="initials"
variant="alternate"
ref={register({ required: 'Initials is required' })}
/>
{errors.initials && <InputError>{errors.initials.message}</InputError>}
<AddUserInput
floatingLabel
width="100%"
label="Password"
id="password"
name="password"
variant="alternate"
ref={register({ required: 'Password is required' })}
/>
{errors.password && <InputError>{errors.password.message}</InputError>}
<CreateUserButton type="submit">Create</CreateUserButton>
</CreateUserForm>
);
};
const AdminRoute = () => {
useEffect(() => {
document.title = 'Citadel | Admin';
}, []);
const { loading, data } = useUsersQuery();
const { showPopup, hidePopup } = usePopup();
const [deleteUser] = useDeleteUserAccountMutation({
update: (client, response) => {
updateApolloCache<UsersQuery>(client, UsersDocument, cache =>
produce(cache, draftCache => {
draftCache.users = cache.users.filter(u => u.id !== response.data.deleteUserAccount.userAccount.id);
}),
);
},
});
const [createUser] = useCreateUserAccountMutation({
update: (client, createData) => {
const cacheData: any = client.readQuery({
query: UsersDocument,
});
console.log(cacheData);
console.log(createData);
const newData = produce(cacheData, (draftState: any) => {
draftState.users = [...draftState.users, { ...createData.data.createUserAccount }];
});
client.writeQuery({
query: UsersDocument,
data: {
...newData,
useEffect(() => {
document.title = 'Citadel | Admin';
}, []);
const { loading, data } = useUsersQuery();
const { showPopup, hidePopup } = usePopup();
const [deleteUser] = useDeleteUserAccountMutation({
update: (client, response) => {
updateApolloCache<UsersQuery>(client, UsersDocument, cache =>
produce(cache, draftCache => {
draftCache.users = cache.users.filter(u => u.id !== response.data.deleteUserAccount.userAccount.id);
}),
);
},
});
},
});
if (loading) {
return <GlobalTopNavbar projectID={null} onSaveProjectName={() => {}} name={null} />;
}
if (data) {
return (
<>
<GlobalTopNavbar projectID={null} onSaveProjectName={() => {}} name={null} />
<Admin
initialTab={1}
users={data.users}
onInviteUser={() => {}}
onDeleteUser={($target, userID) => {
showPopup(
$target,
<Popup tab={0} title="Delete user?" onClose={() => hidePopup()}>
<DeleteUserPopup
onDeleteUser={() => {
deleteUser({ variables: { userID } });
hidePopup();
}}
});
const [createUser] = useCreateUserAccountMutation({
update: (client, createData) => {
const cacheData: any = client.readQuery({
query: UsersDocument,
});
console.log(cacheData);
console.log(createData);
const newData = produce(cacheData, (draftState: any) => {
draftState.users = [...draftState.users, { ...createData.data.createUserAccount }];
});
client.writeQuery({
query: UsersDocument,
data: {
...newData,
},
});
},
});
if (loading) {
return <GlobalTopNavbar projectID={null} onSaveProjectName={() => { }} name={null} />;
}
if (data) {
return (
<>
<GlobalTopNavbar projectID={null} onSaveProjectName={() => { }} name={null} />
<Admin
initialTab={1}
users={data.users}
onInviteUser={() => { }}
onUpdateUserPassword={(user, password) => {
console.log(user)
console.log(password)
hidePopup()
}}
onDeleteUser={($target, userID) => {
showPopup(
$target,
<Popup tab={0} title="Delete user?" onClose={() => hidePopup()}>
<DeleteUserPopup
onDeleteUser={() => {
deleteUser({ variables: { userID } });
hidePopup();
}}
/>
</Popup>,
);
}}
onAddUser={$target => {
showPopup(
$target,
<Popup tab={0} title="Add member" onClose={() => hidePopup()}>
<AddUserPopup
onAddUser={user => {
createUser({ variables: { ...user } });
hidePopup();
}}
/>
</Popup>,
);
}}
/>
</Popup>,
);
}}
onAddUser={$target => {
showPopup(
$target,
<Popup tab={0} title="Add member" onClose={() => hidePopup()}>
<AddUserPopup
onAddUser={user => {
createUser({ variables: { ...user } });
hidePopup();
}}
/>
</Popup>,
);
}}
/>
</>
);
}
return <span>error</span>;
</>
);
}
return <span>error</span>;
};
export default AdminRoute;

View File

@ -1,9 +1,9 @@
import React, { useState, useContext, useEffect } from 'react';
import TopNavbar, { MenuItem } from 'shared/components/TopNavbar';
import React, {useState, useContext, useEffect} from 'react';
import TopNavbar, {MenuItem} from 'shared/components/TopNavbar';
import styled from 'styled-components/macro';
import DropdownMenu, { ProfileMenu } from 'shared/components/DropdownMenu';
import ProjectSettings, { DeleteConfirm, DELETE_INFO } from 'shared/components/ProjectSettings';
import { useHistory } from 'react-router';
import DropdownMenu, {ProfileMenu} from 'shared/components/DropdownMenu';
import ProjectSettings, {DeleteConfirm, DELETE_INFO} from 'shared/components/ProjectSettings';
import {useHistory} from 'react-router';
import UserIDContext from 'App/context';
import {
RoleCode,
@ -12,10 +12,10 @@ import {
useGetProjectsQuery,
GetProjectsDocument,
} from 'shared/generated/graphql';
import { usePopup, Popup } from 'shared/components/PopupMenu';
import { History } from 'history';
import {usePopup, Popup} from 'shared/components/PopupMenu';
import {History} from 'history';
import produce from 'immer';
import { Link } from 'react-router-dom';
import {Link} from 'react-router-dom';
const TeamContainer = styled.div`
display: flex;
@ -45,7 +45,7 @@ const TeamProjectLink = styled(Link)`
user-select: none;
`;
const TeamProjectBackground = styled.div<{ color: string }>`
const TeamProjectBackground = styled.div<{color: string}>`
background-image: url(null);
background-color: ${props => props.color};
@ -68,7 +68,7 @@ const TeamProjectBackground = styled.div<{ color: string }>`
}
`;
const TeamProjectAvatar = styled.div<{ color: string }>`
const TeamProjectAvatar = styled.div<{color: string}>`
background-image: url(null);
background-color: ${props => props.color};
@ -122,12 +122,12 @@ const TeamProjectContainer = styled.div`
const colors = ['#e362e3', '#7a6ff0', '#37c5ab', '#aa62e3', '#e8384f'];
const ProjectFinder = () => {
const { loading, data } = useGetProjectsQuery();
const {loading, data} = useGetProjectsQuery();
if (loading) {
return <span>loading</span>;
}
if (data) {
const { projects, teams, organizations } = data;
const {projects, teams, organizations} = data;
const projectTeams = teams.map(team => {
return {
id: team.id,
@ -166,8 +166,8 @@ type ProjectPopupProps = {
projectID: string;
};
export const ProjectPopup: React.FC<ProjectPopupProps> = ({ history, name, projectID }) => {
const { hidePopup, setTab } = usePopup();
export const ProjectPopup: React.FC<ProjectPopupProps> = ({history, name, projectID}) => {
const {hidePopup, setTab} = usePopup();
const [deleteProject] = useDeleteProjectMutation({
update: (client, deleteData) => {
const cacheData: any = client.readQuery({
@ -206,7 +206,7 @@ export const ProjectPopup: React.FC<ProjectPopupProps> = ({ history, name, proje
deletedItems={DELETE_INFO.DELETE_PROJECTS.deletedItems}
onConfirmDelete={() => {
if (projectID) {
deleteProject({ variables: { projectID } });
deleteProject({variables: {projectID}});
hidePopup();
history.push('/projects');
}
@ -249,16 +249,16 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
nameOnly,
}) => {
console.log(popupContent);
const { loading, data } = useMeQuery();
const { showPopup, hidePopup, setTab } = usePopup();
const {loading, data} = useMeQuery();
const {showPopup, hidePopup, setTab} = usePopup();
const history = useHistory();
const { userID, setUserID } = useContext(UserIDContext);
const {userID, setUserID} = useContext(UserIDContext);
const onLogout = () => {
fetch('http://localhost:3333/auth/logout', {
fetch('/auth/logout', {
method: 'POST',
credentials: 'include',
}).then(async x => {
const { status } = x;
const {status} = x;
if (status === 200) {
history.replace('/login');
setUserID(null);

View File

@ -1,16 +1,16 @@
import React, { useState, useEffect } from 'react';
import React, {useState, useEffect} from 'react';
import jwtDecode from 'jwt-decode';
import { createBrowserHistory } from 'history';
import { setAccessToken } from 'shared/utils/accessToken';
import styled, { ThemeProvider } from 'styled-components';
import {createBrowserHistory} from 'history';
import {setAccessToken} from 'shared/utils/accessToken';
import styled, {ThemeProvider} from 'styled-components';
import NormalizeStyles from './NormalizeStyles';
import BaseStyles from './BaseStyles';
import { theme } from './ThemeStyles';
import {theme} from './ThemeStyles';
import Routes from './Routes';
import { UserIDContext } from './context';
import {UserIDContext} from './context';
import Navbar from './Navbar';
import { Router } from 'react-router';
import { PopupProvider } from 'shared/components/PopupMenu';
import {Router} from 'react-router';
import {PopupProvider} from 'shared/components/PopupMenu';
const history = createBrowserHistory();
@ -19,16 +19,16 @@ const App = () => {
const [userID, setUserID] = useState<string | null>(null);
useEffect(() => {
fetch('http://localhost:3333/auth/refresh_token', {
fetch('/auth/refresh_token', {
method: 'POST',
credentials: 'include',
}).then(async x => {
const { status } = x;
const {status} = x;
if (status === 400) {
history.replace('/login');
} else {
const response: RefreshTokenResponse = await x.json();
const { accessToken } = response;
const {accessToken} = response;
const claims: JWTToken = jwtDecode(accessToken);
setUserID(claims.userId);
setAccessToken(accessToken);
@ -39,7 +39,7 @@ const App = () => {
return (
<>
<UserIDContext.Provider value={{ userID, setUserID }}>
<UserIDContext.Provider value={{userID, setUserID}}>
<ThemeProvider theme={theme}>
<NormalizeStyles />
<BaseStyles />
@ -48,10 +48,10 @@ const App = () => {
{loading ? (
<div>loading</div>
) : (
<>
<Routes history={history} />
</>
)}
<>
<Routes history={history} />
</>
)}
</PopupProvider>
</Router>
</ThemeProvider>

View File

@ -1,24 +1,24 @@
import React, { useState, useEffect, useContext } from 'react';
import { useForm } from 'react-hook-form';
import { useHistory } from 'react-router';
import React, {useState, useEffect, useContext} from 'react';
import {useForm} from 'react-hook-form';
import {useHistory} from 'react-router';
import { setAccessToken } from 'shared/utils/accessToken';
import {setAccessToken} from 'shared/utils/accessToken';
import Login from 'shared/components/Login';
import { Container, LoginWrapper } from './Styles';
import {Container, LoginWrapper} from './Styles';
import UserIDContext from 'App/context';
import JwtDecode from 'jwt-decode';
const Auth = () => {
const [invalidLoginAttempt, setInvalidLoginAttempt] = useState(0);
const history = useHistory();
const { setUserID } = useContext(UserIDContext);
const {setUserID} = useContext(UserIDContext);
const login = (
data: LoginFormData,
setComplete: (val: boolean) => void,
setError: (field: string, eType: string, message: string) => void,
) => {
fetch('http://localhost:3333/auth/login', {
fetch('/auth/login', {
credentials: 'include',
method: 'POST',
body: JSON.stringify({
@ -33,7 +33,7 @@ const Auth = () => {
setComplete(true);
} else {
const response = await x.json();
const { accessToken } = response;
const {accessToken} = response;
const claims: JWTToken = JwtDecode(accessToken);
setUserID(claims.userId);
setComplete(true);
@ -45,11 +45,11 @@ const Auth = () => {
};
useEffect(() => {
fetch('http://localhost:3333/auth/refresh_token', {
fetch('/auth/refresh_token', {
method: 'POST',
credentials: 'include',
}).then(async x => {
const { status } = x;
const {status} = x;
if (status === 200) {
history.replace('/projects');
}

View File

@ -2,13 +2,13 @@ import React from 'react';
import ReactDOM from 'react-dom';
import axios from 'axios';
import createAuthRefreshInterceptor from 'axios-auth-refresh';
import { ApolloProvider } from '@apollo/react-hooks';
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
import { onError } from 'apollo-link-error';
import { ApolloLink, Observable, fromPromise } from 'apollo-link';
import { getAccessToken, getNewToken, setAccessToken } from 'shared/utils/accessToken';
import {ApolloProvider} from '@apollo/react-hooks';
import {ApolloClient} from 'apollo-client';
import {InMemoryCache} from 'apollo-cache-inmemory';
import {HttpLink} from 'apollo-link-http';
import {onError} from 'apollo-link-error';
import {ApolloLink, Observable, fromPromise} from 'apollo-link';
import {getAccessToken, getNewToken, setAccessToken} from 'shared/utils/accessToken';
import App from './App';
// https://able.bio/AnasT/apollo-graphql-async-access-token-refresh--470t1c8
@ -18,7 +18,7 @@ let isRefreshing = false;
let pendingRequests: any = [];
const refreshAuthLogic = (failedRequest: any) =>
axios.post('http://localhost:3333/auth/refresh_token', {}, { withCredentials: true }).then(tokenRefreshResponse => {
axios.post('/auth/refresh_token', {}, {withCredentials: true}).then(tokenRefreshResponse => {
setAccessToken(tokenRefreshResponse.data.accessToken);
failedRequest.response.config.headers.Authorization = `Bearer ${tokenRefreshResponse.data.accessToken}`;
return Promise.resolve();
@ -43,7 +43,7 @@ const setRefreshing = (newVal: boolean) => {
isRefreshing = newVal;
};
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
const errorLink = onError(({graphQLErrors, networkError, operation, forward}) => {
if (graphQLErrors) {
for (const err of graphQLErrors) {
if (err.extensions && err.extensions.code) {
@ -118,9 +118,9 @@ const requestLink = new ApolloLink(
const client = new ApolloClient({
link: ApolloLink.from([
onError(({ graphQLErrors, networkError }) => {
onError(({graphQLErrors, networkError}) => {
if (graphQLErrors) {
graphQLErrors.forEach(({ message, locations, path }) =>
graphQLErrors.forEach(({message, locations, path}) =>
console.log(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`),
);
}
@ -131,7 +131,7 @@ const client = new ApolloClient({
errorLink,
requestLink,
new HttpLink({
uri: 'http://localhost:3333/graphql',
uri: '/graphql',
credentials: 'same-origin',
}),
]),

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'},
],
},
};
@ -26,13 +26,14 @@ export const Default = () => {
<Admin
onInviteUser={action('invite user')}
initialTab={1}
onUpdateUserPassword={action('update user password')}
onDeleteUser={action('delete user')}
users={[
{
id: '1',
username: 'jordanthedev',
email: 'jordan@jordanthedev.com',
role: { code: 'admin', name: 'Admin' },
role: {code: 'admin', name: 'Admin'},
fullName: 'Jordan Knott',
profileIcon: {
bgColor: '#fff',

View File

@ -1,12 +1,12 @@
import React, {useState, useRef} from 'react';
import {UserPlus, Checkmark} from 'shared/icons';
import styled, {css} from 'styled-components';
import React, { useState, useRef } from 'react';
import { UserPlus, Checkmark } from 'shared/icons';
import styled, { css } from 'styled-components';
import TaskAssignee from 'shared/components/TaskAssignee';
import Select from 'shared/components/Select';
import {User, Plus, Lock, Pencil, Trash} from 'shared/icons';
import {usePopup, Popup} from 'shared/components/PopupMenu';
import {RoleCode, useUpdateUserRoleMutation} from 'shared/generated/graphql';
import {AgGridReact} from 'ag-grid-react';
import { User, Plus, Lock, Pencil, Trash } from 'shared/icons';
import { usePopup, Popup } from 'shared/components/PopupMenu';
import { RoleCode, useUpdateUserRoleMutation } from 'shared/generated/graphql';
import { AgGridReact } from 'ag-grid-react';
import Input from 'shared/components/Input';
import Member from 'shared/components/Member';
@ -19,20 +19,20 @@ export const RoleCheckmark = styled(Checkmark)`
`;
const permissions = [
{
code: 'owner',
name: 'Owner',
description:
'Can view, create and edit team projects, and change settings for the team. Will have admin rights on all projects in this team. Can delete the team and all team projects.',
},
{
code: 'admin',
name: 'Admin',
description:
'Can view, create and edit team projects, and change settings for the team. Will have admin rights on all projects in this team.',
},
{
code: 'owner',
name: 'Owner',
description:
'Can view, create and edit team projects, and change settings for the team. Will have admin rights on all projects in this team. Can delete the team and all team projects.',
},
{
code: 'admin',
name: 'Admin',
description:
'Can view, create and edit team projects, and change settings for the team. Will have admin rights on all projects in this team.',
},
{code: 'member', name: 'Member', description: 'Can view, create, and edit team projects, but not change settings.'},
{ code: 'member', name: 'Member', description: 'Can view, create, and edit team projects, but not change settings.' },
];
export const RoleName = styled.div`
@ -50,7 +50,7 @@ export const MiniProfileActions = styled.ul`
export const MiniProfileActionWrapper = styled.li``;
export const MiniProfileActionItem = styled.span<{disabled?: boolean}>`
export const MiniProfileActionItem = styled.span<{ disabled?: boolean }>`
color: #c2c6dc;
display: block;
font-weight: 400;
@ -59,13 +59,13 @@ export const MiniProfileActionItem = styled.span<{disabled?: boolean}>`
text-decoration: none;
${props =>
props.disabled
? css`
props.disabled
? css`
user-select: none;
pointer-events: none;
color: rgba(${props.theme.colors.text.primary}, 0.4);
`
: css`
: css`
cursor: pointer;
&:hover {
background: rgb(115, 103, 240);
@ -104,142 +104,149 @@ export const RemoveMemberButton = styled(Button)`
width: 100%;
`;
type TeamRoleManagerPopupProps = {
user: TaskUser;
warning?: string | null;
canChangeRole: boolean;
onChangeRole: (roleCode: RoleCode) => void;
onRemoveFromTeam?: () => void;
user: TaskUser;
warning?: string | null;
canChangeRole: boolean;
onChangeRole: (roleCode: RoleCode) => void;
updateUserPassword?: (user: TaskUser, password: string) => void;
onRemoveFromTeam?: () => void;
};
const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
warning,
user,
canChangeRole,
onRemoveFromTeam,
onChangeRole,
warning,
user,
canChangeRole,
onRemoveFromTeam,
updateUserPassword,
onChangeRole,
}) => {
const {hidePopup, setTab} = usePopup();
return (
<>
<Popup title={null} tab={0}>
<MiniProfileActions>
<MiniProfileActionWrapper>
{user.role && (
<MiniProfileActionItem
onClick={() => {
setTab(1);
}}
>
Change permissions...
const { hidePopup, setTab } = usePopup();
const [userPass, setUserPass] = useState({ pass: "", passConfirm: "" });
return (
<>
<Popup title={null} tab={0}>
<MiniProfileActions>
<MiniProfileActionWrapper>
{user.role && (
<MiniProfileActionItem
onClick={() => {
setTab(1);
}}
>
Change permissions...
<CurrentPermission>{`(${user.role.name})`}</CurrentPermission>
</MiniProfileActionItem>
)}
<MiniProfileActionItem onClick={() => {
setTab(3)
}}>Reset password...</MiniProfileActionItem>
<MiniProfileActionItem onClick={() =>setTab(5)}>Remove from organzation...</MiniProfileActionItem>
</MiniProfileActionWrapper>
</MiniProfileActions>
{warning && (
<>
<Separator />
<WarningText>{warning}</WarningText>
</>
)}
</Popup>
<Popup title="Change Permissions" onClose={() => hidePopup()} tab={1}>
<MiniProfileActions>
<MiniProfileActionWrapper>
{permissions
.filter(p => (user.role && user.role.code === 'owner') || p.code !== 'owner')
.map(perm => (
<MiniProfileActionItem
disabled={user.role && perm.code !== user.role.code && !canChangeRole}
key={perm.code}
onClick={() => {
if (onChangeRole && user.role && perm.code !== user.role.code) {
switch (perm.code) {
case 'owner':
onChangeRole(RoleCode.Owner);
break;
case 'admin':
onChangeRole(RoleCode.Admin);
break;
case 'member':
onChangeRole(RoleCode.Member);
break;
default:
break;
}
hidePopup();
}
}}
>
<RoleName>
{perm.name}
{user.role && perm.code === user.role.code && <RoleCheckmark width={12} height={12} />}
</RoleName>
<RoleDescription>{perm.description}</RoleDescription>
</MiniProfileActionItem>
))}
</MiniProfileActionWrapper>
{user.role && user.role.code === 'owner' && (
<>
<Separator />
<WarningText>You can't change roles because there must be an owner.</WarningText>
</>
)}
</MiniProfileActions>
</Popup>
<Popup title="Remove from Team?" onClose={() => hidePopup()} tab={2}>
<Content>
<DeleteDescription>
The member will be removed from all cards on this project. They will receive a notification.
</MiniProfileActionItem>
)}
<MiniProfileActionItem onClick={() => {
setTab(3)
}}>Reset password...</MiniProfileActionItem>
<MiniProfileActionItem onClick={() => setTab(5)}>Remove from organzation...</MiniProfileActionItem>
</MiniProfileActionWrapper>
</MiniProfileActions>
{warning && (
<>
<Separator />
<WarningText>{warning}</WarningText>
</>
)}
</Popup>
<Popup title="Change Permissions" onClose={() => hidePopup()} tab={1}>
<MiniProfileActions>
<MiniProfileActionWrapper>
{permissions
.filter(p => (user.role && user.role.code === 'owner') || p.code !== 'owner')
.map(perm => (
<MiniProfileActionItem
disabled={user.role && perm.code !== user.role.code && !canChangeRole}
key={perm.code}
onClick={() => {
if (onChangeRole && user.role && perm.code !== user.role.code) {
switch (perm.code) {
case 'owner':
onChangeRole(RoleCode.Owner);
break;
case 'admin':
onChangeRole(RoleCode.Admin);
break;
case 'member':
onChangeRole(RoleCode.Member);
break;
default:
break;
}
hidePopup();
}
}}
>
<RoleName>
{perm.name}
{user.role && perm.code === user.role.code && <RoleCheckmark width={12} height={12} />}
</RoleName>
<RoleDescription>{perm.description}</RoleDescription>
</MiniProfileActionItem>
))}
</MiniProfileActionWrapper>
{user.role && user.role.code === 'owner' && (
<>
<Separator />
<WarningText>You can't change roles because there must be an owner.</WarningText>
</>
)}
</MiniProfileActions>
</Popup>
<Popup title="Remove from Team?" onClose={() => hidePopup()} tab={2}>
<Content>
<DeleteDescription>
The member will be removed from all cards on this project. They will receive a notification.
</DeleteDescription>
<RemoveMemberButton
color="danger"
onClick={() => {
if (onRemoveFromTeam) {
onRemoveFromTeam();
}
}}
>
Remove Member
<RemoveMemberButton
color="danger"
onClick={() => {
if (onRemoveFromTeam) {
onRemoveFromTeam();
}
}}
>
Remove Member
</RemoveMemberButton>
</Content>
</Popup>
<Popup title="Reset password?" onClose={() => hidePopup()} tab={3}>
<Content>
<DeleteDescription>
You can either set the user's new password directly or send the user an email allowing them to reset their own password.
</Content>
</Popup>
<Popup title="Reset password?" onClose={() => hidePopup()} tab={3}>
<Content>
<DeleteDescription>
You can either set the user's new password directly or send the user an email allowing them to reset their own password.
</DeleteDescription>
<UserPassBar>
<UserPassButton onClick={() => setTab(4)} color="warning">Set password...</UserPassButton>
<UserPassButton color="warning" variant="outline">Send reset link</UserPassButton>
</UserPassBar>
</Content>
</Popup>
<Popup title="Reset password" onClose={() => hidePopup()} tab={4}>
<Content>
<NewUserPassInput width="100%" variant="alternate" placeholder="New password" />
<NewUserPassInput width="100%" variant="alternate" placeholder="New password (confirm)" />
<UserPassConfirmButton onClick={() => {}} color="danger">Set password</UserPassConfirmButton>
</Content>
</Popup>
<Popup title="Remove user" onClose={() => hidePopup()} tab={5}>
<Content>
<DeleteDescription>
Removing this user from the organzation will remove them from assigned tasks, projects, and teams.
<UserPassBar>
<UserPassButton onClick={() => setTab(4)} color="warning">Set password...</UserPassButton>
<UserPassButton color="warning" variant="outline">Send reset link</UserPassButton>
</UserPassBar>
</Content>
</Popup>
<Popup title="Reset password" onClose={() => hidePopup()} tab={4}>
<Content>
<NewUserPassInput onChange={e => setUserPass({ pass: e.currentTarget.value, passConfirm: userPass.passConfirm })} value={userPass.pass} width="100%" variant="alternate" placeholder="New password" />
<NewUserPassInput onChange={e => setUserPass({ passConfirm: e.currentTarget.value, pass: userPass.pass })} value={userPass.passConfirm} width="100%" variant="alternate" placeholder="New password (confirm)" />
<UserPassConfirmButton disabled={userPass.pass === "" || userPass.passConfirm === ""} onClick={() => {
if (userPass.pass === userPass.passConfirm && updateUserPassword) {
updateUserPassword(user, userPass.pass)
}
}} color="danger">Set password</UserPassConfirmButton>
</Content>
</Popup>
<Popup title="Remove user" onClose={() => hidePopup()} tab={5}>
<Content>
<DeleteDescription>
Removing this user from the organzation will remove them from assigned tasks, projects, and teams.
</DeleteDescription>
<DeleteDescription>
The user is the owner of 3 projects & 2 teams.
<DeleteDescription>
The user is the owner of 3 projects & 2 teams.
</DeleteDescription>
<UserSelect onChange={() => {}} value={null} options={[{label: 'Jordan Knott', value: "jordanknott"}]} />
<UserPassConfirmButton onClick={() => {}} color="danger">Set password</UserPassConfirmButton>
</Content>
</Popup>
</>
);
<UserSelect onChange={() => { }} value={null} options={[{ label: 'Jordan Knott', value: "jordanknott" }]} />
<UserPassConfirmButton onClick={() => { }} color="danger">Set password</UserPassConfirmButton>
</Content>
</Popup>
</>
);
};
const UserSelect = styled(Select)`
margin: 8px 0;
@ -406,7 +413,7 @@ const LockUserIcon = styled(Lock)``;
const DeleteUserIcon = styled(Trash)``;
type ActionButtonProps = {
onClick: ($target: React.RefObject<HTMLElement>) => void;
onClick: ($target: React.RefObject<HTMLElement>) => void;
};
const ActionButtonWrapper = styled.div`
@ -415,85 +422,85 @@ const ActionButtonWrapper = styled.div`
display: inline-flex;
`;
const ActionButton: React.FC<ActionButtonProps> = ({onClick, children}) => {
const $wrapper = useRef<HTMLDivElement>(null);
return (
<ActionButtonWrapper onClick={() => onClick($wrapper)} ref={$wrapper}>
{children}
</ActionButtonWrapper>
);
const ActionButton: React.FC<ActionButtonProps> = ({ onClick, children }) => {
const $wrapper = useRef<HTMLDivElement>(null);
return (
<ActionButtonWrapper onClick={() => onClick($wrapper)} ref={$wrapper}>
{children}
</ActionButtonWrapper>
);
};
const ActionButtons = (params: any) => {
return (
<>
<ActionButton onClick={() => {}}>
<EditUserIcon width={16} height={16} />
</ActionButton>
<ActionButton onClick={$target => params.onDeleteUser($target, params.value)}>
<DeleteUserIcon width={16} height={16} />
</ActionButton>
</>
);
return (
<>
<ActionButton onClick={() => { }}>
<EditUserIcon width={16} height={16} />
</ActionButton>
<ActionButton onClick={$target => params.onDeleteUser($target, params.value)}>
<DeleteUserIcon width={16} height={16} />
</ActionButton>
</>
);
};
type ListTableProps = {
users: Array<User>;
onDeleteUser: ($target: React.RefObject<HTMLElement>, userID: string) => void;
users: Array<User>;
onDeleteUser: ($target: React.RefObject<HTMLElement>, userID: string) => void;
};
const ListTable: React.FC<ListTableProps> = ({users, onDeleteUser}) => {
const data = {
defaultColDef: {
resizable: true,
sortable: true,
},
columnDefs: [
{
minWidth: 55,
width: 55,
headerCheckboxSelection: true,
checkboxSelection: true,
},
{minWidth: 210, headerName: 'Username', editable: true, field: 'username'},
{minWidth: 225, headerName: 'Email', field: 'email'},
{minWidth: 200, headerName: 'Name', editable: true, field: 'fullName'},
{minWidth: 200, headerName: 'Role', editable: true, field: 'roleName'},
{
minWidth: 200,
headerName: 'Actions',
field: 'id',
cellRenderer: 'actionButtons',
cellRendererParams: {
onDeleteUser: (target: any, userID: any) => {
onDeleteUser(target, userID);
},
const ListTable: React.FC<ListTableProps> = ({ users, onDeleteUser }) => {
const data = {
defaultColDef: {
resizable: true,
sortable: true,
},
},
],
frameworkComponents: {
actionButtons: ActionButtons,
},
};
return (
<Root>
<div className="ag-theme-material" style={{height: '296px', width: '100%'}}>
<AgGridReact
rowSelection="multiple"
defaultColDef={data.defaultColDef}
columnDefs={data.columnDefs}
rowData={users.map(u => ({...u, roleName: u.role.name}))}
frameworkComponents={data.frameworkComponents}
onFirstDataRendered={params => {
params.api.sizeColumnsToFit();
}}
onGridSizeChanged={params => {
params.api.sizeColumnsToFit();
}}
/>
</div>
</Root>
);
columnDefs: [
{
minWidth: 55,
width: 55,
headerCheckboxSelection: true,
checkboxSelection: true,
},
{ minWidth: 210, headerName: 'Username', editable: true, field: 'username' },
{ minWidth: 225, headerName: 'Email', field: 'email' },
{ minWidth: 200, headerName: 'Name', editable: true, field: 'fullName' },
{ minWidth: 200, headerName: 'Role', editable: true, field: 'roleName' },
{
minWidth: 200,
headerName: 'Actions',
field: 'id',
cellRenderer: 'actionButtons',
cellRendererParams: {
onDeleteUser: (target: any, userID: any) => {
onDeleteUser(target, userID);
},
},
},
],
frameworkComponents: {
actionButtons: ActionButtons,
},
};
return (
<Root>
<div className="ag-theme-material" style={{ height: '296px', width: '100%' }}>
<AgGridReact
rowSelection="multiple"
defaultColDef={data.defaultColDef}
columnDefs={data.columnDefs}
rowData={users.map(u => ({ ...u, roleName: u.role.name }))}
frameworkComponents={data.frameworkComponents}
onFirstDataRendered={params => {
params.api.sizeColumnsToFit();
}}
onGridSizeChanged={params => {
params.api.sizeColumnsToFit();
}}
/>
</div>
</Root>
);
};
const Wrapper = styled.div`
@ -534,7 +541,7 @@ const TabNavItem = styled.li`
display: block;
position: relative;
`;
const TabNavItemButton = styled.button<{active: boolean}>`
const TabNavItemButton = styled.button<{ active: boolean }>`
cursor: pointer;
display: flex;
align-items: center;
@ -561,7 +568,7 @@ const TabNavItemSpan = styled.span`
font-size: 14px;
`;
const TabNavLine = styled.span<{top: number}>`
const TabNavLine = styled.span<{ top: number }>`
left: auto;
right: 0;
width: 2px;
@ -594,137 +601,141 @@ const TabContent = styled.div`
border-radius: 0.5rem;
`;
const items = [{name: 'Members'}, {name: 'Settings'}];
const items = [{ name: 'Members' }, { name: 'Settings' }];
type NavItemProps = {
active: boolean;
name: string;
tab: number;
onClick: (tab: number, top: number) => void;
active: boolean;
name: string;
tab: number;
onClick: (tab: number, top: number) => void;
};
const NavItem: React.FC<NavItemProps> = ({active, name, tab, onClick}) => {
const $item = useRef<HTMLLIElement>(null);
return (
<TabNavItem
key={name}
ref={$item}
onClick={() => {
if ($item && $item.current) {
const pos = $item.current.getBoundingClientRect();
onClick(tab, pos.top);
}
}}
>
<TabNavItemButton active={active}>
<User size={14} color={active ? 'rgba(115, 103, 240)' : '#c2c6dc'} />
<TabNavItemSpan>{name}</TabNavItemSpan>
</TabNavItemButton>
</TabNavItem>
);
const NavItem: React.FC<NavItemProps> = ({ active, name, tab, onClick }) => {
const $item = useRef<HTMLLIElement>(null);
return (
<TabNavItem
key={name}
ref={$item}
onClick={() => {
if ($item && $item.current) {
const pos = $item.current.getBoundingClientRect();
onClick(tab, pos.top);
}
}}
>
<TabNavItemButton active={active}>
<User size={14} color={active ? 'rgba(115, 103, 240)' : '#c2c6dc'} />
<TabNavItemSpan>{name}</TabNavItemSpan>
</TabNavItemButton>
</TabNavItem>
);
};
type AdminProps = {
initialTab: number;
onAddUser: ($target: React.RefObject<HTMLElement>) => void;
onDeleteUser: ($target: React.RefObject<HTMLElement>, userID: string) => void;
onInviteUser: ($target: React.RefObject<HTMLElement>) => void;
users: Array<User>;
initialTab: number;
onAddUser: ($target: React.RefObject<HTMLElement>) => void;
onDeleteUser: ($target: React.RefObject<HTMLElement>, userID: string) => void;
onInviteUser: ($target: React.RefObject<HTMLElement>) => void;
users: Array<User>;
onUpdateUserPassword: (user: TaskUser, password: string) => void;
};
const Admin: React.FC<AdminProps> = ({initialTab, onAddUser, onDeleteUser, onInviteUser, users}) => {
const warning =
'You cant leave because you are the only admin. To make another user an admin, click their avatar, select “Change permissions…”, and select “Admin”.';
const [currentTop, setTop] = useState(initialTab * 48);
const [currentTab, setTab] = useState(initialTab);
const {showPopup, hidePopup} = usePopup();
const $tabNav = useRef<HTMLDivElement>(null);
const Admin: React.FC<AdminProps> = ({ initialTab, onAddUser, onUpdateUserPassword, onDeleteUser, onInviteUser, users }) => {
const warning =
'You cant leave because you are the only admin. To make another user an admin, click their avatar, select “Change permissions…”, and select “Admin”.';
const [currentTop, setTop] = useState(initialTab * 48);
const [currentTab, setTab] = useState(initialTab);
const { showPopup, hidePopup } = usePopup();
const $tabNav = useRef<HTMLDivElement>(null);
const [updateUserRole] = useUpdateUserRoleMutation()
return (
<Container>
<TabNav ref={$tabNav}>
<TabNavContent>
{items.map((item, idx) => (
<NavItem
onClick={(tab, top) => {
if ($tabNav && $tabNav.current) {
const pos = $tabNav.current.getBoundingClientRect();
setTab(tab);
setTop(top - pos.top);
}
}}
name={item.name}
tab={idx}
active={idx === currentTab}
/>
))}
<TabNavLine top={currentTop} />
</TabNavContent>
</TabNav>
<TabContentWrapper>
<TabContent>
<MemberListWrapper>
<MemberListHeader>
<ListTitle>{`Users (${users.length})`}</ListTitle>
<ListDesc>
Team members can view and join all Team Visible boards and create new boards in the team.
</ListDesc>
<ListActions>
<FilterSearch width="250px" variant="alternate" placeholder="Filter by name" />
<InviteMemberButton
onClick={$target => {
onAddUser($target);
}}
>
<InviteIcon width={16} height={16} />
New Member
</InviteMemberButton>
</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}
canChangeRole={member.role && member.role.code !== 'owner'}
onChangeRole={roleCode => {
updateUserRole({variables: {userID: member.id, roleCode}})
}}
onRemoveFromTeam={
member.role && member.role.code === 'owner'
? undefined
: () => {
hidePopup();
const [updateUserRole] = useUpdateUserRoleMutation()
return (
<Container>
<TabNav ref={$tabNav}>
<TabNavContent>
{items.map((item, idx) => (
<NavItem
onClick={(tab, top) => {
if ($tabNav && $tabNav.current) {
const pos = $tabNav.current.getBoundingClientRect();
setTab(tab);
setTop(top - pos.top);
}
}
/>,
);
}}
>
Manage
}}
name={item.name}
tab={idx}
active={idx === currentTab}
/>
))}
<TabNavLine top={currentTop} />
</TabNavContent>
</TabNav>
<TabContentWrapper>
<TabContent>
<MemberListWrapper>
<MemberListHeader>
<ListTitle>{`Users (${users.length})`}</ListTitle>
<ListDesc>
Team members can view and join all Team Visible boards and create new boards in the team.
</ListDesc>
<ListActions>
<FilterSearch width="250px" variant="alternate" placeholder="Filter by name" />
<InviteMemberButton
onClick={$target => {
onAddUser($target);
}}
>
<InviteIcon width={16} height={16} />
New Member
</InviteMemberButton>
</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>
))}
</MemberList>
</MemberListWrapper>
</TabContent>
</TabContentWrapper>
</Container>
);
</MemberItemOptions>
</MemberListItem>
))}
</MemberList>
</MemberListWrapper>
</TabContent>
</TabContentWrapper>
</Container>
);
};
export default Admin;