20 Commits
0.3.2 ... 0.3.6

Author SHA1 Message Date
8d724fa3cf refactor: add release target 2021-09-13 13:07:49 -05:00
76e398488f fix: rewrite the label manager to no longer use useRef
useRef was causing a `readonly` error when trying to overwrite
`ref.current`. Rewrote components to use an Apollo query instead.

fixes #121
2021-09-13 12:44:02 -05:00
d1b867db35 deps: upgrade @types/react & @types/react-dom 2021-09-13 12:43:39 -05:00
aeb97a30d8 refactor: add docker testing targets to magefile 2021-09-13 11:23:09 -05:00
56e925a48d fix: add error to log when user creation fails 2021-09-13 11:22:48 -05:00
65cd431c1a fix: TaskDetails editor theme updated to work with latest version 2021-09-07 11:32:29 -05:00
a188c4b0ca fix: clean up component to fix lint warnings preventing frontend build 2021-09-04 14:08:44 -05:00
3bfce1825c docs: update unreleased changelog section 2021-09-04 13:16:03 -05:00
2b4f94117c fix: add missing rich-markdown-editor dependency
fixes #122
2021-09-04 12:16:01 -05:00
05799fce90 fix: hide any open popups when closing task details modal 2021-05-10 12:46:46 -05:00
b4f37350a9 refactor: switch to personal fork of rich-markdown-editor 2021-05-10 12:45:40 -05:00
8c6a3db0bc deps: upgrade all dependencies 2021-05-02 17:31:24 -05:00
5a9a66effe feat: apply new label to task when available 2021-04-30 23:49:12 -05:00
167d285d02 refactor: polling is now turned off in development mode 2021-04-30 23:36:58 -05:00
e2634dc490 feat: redirect after login when applicable 2021-04-30 23:25:48 -05:00
04c12e4da9 feat: projects can be set to public 2021-04-30 22:55:37 -05:00
3e72271d9b refactor(Project): split out components into their own files 2021-04-30 20:06:05 -05:00
bd34f4b3ad feat: change primary font to Open Sans 2021-04-30 16:35:43 -05:00
f45e359402 refactor: clean up components 2021-04-28 21:51:47 -05:00
229a53fa0a refactor: replace refresh & access token with auth token only
changes authentication to no longer use a refresh token & access token
for accessing protected endpoints. Instead only an auth token is used.

Before the login flow was:

Login -> get refresh (stored as HttpOnly cookie) + access token (stored in memory) ->
  protected endpoint request (attach access token as Authorization header) -> access token expires in
  15 minutes, so use refresh token to obtain new one when that happens

now it looks like this:

Login -> get auth token (stored as HttpOnly cookie) -> make protected endpont
request (token sent)

the reasoning for using the refresh + access token was to reduce DB
calls, but in the end I don't think its worth the hassle.
2021-04-28 21:38:49 -05:00
128 changed files with 12112 additions and 10700 deletions

View File

@ -4,17 +4,18 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [0.3.5] - 2021-09-04
### Added ### Added
- Task sorting & filtering - Project visibility can now be set to public - meaning anyone can view the project board
- Redesigned the Task Details UI - When redirected to login page while trying to view a page that requires login, you'll be redirected back to the correct page after login
- Implement task group actions (duplicate/delete all tasks/sort) - When creating a new label within the LabelManager on a card, the new label will automatically be applied to the task after creation
### Changed
- Switch primary font to Open Sans
### Fixed ### Fixed
- removed CORS middleware to fix security issue - Any open popups are hidden when closing the Task Details window
- Added 3 retries with backoff to initial database connection [(#47)](https://github.com/JordanKnott/taskcafe/issues/47)
- Can now actually set a due date
## [0.1.1] - 2020-08-21 ## [0.1.1] - 2020-08-21

View File

@ -12,7 +12,7 @@ services:
volumes: volumes:
- taskcafe-postgres:/var/lib/postgresql/data - taskcafe-postgres:/var/lib/postgresql/data
ports: ports:
- 8855:5432 - 8865:5432
mailhog: mailhog:
image: mailhog/mailhog:latest image: mailhog/mailhog:latest
restart: always restart: always

2
frontend/.env Normal file
View File

@ -0,0 +1,2 @@
REACT_APP_ENABLE_POLLING=true
ESLINT_NO_DEV_ERRORS=true

View File

@ -0,0 +1 @@
REACT_APP_ENABLE_POLLING=false

View File

@ -24,15 +24,18 @@
"plugin:@typescript-eslint/recommended" "plugin:@typescript-eslint/recommended"
], ],
"rules": { "rules": {
"prettier/prettier": "error", "prettier/prettier": "warn",
"no-shadow": "off",
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"], "no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
"@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-unused-vars": "off",
"react/jsx-filename-extension": [2, { "extensions": [".js", ".jsx", ".ts", ".tsx"] }], "react/jsx-filename-extension": [2, { "extensions": [".js", ".jsx", ".ts", ".tsx"] }],
"react/require-default-props": "off",
"no-case-declarations": "off", "no-case-declarations": "off",
"no-plusplus": "off", "no-plusplus": "off",
"react/prop-types": 0, "react/prop-types": 0,
"react/no-unused-prop-types": "off",
"no-continue": "off", "no-continue": "off",
"react/jsx-props-no-spreading": "off", "react/jsx-props-no-spreading": "off",
"no-param-reassign": "off", "no-param-reassign": "off",
@ -47,6 +50,8 @@
"tsx": "never" "tsx": "never"
} }
], ],
"no-use-before-define": "off",
"@typescript-eslint/no-use-before-define": ["error"],
"import/no-extraneous-dependencies": [ "import/no-extraneous-dependencies": [
"error", "error",
{ {

View File

@ -3,28 +3,25 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@apollo/client": "^3.0.0-rc.8", "@apollo/client": "^3.3.16",
"@apollo/react-common": "^3.1.4", "@apollo/react-common": "^3.1.4",
"@apollo/react-hooks": "^3.1.3", "@apollo/react-hooks": "^4.0.0",
"@types/axios": "^0.14.0", "@taskcafe/rich-markdown-editor": "^11.0.10",
"@types/color": "^3.0.1", "@types/color": "^3.0.1",
"@types/date-fns": "^2.6.0", "@types/dompurify": "^2.2.2",
"@types/dompurify": "^2.0.4",
"@types/emoji-mart": "^3.0.4", "@types/emoji-mart": "^3.0.4",
"@types/jest": "^24.0.0", "@types/jest": "^26.0.23",
"@types/jwt-decode": "^2.2.1", "@types/lodash": "^4.14.168",
"@types/lodash": "^4.14.149", "@types/node": "^15.0.1",
"@types/node": "^12.0.0", "@types/react": "^17.0.20",
"@types/query-string": "^6.3.0", "@types/react-beautiful-dnd": "^13.0.0",
"@types/react": "^16.9.21", "@types/react-datepicker": "^3.1.8",
"@types/react-beautiful-dnd": "^12.1.1", "@types/react-dom": "^17.0.9",
"@types/react-datepicker": "^2.11.0", "@types/react-router": "^5.1.13",
"@types/react-dom": "^16.9.5", "@types/react-router-dom": "^5.1.7",
"@types/react-router": "^5.1.4", "@types/react-select": "^4.0.15",
"@types/react-router-dom": "^5.1.3",
"@types/react-select": "^3.0.13",
"@types/react-timeago": "^4.1.1", "@types/react-timeago": "^4.1.1",
"@types/styled-components": "^5.0.0", "@types/styled-components": "^5.1.0",
"apollo-cache-inmemory": "^1.6.5", "apollo-cache-inmemory": "^1.6.5",
"apollo-client": "^2.6.8", "apollo-client": "^2.6.8",
"apollo-link": "^1.2.13", "apollo-link": "^1.2.13",
@ -33,39 +30,40 @@
"apollo-link-state": "^0.4.2", "apollo-link-state": "^0.4.2",
"apollo-utilities": "^1.3.3", "apollo-utilities": "^1.3.3",
"axios": "^0.21.1", "axios": "^0.21.1",
"axios-auth-refresh": "^2.2.7", "axios-auth-refresh": "^3.1.0",
"color": "^3.1.2", "color": "^3.1.2",
"date-fns": "^2.14.0", "date-fns": "^2.21.1",
"dayjs": "^1.9.1", "dayjs": "^1.10.4",
"dompurify": "^2.2.6", "dompurify": "^2.2.8",
"emoji-mart": "^3.0.0", "emoji-mart": "^3.0.1",
"emoticon": "^3.2.0", "emoticon": "^4.0.0",
"graphql": "^15.0.0", "graphql": "^15.5.0",
"graphql-tag": "^2.10.3", "graphql-tag": "^2.12.4",
"history": "^4.10.1", "history": "^5.0.0",
"immer": "^8.0.1", "immer": "^9.0.2",
"jwt-decode": "^2.2.0", "jwt-decode": "^3.1.2",
"lodash": "^4.17.20", "lodash": "^4.17.21",
"node-emoji": "^1.10.0", "node-emoji": "^1.10.0",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"query-string": "^6.13.7", "query-string": "^7.0.0",
"react": "^16.12.0", "react": "^17.0.2",
"react-autosize-textarea": "^7.0.0", "react-autosize-textarea": "^7.0.0",
"react-beautiful-dnd": "^13.0.0", "react-beautiful-dnd": "^13.1.0",
"react-datepicker": "^2.14.1", "react-datepicker": "^3.8.0",
"react-dom": "^16.12.0", "react-dom": "^17.0.2",
"react-emoji-render": "^1.2.4", "react-emoji-render": "^1.2.4",
"react-hook-form": "^6.0.6", "react-hook-form": "^7.3.6",
"react-markdown": "^4.3.1", "react-markdown": "^6.0.1",
"react-router": "^5.1.2", "react-router": "^5.1.2",
"react-router-dom": "^5.1.2", "react-router-dom": "^5.1.2",
"react-scripts": "3.4.0", "react-scripts": "4.0.3",
"react-select": "^3.1.0", "react-select": "^4.3.0",
"react-timeago": "^4.4.0", "react-timeago": "^5.2.0",
"react-toastify": "^6.0.8", "react-toastify": "^7.0.4",
"rich-markdown-editor": "^10.6.5", "rich-markdown-editor": "^11.17.4-0",
"styled-components": "^5.0.1", "styled-components": "^5.2.3",
"typescript": "~3.7.2" "typescript": "~4.2.4",
"unist-util-visit": "^4.0.0"
}, },
"proxy": "http://localhost:3333", "proxy": "http://localhost:3333",
"scripts": { "scripts": {
@ -93,20 +91,20 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@graphql-codegen/cli": "^1.13.2", "@graphql-codegen/cli": "^1.21.4",
"@graphql-codegen/typescript": "^1.13.2", "@graphql-codegen/typescript": "^1.22.0",
"@graphql-codegen/typescript-operations": "^1.13.2", "@graphql-codegen/typescript-operations": "^1.17.16",
"@graphql-codegen/typescript-react-apollo": "^1.13.2", "@graphql-codegen/typescript-react-apollo": "^2.2.4",
"@typescript-eslint/eslint-plugin": "^2.20.0", "@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^2.20.0", "@typescript-eslint/parser": "^4.22.0",
"eslint": "^6.8.0", "eslint": "^7.25.0",
"eslint-config-airbnb": "^18.0.1", "eslint-config-airbnb": "^18.0.1",
"eslint-config-prettier": "^6.10.0", "eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.20.1", "eslint-plugin-import": "^2.20.1",
"eslint-plugin-jsx-a11y": "^6.2.3", "eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-prettier": "^3.1.2", "eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-react": "^7.18.3", "eslint-plugin-react": "^7.23.2",
"eslint-plugin-react-hooks": "^1.7.0", "eslint-plugin-react-hooks": "^4.2.0",
"prettier": "^1.19.1" "prettier": "^2.2.1"
} }
} }

View File

@ -10,7 +10,6 @@ import {
UsersDocument, UsersDocument,
UsersQuery, UsersQuery,
} from 'shared/generated/graphql'; } from 'shared/generated/graphql';
import Input from 'shared/components/Input';
import styled from 'styled-components'; import styled from 'styled-components';
import Button from 'shared/components/Button'; import Button from 'shared/components/Button';
import { useForm, Controller } from 'react-hook-form'; import { useForm, Controller } from 'react-hook-form';
@ -20,6 +19,7 @@ import updateApolloCache from 'shared/utils/cache';
import { useCurrentUser } from 'App/context'; import { useCurrentUser } from 'App/context';
import { Redirect } from 'react-router'; import { Redirect } from 'react-router';
import NOOP from 'shared/utils/noop'; import NOOP from 'shared/utils/noop';
import ControlledInput from 'shared/components/ControlledInput';
const DeleteUserWrapper = styled.div` const DeleteUserWrapper = styled.div`
display: flex; display: flex;
@ -77,12 +77,12 @@ const CreateUserButton = styled(Button)`
width: 100%; width: 100%;
`; `;
const AddUserInput = styled(Input)` const AddUserInput = styled(ControlledInput)`
margin-bottom: 8px; margin-bottom: 8px;
`; `;
const InputError = styled.span` const InputError = styled.span`
color: ${props => props.theme.colors.danger}; color: ${(props) => props.theme.colors.danger};
font-size: 12px; font-size: 12px;
`; `;
@ -91,7 +91,12 @@ type AddUserPopupProps = {
}; };
const AddUserPopup: React.FC<AddUserPopupProps> = ({ onAddUser }) => { const AddUserPopup: React.FC<AddUserPopupProps> = ({ onAddUser }) => {
const { register, handleSubmit, errors, control } = useForm<CreateUserData>(); const {
register,
handleSubmit,
formState: { errors },
control,
} = useForm<CreateUserData>();
const createUser = (data: CreateUserData) => { const createUser = (data: CreateUserData) => {
onAddUser(data); onAddUser(data);
@ -102,30 +107,25 @@ const AddUserPopup: React.FC<AddUserPopupProps> = ({ onAddUser }) => {
floatingLabel floatingLabel
width="100%" width="100%"
label="Full Name" label="Full Name"
id="fullName"
name="fullName"
variant="alternate" variant="alternate"
ref={register({ required: 'Full name is required' })} {...register('fullName', { required: 'Full name is required' })}
/> />
{errors.fullName && <InputError>{errors.fullName.message}</InputError>} {errors.fullName && <InputError>{errors.fullName.message}</InputError>}
<AddUserInput <AddUserInput
floatingLabel floatingLabel
width="100%" width="100%"
label="Email" label="Email"
id="email"
name="email"
variant="alternate" variant="alternate"
ref={register({ required: 'Email is required' })} {...register('email', { required: 'Email is required' })}
/> />
<Controller <Controller
control={control} control={control}
name="roleCode" name="roleCode"
rules={{ required: 'Role is required' }} rules={{ required: 'Role is required' }}
render={({ onChange, value }) => ( render={({ field }) => (
<Select <Select
{...field}
label="Role" label="Role"
value={value}
onChange={onChange}
options={[ options={[
{ label: 'Admin', value: 'admin' }, { label: 'Admin', value: 'admin' },
{ label: 'Member', value: 'member' }, { label: 'Member', value: 'member' },
@ -138,31 +138,25 @@ const AddUserPopup: React.FC<AddUserPopupProps> = ({ onAddUser }) => {
floatingLabel floatingLabel
width="100%" width="100%"
label="Username" label="Username"
id="username"
name="username"
variant="alternate" variant="alternate"
ref={register({ required: 'Username is required' })} {...register('username', { required: 'Username is required' })}
/> />
{errors.username && <InputError>{errors.username.message}</InputError>} {errors.username && <InputError>{errors.username.message}</InputError>}
<AddUserInput <AddUserInput
floatingLabel floatingLabel
width="100%" width="100%"
label="Initials" label="Initials"
id="initials"
name="initials"
variant="alternate" variant="alternate"
ref={register({ required: 'Initials is required' })} {...register('initials', { required: 'Initials is required' })}
/> />
{errors.initials && <InputError>{errors.initials.message}</InputError>} {errors.initials && <InputError>{errors.initials.message}</InputError>}
<AddUserInput <AddUserInput
floatingLabel floatingLabel
width="100%" width="100%"
label="Password" label="Password"
id="password"
name="password"
variant="alternate" variant="alternate"
type="password" type="password"
ref={register({ required: 'Password is required' })} {...register('password', { required: 'Password is required' })}
/> />
{errors.password && <InputError>{errors.password.message}</InputError>} {errors.password && <InputError>{errors.password.message}</InputError>}
<CreateUserButton type="submit">Create</CreateUserButton> <CreateUserButton type="submit">Create</CreateUserButton>
@ -179,10 +173,10 @@ const AdminRoute = () => {
const { user } = useCurrentUser(); const { user } = useCurrentUser();
const [deleteInvitedUser] = useDeleteInvitedUserAccountMutation({ const [deleteInvitedUser] = useDeleteInvitedUserAccountMutation({
update: (client, response) => { update: (client, response) => {
updateApolloCache<UsersQuery>(client, UsersDocument, cache => updateApolloCache<UsersQuery>(client, UsersDocument, (cache) =>
produce(cache, draftCache => { produce(cache, (draftCache) => {
draftCache.invitedUsers = cache.invitedUsers.filter( draftCache.invitedUsers = cache.invitedUsers.filter(
u => u.id !== response.data?.deleteInvitedUserAccount.invitedUser.id, (u) => u.id !== response.data?.deleteInvitedUserAccount.invitedUser.id,
); );
}), }),
); );
@ -190,9 +184,9 @@ const AdminRoute = () => {
}); });
const [deleteUser] = useDeleteUserAccountMutation({ const [deleteUser] = useDeleteUserAccountMutation({
update: (client, response) => { update: (client, response) => {
updateApolloCache<UsersQuery>(client, UsersDocument, cache => updateApolloCache<UsersQuery>(client, UsersDocument, (cache) =>
produce(cache, draftCache => { produce(cache, (draftCache) => {
draftCache.users = cache.users.filter(u => u.id !== response.data?.deleteUserAccount.userAccount.id); draftCache.users = cache.users.filter((u) => u.id !== response.data?.deleteUserAccount.userAccount.id);
}), }),
); );
}, },
@ -215,9 +209,12 @@ const AdminRoute = () => {
}, },
}); });
if (data && user) { if (data && user) {
/*
TODO: add permision check
if (user.roles.org !== 'admin') { if (user.roles.org !== 'admin') {
return <Redirect to="/" />; return <Redirect to="/" />;
} }
*/
return ( return (
<> <>
<GlobalTopNavbar projectID={null} onSaveProjectName={NOOP} name={null} /> <GlobalTopNavbar projectID={null} onSaveProjectName={NOOP} name={null} />
@ -225,12 +222,13 @@ const AdminRoute = () => {
initialTab={0} initialTab={0}
users={data.users} users={data.users}
invitedUsers={data.invitedUsers} invitedUsers={data.invitedUsers}
canInviteUser={user.roles.org === 'admin'} // canInviteUser={user.roles.org === 'admin'} TODO: add permision check
canInviteUser
onInviteUser={NOOP} onInviteUser={NOOP}
onUpdateUserPassword={() => { onUpdateUserPassword={() => {
hidePopup(); hidePopup();
}} }}
onDeleteInvitedUser={invitedUserID => { onDeleteInvitedUser={(invitedUserID) => {
deleteInvitedUser({ variables: { invitedUserID } }); deleteInvitedUser({ variables: { invitedUserID } });
hidePopup(); hidePopup();
}} }}
@ -238,12 +236,12 @@ const AdminRoute = () => {
deleteUser({ variables: { userID, newOwnerID } }); deleteUser({ variables: { userID, newOwnerID } });
hidePopup(); hidePopup();
}} }}
onAddUser={$target => { onAddUser={($target) => {
showPopup( showPopup(
$target, $target,
<Popup tab={0} title="Add member" onClose={() => hidePopup()}> <Popup tab={0} title="Add member" onClose={() => hidePopup()}>
<AddUserPopup <AddUserPopup
onAddUser={u => { onAddUser={(u) => {
const { roleCode, ...userData } = u; const { roleCode, ...userData } = u;
createUser({ variables: { ...userData, roleCode: roleCode.value } }); createUser({ variables: { ...userData, roleCode: roleCode.value } });
hidePopup(); hidePopup();

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Switch, Route, useHistory } from 'react-router-dom'; import { Switch, Route, useHistory, useLocation, Redirect } from 'react-router-dom';
import * as H from 'history'; import * as H from 'history';
import Dashboard from 'Dashboard'; import Dashboard from 'Dashboard';
@ -13,8 +13,6 @@ import Login from 'Auth';
import Register from 'Register'; import Register from 'Register';
import Profile from 'Profile'; import Profile from 'Profile';
import styled from 'styled-components'; import styled from 'styled-components';
import JwtDecode from 'jwt-decode';
import { setAccessToken } from 'shared/utils/accessToken';
import { useCurrentUser } from 'App/context'; import { useCurrentUser } from 'App/context';
const MainContent = styled.div` const MainContent = styled.div`
@ -26,67 +24,67 @@ const MainContent = styled.div`
flex-grow: 1; flex-grow: 1;
`; `;
type RefreshTokenResponse = { type ValidateTokenResponse = {
accessToken: string; valid: boolean;
setup?: null | { confirmToken: string }; userID: string;
}; };
const AuthorizedRoutes = () => { const UserRequiredRoute: React.FC<any> = ({ children }) => {
const history = useHistory(); const { user } = useCurrentUser();
const location = useLocation();
console.log('user required', user);
if (user) {
return children;
}
return (
<Redirect
to={{
pathname: '/login',
state: { redirect: location.pathname },
}}
/>
);
};
const Routes: React.FC = () => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const { setUser } = useCurrentUser(); const { setUser } = useCurrentUser();
useEffect(() => { useEffect(() => {
fetch('/auth/refresh_token', { fetch('/auth/validate', {
method: 'POST', method: 'POST',
credentials: 'include', credentials: 'include',
}).then(async x => { }).then(async (x) => {
const { status } = x; const response: ValidateTokenResponse = await x.json();
if (status === 400) { const { valid, userID } = response;
history.replace('/login'); if (valid) {
} else { setUser(userID);
const response: RefreshTokenResponse = await x.json();
const { accessToken, setup } = response;
if (setup) {
history.replace(`/register?confirmToken=${setup.confirmToken}`);
} else {
const claims: JWTToken = JwtDecode(accessToken);
const currentUser = {
id: claims.userId,
roles: { org: claims.orgRole, teams: new Map<string, string>(), projects: new Map<string, string>() },
};
setUser(currentUser);
setAccessToken(accessToken);
}
} }
setLoading(false); setLoading(false);
}); });
}, []); }, []);
return loading ? null : ( console.log('loading', loading);
<Switch> if (loading) return null;
<MainContent> return (
<Route exact path="/" component={Dashboard} />
<Route exact path="/projects" component={Projects} />
<Route path="/projects/:projectID" component={Project} />
<Route path="/teams/:teamID" component={Teams} />
<Route path="/profile" component={Profile} />
<Route path="/admin" component={Admin} />
<Route path="/tasks" component={MyTasks} />
</MainContent>
</Switch>
);
};
type RoutesProps = {
history: H.History;
};
const Routes: React.FC<RoutesProps> = () => (
<Switch> <Switch>
<Route exact path="/login" component={Login} /> <Route exact path="/login" component={Login} />
<Route exact path="/register" component={Register} /> <Route exact path="/register" component={Register} />
<Route exact path="/confirm" component={Confirm} /> <Route exact path="/confirm" component={Confirm} />
<AuthorizedRoutes /> <Switch>
<MainContent>
<Route path="/projects/:projectID" component={Project} />
<UserRequiredRoute>
<Route exact path="/" component={Dashboard} />
<Route exact path="/projects" component={Projects} />
<Route path="/teams/:teamID" component={Teams} />
<Route path="/profile" component={Profile} />
<Route path="/admin" component={Admin} />
<Route path="/tasks" component={MyTasks} />
</UserRequiredRoute>
</MainContent>
</Switch> </Switch>
); </Switch>
);
};
export default Routes; export default Routes;

35
frontend/src/App/Toast.ts Normal file
View File

@ -0,0 +1,35 @@
import styled from 'styled-components';
import { ToastContainer } from 'react-toastify';
const ToastedContainer = styled(ToastContainer).attrs({
// custom props
})`
.Toastify__toast-container {
}
.Toastify__toast {
padding: 5px;
margin-left: 5px;
margin-right: 5px;
border-radius: 10px;
background: #7367f0;
color: #fff;
}
.Toastify__toast--error {
background: ${props => props.theme.colors.danger};
}
.Toastify__toast--warning {
background: ${props => props.theme.colors.warning};
}
.Toastify__toast--success {
background: ${props => props.theme.colors.success};
}
.Toastify__toast-body {
}
.Toastify__progress-bar {
}
.Toastify__close-button {
display: none;
}
`;
export default ToastedContainer;

View File

@ -1,16 +1,13 @@
import React, { useState, useRef, useEffect } from 'react'; import React, { useState } from 'react';
import styled from 'styled-components/macro'; import styled from 'styled-components/macro';
import { useGetProjectsQuery } from 'shared/generated/graphql'; import { useGetProjectsQuery } from 'shared/generated/graphql';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import LoadingSpinner from 'shared/components/LoadingSpinner'; import LoadingSpinner from 'shared/components/LoadingSpinner';
import theme from './ThemeStyles';
import ControlledInput from 'shared/components/ControlledInput'; import ControlledInput from 'shared/components/ControlledInput';
import { CaretDown, CaretRight } from 'shared/icons'; import { CaretDown, CaretRight } from 'shared/icons';
import useStickyState from 'shared/hooks/useStickyState'; import useStickyState from 'shared/hooks/useStickyState';
import { usePopup } from 'shared/components/PopupMenu'; import { usePopup } from 'shared/components/PopupMenu';
const colors = [theme.colors.primary, theme.colors.secondary];
const TeamContainer = styled.div` const TeamContainer = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -27,6 +24,7 @@ const TeamTitleText = styled.span`
font-size: 14px; font-size: 14px;
font-weight: 700; font-weight: 700;
`; `;
const TeamProjects = styled.div` const TeamProjects = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -0,0 +1,89 @@
import React, { useState } from 'react';
import ProjectSettings, { DeleteConfirm, DELETE_INFO, PublicConfirm } from 'shared/components/ProjectSettings';
import {
useDeleteProjectMutation,
GetProjectsDocument,
useToggleProjectVisibilityMutation,
} from 'shared/generated/graphql';
import { usePopup, Popup } from 'shared/components/PopupMenu';
import produce from 'immer';
type ProjectPopupProps = {
history: any;
name: string;
publicOn: string | null;
projectID: string;
};
const ProjectPopup: React.FC<ProjectPopupProps> = ({ history, name, projectID, publicOn: initialPublicOn }) => {
const { hidePopup, setTab } = usePopup();
const [publicOn, setPublicOn] = useState(initialPublicOn);
const [toggleProjectVisibility] = useToggleProjectVisibilityMutation({
onCompleted: data => {
setPublicOn(data.toggleProjectVisibility.project.publicOn);
},
});
const [deleteProject] = useDeleteProjectMutation({
update: (client, deleteData) => {
const cacheData: any = client.readQuery({
query: GetProjectsDocument,
});
const newData = produce(cacheData, (draftState: any) => {
draftState.projects = draftState.projects.filter(
(project: any) => project.id !== deleteData.data?.deleteProject.project.id,
);
});
client.writeQuery({
query: GetProjectsDocument,
data: {
...newData,
},
});
},
});
return (
<>
<Popup title={null} tab={0}>
<ProjectSettings
publicOn={publicOn}
onToggleProjectVisible={visible => {
if (visible) {
setTab(2, { width: 300 });
} else {
toggleProjectVisibility({ variables: { projectID, isPublic: false } });
}
}}
onDeleteProject={() => {
setTab(1, { width: 300 });
}}
/>
</Popup>
<Popup title="Change to public?" tab={1}>
<PublicConfirm
onConfirm={() => {
if (projectID) {
toggleProjectVisibility({ variables: { projectID, isPublic: true } });
}
}}
/>
</Popup>
<Popup title={`Delete the "${name}" project?`} tab={1}>
<DeleteConfirm
description={DELETE_INFO.DELETE_PROJECTS.description}
deletedItems={DELETE_INFO.DELETE_PROJECTS.deletedItems}
onConfirmDelete={() => {
if (projectID) {
deleteProject({ variables: { projectID } });
hidePopup();
history.push('/projects');
}
}}
/>
</Popup>
</>
);
};
export default ProjectPopup;

View File

@ -1,76 +1,18 @@
import React from 'react'; import React from 'react';
import TopNavbar, { MenuItem } from 'shared/components/TopNavbar'; import TopNavbar, { MenuItem } from 'shared/components/TopNavbar';
import LoggedOutNavbar from 'shared/components/TopNavbar/LoggedOut';
import { ProfileMenu } from 'shared/components/DropdownMenu'; import { ProfileMenu } from 'shared/components/DropdownMenu';
import ProjectSettings, { DeleteConfirm, DELETE_INFO } from 'shared/components/ProjectSettings'; import { useHistory, useRouteMatch } from 'react-router';
import { useHistory } from 'react-router'; import { useCurrentUser } from 'App/context';
import { PermissionLevel, PermissionObjectType, useCurrentUser } from 'App/context'; import { RoleCode, useTopNavbarQuery } from 'shared/generated/graphql';
import {
RoleCode,
useTopNavbarQuery,
useDeleteProjectMutation,
GetProjectsDocument,
} from 'shared/generated/graphql';
import { usePopup, Popup } from 'shared/components/PopupMenu'; import { usePopup, Popup } from 'shared/components/PopupMenu';
import produce from 'immer';
import MiniProfile from 'shared/components/MiniProfile'; import MiniProfile from 'shared/components/MiniProfile';
import cache from 'App/cache'; import cache from 'App/cache';
import NotificationPopup, { NotificationItem } from 'shared/components/NotifcationPopup'; import NotificationPopup, { NotificationItem } from 'shared/components/NotifcationPopup';
import theme from './ThemeStyles'; import theme from 'App/ThemeStyles';
import ProjectFinder from './ProjectFinder'; import ProjectFinder from './ProjectFinder';
type ProjectPopupProps = { // TODO: Move to context based navbar?
history: any;
name: string;
projectID: string;
};
export const ProjectPopup: React.FC<ProjectPopupProps> = ({ history, name, projectID }) => {
const { hidePopup, setTab } = usePopup();
const [deleteProject] = useDeleteProjectMutation({
update: (client, deleteData) => {
const cacheData: any = client.readQuery({
query: GetProjectsDocument,
});
const newData = produce(cacheData, (draftState: any) => {
draftState.projects = draftState.projects.filter(
(project: any) => project.id !== deleteData.data?.deleteProject.project.id,
);
});
client.writeQuery({
query: GetProjectsDocument,
data: {
...newData,
},
});
},
});
return (
<>
<Popup title={null} tab={0}>
<ProjectSettings
onDeleteProject={() => {
setTab(1, { width: 300 });
}}
/>
</Popup>
<Popup title={`Delete the "${name}" project?`} tab={1}>
<DeleteConfirm
description={DELETE_INFO.DELETE_PROJECTS.description}
deletedItems={DELETE_INFO.DELETE_PROJECTS.deletedItems}
onConfirmDelete={() => {
if (projectID) {
deleteProject({ variables: { projectID } });
hidePopup();
history.push('/projects');
}
}}
/>
</Popup>
</>
);
};
type GlobalTopNavbarProps = { type GlobalTopNavbarProps = {
nameOnly?: boolean; nameOnly?: boolean;
@ -91,7 +33,7 @@ type GlobalTopNavbarProps = {
onRemoveInvitedFromBoard?: (email: string) => void; onRemoveInvitedFromBoard?: (email: string) => void;
}; };
const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({ const LoggedInNavbar: React.FC<GlobalTopNavbarProps> = ({
currentTab, currentTab,
onSetTab, onSetTab,
menuType, menuType,
@ -107,25 +49,9 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
onRemoveInvitedFromBoard, onRemoveInvitedFromBoard,
onRemoveFromBoard, onRemoveFromBoard,
}) => { }) => {
const { user, setUserRoles, setUser } = useCurrentUser(); const { data } = useTopNavbarQuery();
const { loading, data } = useTopNavbarQuery({
onCompleted: response => {
if (user && user.roles) {
setUserRoles({
org: user.roles.org,
teams: response.me.teamRoles.reduce((map, obj) => {
map.set(obj.teamID, obj.roleCode);
return map;
}, new Map<string, string>()),
projects: response.me.projectRoles.reduce((map, obj) => {
map.set(obj.projectID, obj.roleCode);
return map;
}, new Map<string, string>()),
});
}
},
});
const { showPopup, hidePopup } = usePopup(); const { showPopup, hidePopup } = usePopup();
const { setUser } = useCurrentUser();
const history = useHistory(); const history = useHistory();
const onLogout = () => { const onLogout = () => {
fetch('/auth/logout', { fetch('/auth/logout', {
@ -147,7 +73,7 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
<Popup title={null} tab={0}> <Popup title={null} tab={0}>
<ProfileMenu <ProfileMenu
onLogout={onLogout} onLogout={onLogout}
showAdminConsole={user ? user.roles.org === 'admin' : false} showAdminConsole // TODO: add permision check
onAdminConsole={() => { onAdminConsole={() => {
history.push('/admin'); history.push('/admin');
hidePopup(); hidePopup();
@ -186,10 +112,9 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
} }
}; };
if (!user) { // TODO: readd permision check
return null; // const userIsTeamOrProjectAdmin = user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID);
} const userIsTeamOrProjectAdmin = true;
const userIsTeamOrProjectAdmin = user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID);
const onInvitedMemberProfile = ($targetRef: React.RefObject<HTMLElement>, email: string) => { const onInvitedMemberProfile = ($targetRef: React.RefObject<HTMLElement>, email: string) => {
const member = projectInvitedMembers ? projectInvitedMembers.find(u => u.email === email) : null; const member = projectInvitedMembers ? projectInvitedMembers.find(u => u.email === email) : null;
if (member) { if (member) {
@ -249,6 +174,8 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
} }
}; };
const user = data ? data.me?.user : null;
return ( return (
<> <>
<TopNavbar <TopNavbar
@ -263,7 +190,7 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
); );
}} }}
currentTab={currentTab} currentTab={currentTab}
user={data ? data.me.user : null} user={user ?? null}
canEditProjectName={userIsTeamOrProjectAdmin} canEditProjectName={userIsTeamOrProjectAdmin}
canInviteUser={userIsTeamOrProjectAdmin} canInviteUser={userIsTeamOrProjectAdmin}
onMemberProfile={onMemberProfile} onMemberProfile={onMemberProfile}
@ -290,4 +217,46 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
); );
}; };
const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
currentTab,
onSetTab,
menuType,
teamID,
onChangeProjectOwner,
onChangeRole,
name,
popupContent,
projectMembers,
projectInvitedMembers,
onInviteUser,
onSaveProjectName,
onRemoveInvitedFromBoard,
onRemoveFromBoard,
}) => {
const { user } = useCurrentUser();
const match = useRouteMatch();
if (user) {
return (
<LoggedInNavbar
currentTab={currentTab}
projectID={null}
onSetTab={onSetTab}
menuType={menuType}
teamID={teamID}
onChangeRole={onChangeRole}
onChangeProjectOwner={onChangeProjectOwner}
name={name}
popupContent={popupContent}
projectMembers={projectMembers}
projectInvitedMembers={projectInvitedMembers}
onInviteUser={onInviteUser}
onSaveProjectName={onSaveProjectName}
onRemoveInvitedFromBoard={onRemoveInvitedFromBoard}
onRemoveFromBoard={onRemoveFromBoard}
/>
);
}
return <LoggedOutNavbar match={match.url} name={name} menuType={menuType} />;
};
export default GlobalTopNavbar; export default GlobalTopNavbar;

View File

@ -1,4 +1,4 @@
import { InMemoryCache } from 'apollo-cache-inmemory'; import { InMemoryCache } from '@apollo/client';
const cache = new InMemoryCache(); const cache = new InMemoryCache();

View File

@ -1,79 +1,20 @@
import React, { useContext } from 'react'; import React, { useContext } from 'react';
export enum PermissionLevel {
ORG,
TEAM,
PROJECT,
}
export enum PermissionObjectType {
ORG,
TEAM,
PROJECT,
TASK,
}
export type CurrentUserRoles = {
org: string;
teams: Map<string, string>;
projects: Map<string, string>;
};
export interface CurrentUserRaw {
id: string;
roles: CurrentUserRoles;
}
type UserContextState = { type UserContextState = {
user: CurrentUserRaw | null; user: string | null;
setUser: (user: CurrentUserRaw | null) => void; setUser: (user: string | null) => void;
setUserRoles: (roles: CurrentUserRoles) => void;
}; };
export const UserContext = React.createContext<UserContextState>({ export const UserContext = React.createContext<UserContextState>({
user: null, user: null,
setUser: _user => null, setUser: _user => null,
setUserRoles: roles => null,
}); });
export interface CurrentUser extends CurrentUserRaw {
isAdmin: (level: PermissionLevel, objectType: PermissionObjectType, subjectID?: string | null) => boolean;
isVisible: (level: PermissionLevel, objectType: PermissionObjectType, subjectID?: string | null) => boolean;
}
export const useCurrentUser = () => { export const useCurrentUser = () => {
const { user, setUser, setUserRoles } = useContext(UserContext); const { user, setUser } = useContext(UserContext);
let currentUser: CurrentUser | null = null;
if (user) {
currentUser = {
...user,
isAdmin(level: PermissionLevel, objectType: PermissionObjectType, subjectID?: string | null) {
if (user.roles.org === 'admin') {
return true;
}
switch (level) {
case PermissionLevel.TEAM:
return subjectID ? this.roles.teams.get(subjectID) === 'admin' : false;
default:
return false;
}
},
isVisible(level: PermissionLevel, objectType: PermissionObjectType, subjectID?: string | null) {
if (user.roles.org === 'admin') {
return true;
}
switch (level) {
case PermissionLevel.TEAM:
return subjectID ? this.roles.teams.get(subjectID) !== null : false;
default:
return false;
}
},
};
}
return { return {
user: currentUser, user,
setUser, setUser,
setUserRoles,
}; };
}; };

View File

@ -0,0 +1,6 @@
@font-face {
font-family: 'Open Sans';
src: url(../shared/fonts/OpenSans-Regular.ttf) format('truetype');
/* other formats include: 'woff2', 'truetype, 'opentype',
'embedded-opentype', and 'svg' */
}

View File

@ -1,75 +1,32 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import jwtDecode from 'jwt-decode'; import { BrowserRouter } from 'react-router-dom';
import { createBrowserHistory } from 'history';
import { Router } from 'react-router';
import { PopupProvider } from 'shared/components/PopupMenu'; import { PopupProvider } from 'shared/components/PopupMenu';
import { ToastContainer } from 'react-toastify';
import { setAccessToken } from 'shared/utils/accessToken';
import styled, { ThemeProvider } from 'styled-components'; import styled, { ThemeProvider } from 'styled-components';
import NormalizeStyles from './NormalizeStyles'; import NormalizeStyles from './NormalizeStyles';
import BaseStyles from './BaseStyles'; import BaseStyles from './BaseStyles';
import theme from './ThemeStyles'; import theme from './ThemeStyles';
import Routes from './Routes'; import Routes from './Routes';
import { UserContext, CurrentUserRaw, CurrentUserRoles, PermissionLevel, PermissionObjectType } from './context'; import ToastedContainer from './Toast';
import { UserContext } from './context';
import 'react-toastify/dist/ReactToastify.css'; import 'react-toastify/dist/ReactToastify.css';
import './fonts.css';
const StyledContainer = styled(ToastContainer).attrs({
// custom props
})`
.Toastify__toast-container {
}
.Toastify__toast {
padding: 5px;
margin-left: 5px;
margin-right: 5px;
border-radius: 10px;
background: #7367f0;
color: #fff;
}
.Toastify__toast--error {
background: ${props => props.theme.colors.danger};
}
.Toastify__toast--warning {
background: ${props => props.theme.colors.warning};
}
.Toastify__toast--success {
background: ${props => props.theme.colors.success};
}
.Toastify__toast-body {
}
.Toastify__progress-bar {
}
.Toastify__close-button {
display: none;
}
`;
const history = createBrowserHistory();
const App = () => { const App = () => {
const [user, setUser] = useState<CurrentUserRaw | null>(null); const [user, setUser] = useState<string | null>(null);
const setUserRoles = (roles: CurrentUserRoles) => {
if (user) {
setUser({
...user,
roles,
});
}
};
return ( return (
<> <>
<UserContext.Provider value={{ user, setUser, setUserRoles }}> <UserContext.Provider value={{ user, setUser }}>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<NormalizeStyles /> <NormalizeStyles />
<BaseStyles /> <BaseStyles />
<Router history={history}> <BrowserRouter>
<PopupProvider> <PopupProvider>
<Routes history={history} /> <Routes />
</PopupProvider> </PopupProvider>
</Router> </BrowserRouter>
<StyledContainer <ToastedContainer
position="bottom-right" position="bottom-right"
autoClose={5000} autoClose={5000}
hideProgressBar hideProgressBar

View File

@ -1,7 +1,5 @@
import React, { useState, useEffect, useContext } from 'react'; import React, { useState, useEffect, useContext } from 'react';
import { useHistory } from 'react-router'; import { useHistory, useLocation } from 'react-router';
import JwtDecode from 'jwt-decode';
import { setAccessToken } from 'shared/utils/accessToken';
import Login from 'shared/components/Login'; import Login from 'shared/components/Login';
import UserContext from 'App/context'; import UserContext from 'App/context';
import { Container, LoginWrapper } from './Styles'; import { Container, LoginWrapper } from './Styles';
@ -9,7 +7,9 @@ import { Container, LoginWrapper } from './Styles';
const Auth = () => { const Auth = () => {
const [invalidLoginAttempt, setInvalidLoginAttempt] = useState(0); const [invalidLoginAttempt, setInvalidLoginAttempt] = useState(0);
const history = useHistory(); const history = useHistory();
const location = useLocation<{ redirect: string } | undefined>();
const { setUser } = useContext(UserContext); const { setUser } = useContext(UserContext);
console.log('auth');
const login = ( const login = (
data: LoginFormData, data: LoginFormData,
setComplete: (val: boolean) => void, setComplete: (val: boolean) => void,
@ -22,7 +22,7 @@ const Auth = () => {
username: data.username, username: data.username,
password: data.password, password: data.password,
}), }),
}).then(async x => { }).then(async (x) => {
if (x.status === 401) { if (x.status === 401) {
setInvalidLoginAttempt(invalidLoginAttempt + 1); setInvalidLoginAttempt(invalidLoginAttempt + 1);
setError('username', { type: 'error', message: 'Invalid username' }); setError('username', { type: 'error', message: 'Invalid username' });
@ -30,43 +30,28 @@ const Auth = () => {
setComplete(true); setComplete(true);
} else { } else {
const response = await x.json(); const response = await x.json();
const { accessToken } = response; const { userID } = response;
const claims: JWTToken = JwtDecode(accessToken); setUser(userID);
const currentUser = { if (location.state && location.state.redirect) {
id: claims.userId, history.push(location.state.redirect);
roles: { org: claims.orgRole, teams: new Map<string, string>(), projects: new Map<string, string>() }, } else {
};
setUser(currentUser);
setComplete(true);
setAccessToken(accessToken);
history.push('/'); history.push('/');
} }
}
}); });
}; };
useEffect(() => { useEffect(() => {
fetch('/auth/refresh_token', { fetch('/auth/validate', {
method: 'POST', method: 'POST',
credentials: 'include', credentials: 'include',
}).then(async x => { }).then(async (x) => {
const { status } = x; const response = await x.json();
if (status === 200) { const { valid, userID } = response;
const response: RefreshTokenResponse = await x.json(); if (valid) {
const { accessToken, setup } = response; setUser(userID);
if (setup) {
history.replace(`/register?confirmToken=${setup.confirmToken}`);
} else {
const claims: JWTToken = JwtDecode(accessToken);
const currentUser = {
id: claims.userId,
roles: { org: claims.orgRole, teams: new Map<string, string>(), projects: new Map<string, string>() },
};
setUser(currentUser);
setAccessToken(accessToken);
history.replace('/projects'); history.replace('/projects');
} }
}
}); });
}, []); }, []);

View File

@ -1,18 +1,13 @@
import React, { useState } from 'react'; import React from 'react';
import axios from 'axios';
import Confirm from 'shared/components/Confirm'; import Confirm from 'shared/components/Confirm';
import { useHistory, useLocation } from 'react-router'; import { useHistory, useLocation } from 'react-router';
import * as QueryString from 'query-string'; import * as QueryString from 'query-string';
import { toast } from 'react-toastify';
import { Container, LoginWrapper } from './Styles';
import JwtDecode from 'jwt-decode';
import { setAccessToken } from 'shared/utils/accessToken';
import { useCurrentUser } from 'App/context'; import { useCurrentUser } from 'App/context';
import { Container, LoginWrapper } from './Styles';
const UsersConfirm = () => { const UsersConfirm = () => {
const history = useHistory(); const history = useHistory();
const location = useLocation(); const location = useLocation();
const [registered, setRegistered] = useState(false);
const params = QueryString.parse(location.search); const params = QueryString.parse(location.search);
const { setUser } = useCurrentUser(); const { setUser } = useCurrentUser();
return ( return (
@ -31,18 +26,8 @@ const UsersConfirm = () => {
const { status } = x; const { status } = x;
if (status === 200) { if (status === 200) {
const response = await x.json(); const response = await x.json();
const { accessToken } = response; const { userID } = response;
const claims: JWTToken = JwtDecode(accessToken); setUser(userID);
const currentUser = {
id: claims.userId,
roles: {
org: claims.orgRole,
teams: new Map<string, string>(),
projects: new Map<string, string>(),
},
};
setUser(currentUser);
setAccessToken(accessToken);
history.push('/'); history.push('/');
} else { } else {
setFailed(); setFailed();

View File

@ -26,6 +26,7 @@ import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import DueDateManager from 'shared/components/DueDateManager'; import DueDateManager from 'shared/components/DueDateManager';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import useStickyState from 'shared/hooks/useStickyState'; import useStickyState from 'shared/hooks/useStickyState';
import { StaticContext } from 'react-router';
import MyTasksSortPopup from './MyTasksSort'; import MyTasksSortPopup from './MyTasksSort';
import MyTasksStatusPopup from './MyTasksStatus'; import MyTasksStatusPopup from './MyTasksStatus';
import TaskEntry from './TaskEntry'; import TaskEntry from './TaskEntry';
@ -61,11 +62,7 @@ function prettySort(sort: MyTasksSort) {
if (sort === MyTasksSort.None) { if (sort === MyTasksSort.None) {
return 'Sort'; return 'Sort';
} }
return `Sort: ${sort.charAt(0) + return `Sort: ${sort.charAt(0) + sort.slice(1).toLowerCase().replace(/_/gi, ' ')}`;
sort
.slice(1)
.toLowerCase()
.replace(/_/gi, ' ')}`;
} }
type Group = { type Group = {
@ -75,7 +72,7 @@ type Group = {
}; };
const DueDateEditorLabel = styled.div` const DueDateEditorLabel = styled.div`
align-items: center; align-items: center;
color: ${props => props.theme.colors.text.primary}; color: ${(props) => props.theme.colors.text.primary};
font-size: 11px; font-size: 11px;
padding: 0 8px; padding: 0 8px;
@ -107,16 +104,16 @@ const ProjectActionWrapper = styled.div<{ disabled?: boolean }>`
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 15px; font-size: 15px;
color: ${props => props.theme.colors.text.primary}; color: ${(props) => props.theme.colors.text.primary};
&:not(:last-of-type) { &:not(:last-of-type) {
margin-right: 16px; margin-right: 16px;
} }
&:hover { &:hover {
color: ${props => props.theme.colors.text.secondary}; color: ${(props) => props.theme.colors.text.secondary};
} }
${props => ${(props) =>
props.disabled && props.disabled &&
css` css`
opacity: 0.5; opacity: 0.5;
@ -150,7 +147,7 @@ const ProjectAction: React.FC<ProjectActionProps> = ({ onClick, disabled = false
const EditorPositioner = styled.div<{ top: number; left: number }>` const EditorPositioner = styled.div<{ top: number; left: number }>`
position: absolute; position: absolute;
top: ${p => p.top}px; top: ${(p) => p.top}px;
justify-content: flex-end; justify-content: flex-end;
margin-left: -100vw; margin-left: -100vw;
z-index: 10000; z-index: 10000;
@ -160,7 +157,7 @@ const EditorPositioner = styled.div<{ top: number; left: number }>`
height: 0; height: 0;
position: fixed; position: fixed;
width: 100vw; width: 100vw;
left: ${p => p.left}px; left: ${(p) => p.left}px;
`; `;
const EditorPositionerContents = styled.div` const EditorPositionerContents = styled.div`
@ -168,15 +165,15 @@ const EditorPositionerContents = styled.div`
`; `;
const EditorContainer = styled.div<{ width: number }>` const EditorContainer = styled.div<{ width: number }>`
border: 1px solid ${props => props.theme.colors.primary}; border: 1px solid ${(props) => props.theme.colors.primary};
background: ${props => props.theme.colors.bg.secondary}; background: ${(props) => props.theme.colors.bg.secondary};
position: relative; position: relative;
width: ${p => p.width}px; width: ${(p) => p.width}px;
`; `;
const EditorCell = styled.div<{ width: number }>` const EditorCell = styled.div<{ width: number }>`
display: flex; display: flex;
width: ${p => p.width}px; width: ${(p) => p.width}px;
`; `;
// TABLE // TABLE
@ -224,7 +221,7 @@ const TaskGroupItems = styled.div`
`; `;
const ProjectPill = styled.div` const ProjectPill = styled.div`
background-color: ${props => props.theme.colors.bg.primary}; background-color: ${(props) => props.theme.colors.bg.primary};
text-overflow: ellipsis; text-overflow: ellipsis;
border-radius: 10px; border-radius: 10px;
box-sizing: border-box; box-sizing: border-box;
@ -250,7 +247,7 @@ const ProjectPillName = styled.span`
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
color: ${props => props.theme.colors.text.primary}; color: ${(props) => props.theme.colors.text.primary};
`; `;
const ProjectPillColor = styled.svg` const ProjectPillColor = styled.svg`
@ -299,7 +296,7 @@ const OptionTitle = styled.div`
white-space: nowrap; white-space: nowrap;
`; `;
const OptionSubTitle = styled.div` const OptionSubTitle = styled.div`
color: ${props => props.theme.colors.text.primary}; color: ${(props) => props.theme.colors.text.primary};
font-size: 11px; font-size: 11px;
margin-left: 8px; margin-left: 8px;
min-width: 50px; min-width: 50px;
@ -319,7 +316,7 @@ const Option = ({ innerProps, data }: any) => {
}; };
const TaskGroupHeaderContents = styled.div<{ width: number }>` const TaskGroupHeaderContents = styled.div<{ width: number }>`
width: ${p => p.width}px; width: ${(p) => p.width}px;
left: 0; left: 0;
position: absolute; position: absolute;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif;
@ -356,13 +353,13 @@ const TaskGroupMinify = styled.div`
transition-property: background, border, box-shadow, fill; transition-property: background, border, box-shadow, fill;
cursor: pointer; cursor: pointer;
svg { svg {
fill: ${props => props.theme.colors.text.primary}; fill: ${(props) => props.theme.colors.text.primary};
transition-duration: 0.2s; transition-duration: 0.2s;
transition-property: background, border, box-shadow, fill; transition-property: background, border, box-shadow, fill;
} }
&:hover svg { &:hover svg {
fill: ${props => props.theme.colors.text.secondary}; fill: ${(props) => props.theme.colors.text.secondary};
} }
`; `;
const TaskGroupName = styled.div` const TaskGroupName = styled.div`
@ -371,7 +368,7 @@ const TaskGroupName = styled.div`
display: flex; display: flex;
height: 50px; height: 50px;
min-width: 1px; min-width: 1px;
color: ${props => props.theme.colors.text.secondary}; color: ${(props) => props.theme.colors.text.secondary};
font-weight: 400; font-weight: 400;
`; `;
@ -393,7 +390,7 @@ const Row = styled.div`
`; `;
const RowHeaderLeft = styled.div<{ width: number }>` const RowHeaderLeft = styled.div<{ width: number }>`
width: ${p => p.width}px; width: ${(p) => p.width}px;
align-items: stretch; align-items: stretch;
display: flex; display: flex;
@ -405,7 +402,7 @@ const RowHeaderLeft = styled.div<{ width: number }>`
`; `;
const RowHeaderLeftInner = styled.div` const RowHeaderLeftInner = styled.div`
align-items: stretch; align-items: stretch;
color: ${props => props.theme.colors.text.primary}; color: ${(props) => props.theme.colors.text.primary};
display: flex; display: flex;
flex: 1 0 auto; flex: 1 0 auto;
font-size: 12px; font-size: 12px;
@ -429,7 +426,7 @@ const RowHeaderLeftNameText = styled.div`
`; `;
const RowHeaderRight = styled.div<{ left: number }>` const RowHeaderRight = styled.div<{ left: number }>`
left: ${p => p.left}px; left: ${(p) => p.left}px;
right: 0px; right: 0px;
height: 37px; height: 37px;
position: absolute; position: absolute;
@ -461,7 +458,7 @@ const RowHeaderRightContainer = styled.div`
`; `;
const ItemWrapper = styled.div<{ width: number }>` const ItemWrapper = styled.div<{ width: number }>`
width: ${p => p.width}px; width: ${(p) => p.width}px;
align-items: center; align-items: center;
border: 1px solid #414561; border: 1px solid #414561;
border-bottom: 0; border-bottom: 0;
@ -474,11 +471,11 @@ const ItemWrapper = styled.div<{ width: number }>`
margin-right: -1px; margin-right: -1px;
padding: 0 8px; padding: 0 8px;
position: relative; position: relative;
color: ${props => props.theme.colors.text.primary}; color: ${(props) => props.theme.colors.text.primary};
border-bottom: 1px solid #414561; border-bottom: 1px solid #414561;
&:hover { &:hover {
background: ${props => props.theme.colors.primary}; background: ${(props) => props.theme.colors.primary};
color: ${props => props.theme.colors.text.secondary}; color: ${(props) => props.theme.colors.text.secondary};
} }
`; `;
const ItemsContainer = styled.div` const ItemsContainer = styled.div`
@ -566,13 +563,13 @@ const Projects = () => {
onDueDateChange={(task, dueDate, hasTime) => { onDueDateChange={(task, dueDate, hasTime) => {
if (dateEditor.task) { if (dateEditor.task) {
updateTaskDueDate({ variables: { taskID: dateEditor.task.id, dueDate, hasTime } }); updateTaskDueDate({ variables: { taskID: dateEditor.task.id, dueDate, hasTime } });
setDateEditor(prev => ({ ...prev, task: { ...task, dueDate: dueDate.toISOString(), hasTime } })); setDateEditor((prev) => ({ ...prev, task: { ...task, dueDate: dueDate.toISOString(), hasTime } }));
} }
}} }}
onRemoveDueDate={task => { onRemoveDueDate={(task) => {
if (dateEditor.task) { if (dateEditor.task) {
updateTaskDueDate({ variables: { taskID: dateEditor.task.id, dueDate: null, hasTime: false } }); updateTaskDueDate({ variables: { taskID: dateEditor.task.id, dueDate: null, hasTime: false } });
setDateEditor(prev => ({ ...prev, task: { ...task, hasTime: false } })); setDateEditor((prev) => ({ ...prev, task: { ...task, hasTime: false } }));
} }
}} }}
/> />
@ -587,8 +584,8 @@ const Projects = () => {
updateApolloCache<MyTasksQuery>( updateApolloCache<MyTasksQuery>(
client, client,
MyTasksDocument, MyTasksDocument,
cache => (cache) =>
produce(cache, draftCache => { produce(cache, (draftCache) => {
if (newTaskData.data) { if (newTaskData.data) {
draftCache.myTasks.tasks.unshift(newTaskData.data.createTask); draftCache.myTasks.tasks.unshift(newTaskData.data.createTask);
} }
@ -618,7 +615,7 @@ const Projects = () => {
groups.push({ groups.push({
id: 'recently-assigned', id: 'recently-assigned',
name: 'Recently Assigned', name: 'Recently Assigned',
tasks: data.myTasks.tasks.map(task => ({ tasks: data.myTasks.tasks.map((task) => ({
...task, ...task,
labels: [], labels: [],
position: 0, position: 0,
@ -628,27 +625,27 @@ const Projects = () => {
let { tasks } = data.myTasks; let { tasks } = data.myTasks;
if (filters.sort === MyTasksSort.DueDate) { if (filters.sort === MyTasksSort.DueDate) {
const group: Group = { id: 'due_date', name: null, tasks: [] }; const group: Group = { id: 'due_date', name: null, tasks: [] };
data.myTasks.tasks.forEach(task => { data.myTasks.tasks.forEach((task) => {
if (task.dueDate) { if (task.dueDate) {
group.tasks.push({ ...task, labels: [], position: 0 }); group.tasks.push({ ...task, labels: [], position: 0 });
} }
}); });
groups.push(group); groups.push(group);
tasks = tasks.filter(t => t.dueDate === null); tasks = tasks.filter((t) => t.dueDate === null);
} }
const projects = new Map<string, Array<Task>>(); const projects = new Map<string, Array<Task>>();
data.myTasks.projects.forEach(p => { data.myTasks.projects.forEach((p) => {
if (!projects.has(p.projectID)) { if (!projects.has(p.projectID)) {
projects.set(p.projectID, []); projects.set(p.projectID, []);
} }
const prev = projects.get(p.projectID); const prev = projects.get(p.projectID);
const task = tasks.find(t => t.id === p.taskID); const task = tasks.find((t) => t.id === p.taskID);
if (prev && task) { if (prev && task) {
projects.set(p.projectID, [...prev, { ...task, labels: [], position: 0 }]); projects.set(p.projectID, [...prev, { ...task, labels: [], position: 0 }]);
} }
}); });
for (const [id, pTasks] of projects) { for (const [id, pTasks] of projects) {
const project = data.projects.find(c => c.id === id); const project = data.projects.find((c) => c.id === id);
if (pTasks.length === 0) continue; if (pTasks.length === 0) continue;
if (project) { if (project) {
groups.push({ groups.push({
@ -681,13 +678,13 @@ const Projects = () => {
<ProjectActions /> <ProjectActions />
<ProjectActions> <ProjectActions>
<ProjectAction <ProjectAction
onClick={$target => { onClick={($target) => {
showPopup( showPopup(
$target, $target,
<MyTasksStatusPopup <MyTasksStatusPopup
status={filters.status} status={filters.status}
onChangeStatus={status => { onChangeStatus={(status) => {
setFilters(prev => ({ ...prev, status })); setFilters((prev) => ({ ...prev, status }));
hidePopup(); hidePopup();
}} }}
/>, />,
@ -699,13 +696,13 @@ const Projects = () => {
<ProjectActionText>{prettyStatus(filters.status)}</ProjectActionText> <ProjectActionText>{prettyStatus(filters.status)}</ProjectActionText>
</ProjectAction> </ProjectAction>
<ProjectAction <ProjectAction
onClick={$target => { onClick={($target) => {
showPopup( showPopup(
$target, $target,
<MyTasksSortPopup <MyTasksSortPopup
sort={filters.sort} sort={filters.sort}
onChangeSort={sort => { onChangeSort={(sort) => {
setFilters(prev => ({ ...prev, sort })); setFilters((prev) => ({ ...prev, sort }));
hidePopup(); hidePopup();
}} }}
/>, />,
@ -752,8 +749,8 @@ const Projects = () => {
<VerticalScoller> <VerticalScoller>
<VerticalScollerInner> <VerticalScollerInner>
<TableContents> <TableContents>
{groups.map(group => { {groups.map((group) => {
const isMinified = minified.find(m => m === group.id) ?? false; const isMinified = minified.find((m) => m === group.id) ?? false;
return ( return (
<TaskGroupContainer key={group.id}> <TaskGroupContainer key={group.id}>
{group.name && ( {group.name && (
@ -761,9 +758,9 @@ const Projects = () => {
<TaskGroupHeaderContents width={leftRow}> <TaskGroupHeaderContents width={leftRow}>
<TaskGroupMinify <TaskGroupMinify
onClick={() => { onClick={() => {
setMinified(prev => { setMinified((prev) => {
if (isMinified) { if (isMinified) {
return prev.filter(c => c !== group.id); return prev.filter((c) => c !== group.id);
} }
return [...prev, group.id]; return [...prev, group.id];
}); });
@ -781,14 +778,14 @@ const Projects = () => {
)} )}
<TaskGroupItems> <TaskGroupItems>
{!isMinified && {!isMinified &&
group.tasks.map(task => { group.tasks.map((task) => {
const projectID = data.myTasks.projects.find(t => t.taskID === task.id)?.projectID; const projectID = data.myTasks.projects.find((t) => t.taskID === task.id)?.projectID;
const projectName = data.projects.find(p => p.id === projectID)?.name; const projectName = data.projects.find((p) => p.id === projectID)?.name;
return ( return (
<TaskEntry <TaskEntry
key={task.id} key={task.id}
complete={task.complete ?? false} complete={task.complete ?? false}
onToggleComplete={complete => { onToggleComplete={(complete) => {
setTaskComplete({ variables: { taskID: task.id, complete } }); setTaskComplete({ variables: { taskID: task.id, complete } });
}} }}
onTaskDetails={() => { onTaskDetails={() => {
@ -801,9 +798,11 @@ const Projects = () => {
dueDate={task.dueDate} dueDate={task.dueDate}
hasTime={task.hasTime ?? false} hasTime={task.hasTime ?? false}
name={task.name} name={task.name}
onEditName={name => updateTaskName({ variables: { taskID: task.id, name } })} onEditName={(name) => updateTaskName({ variables: { taskID: task.id, name } })}
onEditProject={onEditProject} onEditProject={onEditProject}
onEditDueDate={$target => onEditDueDate({ ...task, position: 0, labels: [] }, $target)} onEditDueDate={($target) =>
onEditDueDate({ ...task, position: 0, labels: [] }, $target)
}
/> />
); );
})} })}
@ -856,12 +855,12 @@ const Projects = () => {
)} )}
<Route <Route
path={`${match.path}/c/:taskID`} path={`${match.path}/c/:taskID`}
render={(routeProps: RouteComponentProps<TaskRouteProps>) => ( render={() => {
return (
<Details <Details
refreshCache={NOOP} refreshCache={NOOP}
availableMembers={[]} availableMembers={[]}
projectURL={`${match.url}`} projectURL={`${match.url}`}
taskID={routeProps.match.params.taskID}
onTaskNameChange={(updatedTask, newName) => { onTaskNameChange={(updatedTask, newName) => {
updateTaskName({ variables: { taskID: updatedTask.id, name: newName } }); updateTaskName({ variables: { taskID: updatedTask.id, name: newName } });
}} }}
@ -880,7 +879,7 @@ const Projects = () => {
}); });
*/ */
}} }}
onDeleteTask={deletedTask => { onDeleteTask={(deletedTask) => {
// deleteTask({ variables: { taskID: deletedTask.id } }); // deleteTask({ variables: { taskID: deletedTask.id } });
history.push(`${match.url}`); history.push(`${match.url}`);
}} }}
@ -902,7 +901,8 @@ const Projects = () => {
*/ */
}} }}
/> />
)} );
}}
/> />
</> </>
); );

View File

@ -1,7 +1,6 @@
import React, { useRef, useEffect } from 'react'; import React, { useRef, useEffect } from 'react';
import styled from 'styled-components/macro'; import styled from 'styled-components/macro';
import GlobalTopNavbar from 'App/TopNavbar'; import GlobalTopNavbar from 'App/TopNavbar';
import { getAccessToken } from 'shared/utils/accessToken';
import Settings from 'shared/components/Settings'; import Settings from 'shared/components/Settings';
import { import {
useMeQuery, useMeQuery,
@ -45,18 +44,15 @@ const Projects = () => {
name="file" name="file"
style={{ display: 'none' }} style={{ display: 'none' }}
ref={$fileUpload} ref={$fileUpload}
onChange={e => { onChange={(e) => {
if (e.target.files) { if (e.target.files) {
const fileData = new FormData(); const fileData = new FormData();
fileData.append('file', e.target.files[0]); fileData.append('file', e.target.files[0]);
const accessToken = getAccessToken();
axios axios
.post('/users/me/avatar', fileData, { .post('/users/me/avatar', fileData, {
headers: { withCredentials: true,
Authorization: `Bearer ${accessToken}`,
},
}) })
.then(res => { .then((res) => {
if ($fileUpload && $fileUpload.current) { if ($fileUpload && $fileUpload.current) {
$fileUpload.current.value = ''; $fileUpload.current.value = '';
refetch(); refetch();
@ -66,7 +62,7 @@ const Projects = () => {
}} }}
/> />
<GlobalTopNavbar projectID={null} onSaveProjectName={NOOP} name={null} /> <GlobalTopNavbar projectID={null} onSaveProjectName={NOOP} name={null} />
{!loading && data && ( {!loading && data && data.me && (
<Settings <Settings
profile={data.me.user} profile={data.me.user}
onProfileAvatarChange={() => { onProfileAvatarChange={() => {
@ -75,13 +71,13 @@ const Projects = () => {
} }
}} }}
onResetPassword={(password, done) => { onResetPassword={(password, done) => {
updateUserPassword({ variables: { userID: user.id, password } }); updateUserPassword({ variables: { userID: user, password } });
toast('Password was changed!'); toast('Password was changed!');
done(); done();
}} }}
onChangeUserInfo={(d, done) => { onChangeUserInfo={(d, done) => {
updateUserInfo({ updateUserInfo({
variables: { name: d.full_name, bio: d.bio, email: d.email, initials: d.initials }, variables: { name: d.fullName, bio: d.bio, email: d.email, initials: d.initials },
}); });
toast('User info was saved!'); toast('User info was saved!');
done(); done();

View File

@ -7,12 +7,13 @@ import { Popup, usePopup } from 'shared/components/PopupMenu';
import produce from 'immer'; import produce from 'immer';
import { mixin } from 'shared/utils/styles'; import { mixin } from 'shared/utils/styles';
import Member from 'shared/components/Member'; import Member from 'shared/components/Member';
import { useLabelsQuery } from 'shared/generated/graphql';
const FilterMember = styled(Member)` const FilterMember = styled(Member)`
margin: 2px 0; margin: 2px 0;
&:hover { &:hover {
cursor: pointer; cursor: pointer;
background: ${props => props.theme.colors.primary}; background: ${(props) => props.theme.colors.primary};
} }
`; `;
@ -28,7 +29,7 @@ export const Label = styled.li`
`; `;
export const CardLabel = styled.span<{ active: boolean; color: string }>` export const CardLabel = styled.span<{ active: boolean; color: string }>`
${props => ${(props) =>
props.active && props.active &&
css` css`
margin-left: 4px; margin-left: 4px;
@ -43,7 +44,7 @@ export const CardLabel = styled.span<{ active: boolean; color: string }>`
padding: 6px 12px; padding: 6px 12px;
position: relative; position: relative;
transition: padding 85ms, margin 85ms, box-shadow 85ms; transition: padding 85ms, margin 85ms, box-shadow 85ms;
background-color: ${props => props.color}; background-color: ${(props) => props.color};
color: #fff; color: #fff;
display: block; display: block;
max-width: 100%; max-width: 100%;
@ -71,7 +72,7 @@ export const ActionItem = styled.li`
align-items: center; align-items: center;
font-size: 14px; font-size: 14px;
&:hover { &:hover {
background: ${props => props.theme.colors.primary}; background: ${(props) => props.theme.colors.primary};
} }
`; `;
@ -80,7 +81,7 @@ export const ActionTitle = styled.span`
`; `;
const ActionItemSeparator = styled.li` const ActionItemSeparator = styled.li`
color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.4)}; color: ${(props) => mixin.rgba(props.theme.colors.text.primary, 0.4)};
font-size: 12px; font-size: 12px;
padding-left: 4px; padding-left: 4px;
padding-right: 4px; padding-right: 4px;
@ -110,15 +111,16 @@ const ActionItemLine = styled.div`
type FilterMetaProps = { type FilterMetaProps = {
filters: TaskMetaFilters; filters: TaskMetaFilters;
userID: string; userID: string;
labels: React.RefObject<Array<ProjectLabel>>; projectID: string;
members: React.RefObject<Array<TaskUser>>; members: React.RefObject<Array<TaskUser>>;
onChangeTaskMetaFilter: (filters: TaskMetaFilters) => void; onChangeTaskMetaFilter: (filters: TaskMetaFilters) => void;
}; };
const FilterMeta: React.FC<FilterMetaProps> = ({ filters, onChangeTaskMetaFilter, userID, labels, members }) => { const FilterMeta: React.FC<FilterMetaProps> = ({ filters, onChangeTaskMetaFilter, userID, projectID, members }) => {
const [currentFilters, setFilters] = useState(filters); const [currentFilters, setFilters] = useState(filters);
const [nameFilter, setNameFilter] = useState(filters.taskName ? filters.taskName.name : ''); const [nameFilter, setNameFilter] = useState(filters.taskName ? filters.taskName.name : '');
const [currentLabel, setCurrentLabel] = useState(''); const [currentLabel, setCurrentLabel] = useState('');
const { data } = useLabelsQuery({ variables: { projectID } });
const handleSetFilters = (f: TaskMetaFilters) => { const handleSetFilters = (f: TaskMetaFilters) => {
setFilters(f); setFilters(f);
@ -127,7 +129,7 @@ const FilterMeta: React.FC<FilterMetaProps> = ({ filters, onChangeTaskMetaFilter
const handleNameChange = (nFilter: string) => { const handleNameChange = (nFilter: string) => {
handleSetFilters( handleSetFilters(
produce(currentFilters, draftFilters => { produce(currentFilters, (draftFilters) => {
draftFilters.taskName = nFilter !== '' ? { name: nFilter } : null; draftFilters.taskName = nFilter !== '' ? { name: nFilter } : null;
}), }),
); );
@ -138,7 +140,7 @@ const FilterMeta: React.FC<FilterMetaProps> = ({ filters, onChangeTaskMetaFilter
const handleSetDueDate = (filterType: DueDateFilterType, label: string) => { const handleSetDueDate = (filterType: DueDateFilterType, label: string) => {
handleSetFilters( handleSetFilters(
produce(currentFilters, draftFilters => { produce(currentFilters, (draftFilters) => {
if (draftFilters.dueDate && draftFilters.dueDate.type === filterType) { if (draftFilters.dueDate && draftFilters.dueDate.type === filterType) {
draftFilters.dueDate = null; draftFilters.dueDate = null;
} else { } else {
@ -157,7 +159,7 @@ const FilterMeta: React.FC<FilterMetaProps> = ({ filters, onChangeTaskMetaFilter
<ActionsList> <ActionsList>
<TaskNameInput <TaskNameInput
width="100%" width="100%"
onChange={e => handleNameChange(e.currentTarget.value)} onChange={(e) => handleNameChange(e.currentTarget.value)}
value={nameFilter} value={nameFilter}
autoFocus autoFocus
variant="alternate" variant="alternate"
@ -167,14 +169,14 @@ const FilterMeta: React.FC<FilterMetaProps> = ({ filters, onChangeTaskMetaFilter
<ActionItem <ActionItem
onClick={() => { onClick={() => {
handleSetFilters( handleSetFilters(
produce(currentFilters, draftFilters => { produce(currentFilters, (draftFilters) => {
if (members.current) { if (members.current) {
const member = members.current.find(m => m.id === userID); const member = members.current.find((m) => m.id === userID);
const draftMember = draftFilters.members.find(m => m.id === userID); const draftMember = draftFilters.members.find((m) => m.id === userID);
if (member && !draftMember) { if (member && !draftMember) {
draftFilters.members.push({ id: userID, username: member.username ? member.username : '' }); draftFilters.members.push({ id: userID, username: member.username ? member.username : '' });
} else { } else {
draftFilters.members = draftFilters.members.filter(m => m.id !== userID); draftFilters.members = draftFilters.members.filter((m) => m.id !== userID);
} }
} }
}), }),
@ -185,7 +187,7 @@ const FilterMeta: React.FC<FilterMetaProps> = ({ filters, onChangeTaskMetaFilter
<User width={12} height={12} /> <User width={12} height={12} />
</ItemIcon> </ItemIcon>
<ActionTitle>Just my tasks</ActionTitle> <ActionTitle>Just my tasks</ActionTitle>
{currentFilters.members.find(m => m.id === userID) && <ActiveIcon width={12} height={12} />} {currentFilters.members.find((m) => m.id === userID) && <ActiveIcon width={12} height={12} />}
</ActionItem> </ActionItem>
<ActionItem onClick={() => handleSetDueDate(DueDateFilterType.THIS_WEEK, 'Due this week')}> <ActionItem onClick={() => handleSetDueDate(DueDateFilterType.THIS_WEEK, 'Due this week')}>
<ItemIcon> <ItemIcon>
@ -228,10 +230,10 @@ const FilterMeta: React.FC<FilterMetaProps> = ({ filters, onChangeTaskMetaFilter
</Popup> </Popup>
<Popup tab={1} title="By Labels"> <Popup tab={1} title="By Labels">
<Labels> <Labels>
{labels.current && {data &&
labels.current data.findProject.labels
// .filter(label => '' === '' || (label.name && label.name.toLowerCase().startsWith(''.toLowerCase()))) // .filter(label => '' === '' || (label.name && label.name.toLowerCase().startsWith(''.toLowerCase())))
.map(label => ( .map((label) => (
<Label key={label.id}> <Label key={label.id}>
<CardLabel <CardLabel
key={label.id} key={label.id}
@ -242,9 +244,9 @@ const FilterMeta: React.FC<FilterMetaProps> = ({ filters, onChangeTaskMetaFilter
}} }}
onClick={() => { onClick={() => {
handleSetFilters( handleSetFilters(
produce(currentFilters, draftFilters => { produce(currentFilters, (draftFilters) => {
if (draftFilters.labels.find(l => l.id === label.id)) { if (draftFilters.labels.find((l) => l.id === label.id)) {
draftFilters.labels = draftFilters.labels.filter(l => l.id !== label.id); draftFilters.labels = draftFilters.labels.filter((l) => l.id !== label.id);
} else { } else {
draftFilters.labels.push({ draftFilters.labels.push({
id: label.id, id: label.id,
@ -265,16 +267,16 @@ const FilterMeta: React.FC<FilterMetaProps> = ({ filters, onChangeTaskMetaFilter
<Popup tab={2} title="By Member"> <Popup tab={2} title="By Member">
<ActionsList> <ActionsList>
{members.current && {members.current &&
members.current.map(member => ( members.current.map((member) => (
<FilterMember <FilterMember
key={member.id} key={member.id}
member={member} member={member}
showName showName
onCardMemberClick={() => { onCardMemberClick={() => {
handleSetFilters( handleSetFilters(
produce(currentFilters, draftFilters => { produce(currentFilters, (draftFilters) => {
if (draftFilters.members.find(m => m.id === member.id)) { if (draftFilters.members.find((m) => m.id === member.id)) {
draftFilters.members = draftFilters.members.filter(m => m.id !== member.id); draftFilters.members = draftFilters.members.filter((m) => m.id !== member.id);
} else { } else {
draftFilters.members.push({ id: member.id, username: member.username ?? '' }); draftFilters.members.push({ id: member.id, username: member.username ?? '' });
} }

View File

@ -136,16 +136,16 @@ const ProjectActionWrapper = styled.div<{ disabled?: boolean }>`
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 15px; font-size: 15px;
color: ${props => props.theme.colors.text.primary}; color: ${(props) => props.theme.colors.text.primary};
&:not(:last-of-type) { &:not(:last-of-type) {
margin-right: 16px; margin-right: 16px;
} }
&:hover { &:hover {
color: ${props => props.theme.colors.text.secondary}; color: ${(props) => props.theme.colors.text.secondary};
} }
${props => ${(props) =>
props.disabled && props.disabled &&
css` css`
opacity: 0.5; opacity: 0.5;
@ -198,6 +198,7 @@ type ProjectBoardProps = {
}; };
export const BoardLoading = () => { export const BoardLoading = () => {
const { user } = useCurrentUser();
return ( return (
<> <>
<ProjectBar> <ProjectBar>
@ -215,6 +216,7 @@ export const BoardLoading = () => {
<ProjectActionText>Filter</ProjectActionText> <ProjectActionText>Filter</ProjectActionText>
</ProjectAction> </ProjectAction>
</ProjectActions> </ProjectActions>
{user && (
<ProjectActions> <ProjectActions>
<ProjectAction> <ProjectAction>
<Tags width={13} height={13} /> <Tags width={13} height={13} />
@ -229,6 +231,7 @@ export const BoardLoading = () => {
<ProjectActionText>Rules</ProjectActionText> <ProjectActionText>Rules</ProjectActionText>
</ProjectAction> </ProjectAction>
</ProjectActions> </ProjectActions>
)}
</ProjectBar> </ProjectBar>
<EmptyBoard /> <EmptyBoard />
</> </>
@ -277,8 +280,8 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
updateApolloCache<FindProjectQuery>( updateApolloCache<FindProjectQuery>(
client, client,
FindProjectDocument, FindProjectDocument,
cache => (cache) =>
produce(cache, draftCache => { produce(cache, (draftCache) => {
draftCache.findProject.taskGroups = draftCache.findProject.taskGroups.filter( draftCache.findProject.taskGroups = draftCache.findProject.taskGroups.filter(
(taskGroup: TaskGroup) => taskGroup.id !== deletedTaskGroupData.data?.deleteTaskGroup.taskGroup.id, (taskGroup: TaskGroup) => taskGroup.id !== deletedTaskGroupData.data?.deleteTaskGroup.taskGroup.id,
); );
@ -293,10 +296,10 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
updateApolloCache<FindProjectQuery>( updateApolloCache<FindProjectQuery>(
client, client,
FindProjectDocument, FindProjectDocument,
cache => (cache) =>
produce(cache, draftCache => { produce(cache, (draftCache) => {
const { taskGroups } = cache.findProject; const { taskGroups } = cache.findProject;
const idx = taskGroups.findIndex(taskGroup => taskGroup.id === newTaskData.data?.createTask.taskGroup.id); const idx = taskGroups.findIndex((taskGroup) => taskGroup.id === newTaskData.data?.createTask.taskGroup.id);
if (idx !== -1) { if (idx !== -1) {
if (newTaskData.data) { if (newTaskData.data) {
draftCache.findProject.taskGroups[idx].tasks.push({ ...newTaskData.data.createTask }); draftCache.findProject.taskGroups[idx].tasks.push({ ...newTaskData.data.createTask });
@ -313,8 +316,8 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
updateApolloCache<FindProjectQuery>( updateApolloCache<FindProjectQuery>(
client, client,
FindProjectDocument, FindProjectDocument,
cache => (cache) =>
produce(cache, draftCache => { produce(cache, (draftCache) => {
if (newTaskGroupData.data) { if (newTaskGroupData.data) {
draftCache.findProject.taskGroups.push({ ...newTaskGroupData.data.createTaskGroup, tasks: [] }); draftCache.findProject.taskGroups.push({ ...newTaskGroupData.data.createTaskGroup, tasks: [] });
} }
@ -333,10 +336,10 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
updateApolloCache<FindProjectQuery>( updateApolloCache<FindProjectQuery>(
client, client,
FindProjectDocument, FindProjectDocument,
cache => (cache) =>
produce(cache, draftCache => { produce(cache, (draftCache) => {
const idx = cache.findProject.taskGroups.findIndex( const idx = cache.findProject.taskGroups.findIndex(
t => t.id === resp.data?.deleteTaskGroupTasks.taskGroupID, (t) => t.id === resp.data?.deleteTaskGroupTasks.taskGroupID,
); );
if (idx !== -1) { if (idx !== -1) {
draftCache.findProject.taskGroups[idx].tasks = []; draftCache.findProject.taskGroups[idx].tasks = [];
@ -350,8 +353,8 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
updateApolloCache<FindProjectQuery>( updateApolloCache<FindProjectQuery>(
client, client,
FindProjectDocument, FindProjectDocument,
cache => (cache) =>
produce(cache, draftCache => { produce(cache, (draftCache) => {
if (resp.data) { if (resp.data) {
draftCache.findProject.taskGroups.push(resp.data.duplicateTaskGroup.taskGroup); draftCache.findProject.taskGroups.push(resp.data.duplicateTaskGroup.taskGroup);
} }
@ -368,8 +371,8 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
updateApolloCache<FindProjectQuery>( updateApolloCache<FindProjectQuery>(
client, client,
FindProjectDocument, FindProjectDocument,
cache => (cache) =>
produce(cache, draftCache => { produce(cache, (draftCache) => {
if (newTask.data) { if (newTask.data) {
const { previousTaskGroupID, task } = newTask.data.updateTaskLocation; const { previousTaskGroupID, task } = newTask.data.updateTaskLocation;
if (previousTaskGroupID !== task.taskGroup.id) { if (previousTaskGroupID !== task.taskGroup.id) {
@ -377,7 +380,9 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
const oldTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === previousTaskGroupID); const oldTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === previousTaskGroupID);
const newTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === task.taskGroup.id); const newTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === task.taskGroup.id);
if (oldTaskGroupIdx !== -1 && newTaskGroupIdx !== -1) { if (oldTaskGroupIdx !== -1 && newTaskGroupIdx !== -1) {
const previousTask = cache.findProject.taskGroups[oldTaskGroupIdx].tasks.find(t => t.id === task.id); const previousTask = cache.findProject.taskGroups[oldTaskGroupIdx].tasks.find(
(t) => t.id === task.id,
);
draftCache.findProject.taskGroups[oldTaskGroupIdx].tasks = taskGroups[oldTaskGroupIdx].tasks.filter( draftCache.findProject.taskGroups[oldTaskGroupIdx].tasks = taskGroups[oldTaskGroupIdx].tasks.filter(
(t: Task) => t.id !== task.id, (t: Task) => t.id !== task.id,
); );
@ -398,14 +403,14 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
const { user } = useCurrentUser(); const { user } = useCurrentUser();
const [deleteTask] = useDeleteTaskMutation(); const [deleteTask] = useDeleteTaskMutation();
const [toggleTaskLabel] = useToggleTaskLabelMutation({ const [toggleTaskLabel] = useToggleTaskLabelMutation({
onCompleted: newTaskLabel => { onCompleted: (newTaskLabel) => {
taskLabelsRef.current = newTaskLabel.toggleTaskLabel.task.labels; taskLabelsRef.current = newTaskLabel.toggleTaskLabel.task.labels;
}, },
}); });
const onCreateTask = (taskGroupID: string, name: string) => { const onCreateTask = (taskGroupID: string, name: string) => {
if (data) { if (data) {
const taskGroup = data.findProject.taskGroups.find(t => t.id === taskGroupID); const taskGroup = data.findProject.taskGroups.find((t) => t.id === taskGroupID);
if (taskGroup) { if (taskGroup) {
let position = 65535; let position = 65535;
if (taskGroup.tasks.length !== 0) { if (taskGroup.tasks.length !== 0) {
@ -469,12 +474,13 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
} }
return 'All Tasks'; return 'All Tasks';
}; };
if (data && user) {
if (data) {
labelsRef.current = data.findProject.labels; labelsRef.current = data.findProject.labels;
membersRef.current = data.findProject.members; membersRef.current = data.findProject.members;
const onQuickEditorOpen = ($target: React.RefObject<HTMLElement>, taskID: string, taskGroupID: string) => { const onQuickEditorOpen = ($target: React.RefObject<HTMLElement>, taskID: string, taskGroupID: string) => {
const taskGroup = data.findProject.taskGroups.find(t => t.id === taskGroupID); const taskGroup = data.findProject.taskGroups.find((t) => t.id === taskGroupID);
const currentTask = taskGroup ? taskGroup.tasks.find(t => t.id === taskID) : null; const currentTask = taskGroup ? taskGroup.tasks.find((t) => t.id === taskID) : null;
if (currentTask) { if (currentTask) {
setQuickCardEditor({ setQuickCardEditor({
target: $target, target: $target,
@ -486,9 +492,9 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
}; };
let currentQuickTask = null; let currentQuickTask = null;
if (quickCardEditor.taskID && quickCardEditor.taskGroupID) { if (quickCardEditor.taskID && quickCardEditor.taskGroupID) {
const targetGroup = data.findProject.taskGroups.find(t => t.id === quickCardEditor.taskGroupID); const targetGroup = data.findProject.taskGroups.find((t) => t.id === quickCardEditor.taskGroupID);
if (targetGroup) { if (targetGroup) {
currentQuickTask = targetGroup.tasks.find(t => t.id === quickCardEditor.taskID); currentQuickTask = targetGroup.tasks.find((t) => t.id === quickCardEditor.taskID);
} }
} }
return ( return (
@ -496,13 +502,13 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
<ProjectBar> <ProjectBar>
<ProjectActions> <ProjectActions>
<ProjectAction <ProjectAction
onClick={target => { onClick={(target) => {
showPopup( showPopup(
target, target,
<Popup tab={0} title={null}> <Popup tab={0} title={null}>
<FilterStatus <FilterStatus
filter={taskStatusFilter} filter={taskStatusFilter}
onChangeTaskStatusFilter={filter => { onChangeTaskStatusFilter={(filter) => {
setTaskStatusFilter(filter); setTaskStatusFilter(filter);
hidePopup(); hidePopup();
}} }}
@ -516,13 +522,13 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
<ProjectActionText>{getTaskStatusFilterLabel(taskStatusFilter)}</ProjectActionText> <ProjectActionText>{getTaskStatusFilterLabel(taskStatusFilter)}</ProjectActionText>
</ProjectAction> </ProjectAction>
<ProjectAction <ProjectAction
onClick={target => { onClick={(target) => {
showPopup( showPopup(
target, target,
<Popup tab={0} title={null}> <Popup tab={0} title={null}>
<SortPopup <SortPopup
sorting={taskSorting} sorting={taskSorting}
onChangeTaskSorting={sorting => { onChangeTaskSorting={(sorting) => {
setTaskSorting(sorting); setTaskSorting(sorting);
}} }}
/> />
@ -535,16 +541,16 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
<ProjectActionText>{renderTaskSortingLabel(taskSorting)}</ProjectActionText> <ProjectActionText>{renderTaskSortingLabel(taskSorting)}</ProjectActionText>
</ProjectAction> </ProjectAction>
<ProjectAction <ProjectAction
onClick={target => { onClick={(target) => {
showPopup( showPopup(
target, target,
<FilterMeta <FilterMeta
filters={taskMetaFilters} filters={taskMetaFilters}
onChangeTaskMetaFilter={filter => { onChangeTaskMetaFilter={(filter) => {
setTaskMetaFilters(filter); setTaskMetaFilters(filter);
}} }}
userID={user?.id} userID={user ?? ''}
labels={labelsRef} projectID={projectID}
members={membersRef} members={membersRef}
/>, />,
{ width: 200 }, { width: 200 },
@ -556,11 +562,11 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
</ProjectAction> </ProjectAction>
{renderMetaFilters(taskMetaFilters, (meta, id) => { {renderMetaFilters(taskMetaFilters, (meta, id) => {
setTaskMetaFilters( setTaskMetaFilters(
produce(taskMetaFilters, draftFilters => { produce(taskMetaFilters, (draftFilters) => {
if (meta === TaskMeta.MEMBER) { if (meta === TaskMeta.MEMBER) {
draftFilters.members = draftFilters.members.filter(m => m.id !== id); draftFilters.members = draftFilters.members.filter((m) => m.id !== id);
} else if (meta === TaskMeta.LABEL) { } else if (meta === TaskMeta.LABEL) {
draftFilters.labels = draftFilters.labels.filter(m => m.id !== id); draftFilters.labels = draftFilters.labels.filter((m) => m.id !== id);
} else if (meta === TaskMeta.TITLE) { } else if (meta === TaskMeta.TITLE) {
draftFilters.taskName = null; draftFilters.taskName = null;
} else if (meta === TaskMeta.DUE_DATE) { } else if (meta === TaskMeta.DUE_DATE) {
@ -570,17 +576,13 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
); );
})} })}
</ProjectActions> </ProjectActions>
{user && (
<ProjectActions> <ProjectActions>
<ProjectAction <ProjectAction
onClick={$labelsRef => { onClick={($labelsRef) => {
showPopup( showPopup(
$labelsRef, $labelsRef,
<LabelManagerEditor <LabelManagerEditor taskLabels={null} labelColors={data.labelColors} projectID={projectID ?? ''} />,
taskLabels={null}
labelColors={data.labelColors}
labels={labelsRef}
projectID={projectID ?? ''}
/>,
); );
}} }}
> >
@ -596,9 +598,11 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
<ProjectActionText>Rules</ProjectActionText> <ProjectActionText>Rules</ProjectActionText>
</ProjectAction> </ProjectAction>
</ProjectActions> </ProjectActions>
)}
</ProjectBar> </ProjectBar>
<SimpleLists <SimpleLists
onTaskClick={task => { isPublic={user === null}
onTaskClick={(task) => {
history.push(`${match.url}/c/${task.id}`); history.push(`${match.url}/c/${task.id}`);
}} }}
onCardLabelClick={onCardLabelClick ?? NOOP} onCardLabelClick={onCardLabelClick ?? NOOP}
@ -631,7 +635,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
}, },
}); });
}} }}
onTaskGroupDrop={droppedTaskGroup => { onTaskGroupDrop={(droppedTaskGroup) => {
updateTaskGroupLocation({ updateTaskGroupLocation({
variables: { taskGroupID: droppedTaskGroup.id, position: droppedTaskGroup.position }, variables: { taskGroupID: droppedTaskGroup.id, position: droppedTaskGroup.position },
optimisticResponse: { optimisticResponse: {
@ -651,7 +655,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
onCreateTask={onCreateTask} onCreateTask={onCreateTask}
onCreateTaskGroup={onCreateList} onCreateTaskGroup={onCreateList}
onCardMemberClick={($targetRef, _taskID, memberID) => { onCardMemberClick={($targetRef, _taskID, memberID) => {
const member = data.findProject.members.find(m => m.id === memberID); const member = data.findProject.members.find((m) => m.id === memberID);
if (member) { if (member) {
showPopup( showPopup(
$targetRef, $targetRef,
@ -678,8 +682,8 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
deleteTaskGroupTasks({ variables: { taskGroupID } }); deleteTaskGroupTasks({ variables: { taskGroupID } });
hidePopup(); hidePopup();
}} }}
onSortTaskGroup={taskSort => { onSortTaskGroup={(taskSort) => {
const taskGroup = data.findProject.taskGroups.find(t => t.id === taskGroupID); const taskGroup = data.findProject.taskGroups.find((t) => t.id === taskGroupID);
if (taskGroup) { if (taskGroup) {
const tasks: Array<{ taskID: string; position: number }> = taskGroup.tasks const tasks: Array<{ taskID: string; position: number }> = taskGroup.tasks
.sort((a, b) => sortTasks(a, b, taskSort)) .sort((a, b) => sortTasks(a, b, taskSort))
@ -691,8 +695,8 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
hidePopup(); hidePopup();
} }
}} }}
onDuplicateTaskGroup={newName => { onDuplicateTaskGroup={(newName) => {
const idx = data.findProject.taskGroups.findIndex(t => t.id === taskGroupID); const idx = data.findProject.taskGroups.findIndex((t) => t.id === taskGroupID);
if (idx !== -1) { if (idx !== -1) {
const taskGroups = data.findProject.taskGroups.sort((a, b) => a.position - b.position); const taskGroups = data.findProject.taskGroups.sort((a, b) => a.position - b.position);
const prevPos = taskGroups[idx].position; const prevPos = taskGroups[idx].position;
@ -705,7 +709,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
hidePopup(); hidePopup();
} }
}} }}
onArchiveTaskGroup={tgID => { onArchiveTaskGroup={(tgID) => {
deleteTaskGroup({ variables: { taskGroupID: tgID } }); deleteTaskGroup({ variables: { taskGroupID: tgID } });
hidePopup(); hidePopup();
}} }}
@ -739,7 +743,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
); );
}} }}
onCardMemberClick={($targetRef, _taskID, memberID) => { onCardMemberClick={($targetRef, _taskID, memberID) => {
const member = data.findProject.members.find(m => m.id === memberID); const member = data.findProject.members.find((m) => m.id === memberID);
if (member) { if (member) {
showPopup( showPopup(
$targetRef, $targetRef,
@ -758,11 +762,11 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
showPopup( showPopup(
$targetRef, $targetRef,
<LabelManagerEditor <LabelManagerEditor
onLabelToggle={labelID => { onLabelToggle={(labelID) => {
toggleTaskLabel({ variables: { taskID: task.id, projectLabelID: labelID } }); toggleTaskLabel({ variables: { taskID: task.id, projectLabelID: labelID } });
}} }}
taskID={task.id}
labelColors={data.labelColors} labelColors={data.labelColors}
labels={labelsRef}
taskLabels={taskLabelsRef} taskLabels={taskLabelsRef}
projectID={projectID ?? ''} projectID={projectID ?? ''}
/>, />,
@ -771,15 +775,15 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
onArchiveCard={(_listId: string, cardId: string) => { onArchiveCard={(_listId: string, cardId: string) => {
return deleteTask({ return deleteTask({
variables: { taskID: cardId }, variables: { taskID: cardId },
update: client => { update: (client) => {
updateApolloCache<FindProjectQuery>( updateApolloCache<FindProjectQuery>(
client, client,
FindProjectDocument, FindProjectDocument,
cache => (cache) =>
produce(cache, draftCache => { produce(cache, (draftCache) => {
draftCache.findProject.taskGroups = cache.findProject.taskGroups.map(taskGroup => ({ draftCache.findProject.taskGroups = cache.findProject.taskGroups.map((taskGroup) => ({
...taskGroup, ...taskGroup,
tasks: taskGroup.tasks.filter(t => t.id !== cardId), tasks: taskGroup.tasks.filter((t) => t.id !== cardId),
})); }));
}), }),
{ projectID }, { projectID },
@ -793,7 +797,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
<Popup title="Change Due Date" tab={0} onClose={() => hidePopup()}> <Popup title="Change Due Date" tab={0} onClose={() => hidePopup()}>
<DueDateManager <DueDateManager
task={task} task={task}
onRemoveDueDate={t => { onRemoveDueDate={(t) => {
updateTaskDueDate({ variables: { taskID: t.id, dueDate: null, hasTime: false } }); updateTaskDueDate({ variables: { taskID: t.id, dueDate: null, hasTime: false } });
// hidePopup(); // hidePopup();
}} }}
@ -806,7 +810,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
</Popup>, </Popup>,
); );
}} }}
onToggleComplete={task => { onToggleComplete={(task) => {
setTaskComplete({ variables: { taskID: task.id, complete: !task.complete } }); setTaskComplete({ variables: { taskID: task.id, complete: !task.complete } });
}} }}
target={quickCardEditor.target} target={quickCardEditor.target}

View File

@ -4,7 +4,7 @@ import TaskDetails from 'shared/components/TaskDetails';
import TaskDetailsLoading from 'shared/components/TaskDetails/Loading'; import TaskDetailsLoading from 'shared/components/TaskDetails/Loading';
import { Popup, usePopup } from 'shared/components/PopupMenu'; import { Popup, usePopup } from 'shared/components/PopupMenu';
import MemberManager from 'shared/components/MemberManager'; import MemberManager from 'shared/components/MemberManager';
import { useRouteMatch, useHistory } from 'react-router'; import { useRouteMatch, useHistory, useParams } from 'react-router';
import { import {
useDeleteTaskChecklistMutation, useDeleteTaskChecklistMutation,
useUpdateTaskChecklistNameMutation, useUpdateTaskChecklistNameMutation,
@ -36,6 +36,7 @@ import Input from 'shared/components/Input';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import updateApolloCache from 'shared/utils/cache'; import updateApolloCache from 'shared/utils/cache';
import NOOP from 'shared/utils/noop'; import NOOP from 'shared/utils/noop';
import polling from 'shared/utils/polling';
export const ActionsList = styled.ul` export const ActionsList = styled.ul`
margin: 0; margin: 0;
@ -55,7 +56,7 @@ export const ActionItem = styled.li`
align-items: center; align-items: center;
font-size: 14px; font-size: 14px;
&:hover { &:hover {
background: ${props => props.theme.colors.primary}; background: ${(props) => props.theme.colors.primary};
} }
`; `;
@ -165,10 +166,8 @@ const CreateChecklistPopup: React.FC<CreateChecklistPopupProps> = ({ onCreateChe
defaultValue="Checklist" defaultValue="Checklist"
width="100%" width="100%"
label="Name" label="Name"
id="name"
name="name"
variant="alternate" variant="alternate"
ref={register({ required: 'Checklist name is required' })} {...register('name', { required: 'Checklist name is required' })}
/> />
<CreateChecklistButton type="submit">Create</CreateChecklistButton> <CreateChecklistButton type="submit">Create</CreateChecklistButton>
</CreateChecklistForm> </CreateChecklistForm>
@ -176,7 +175,6 @@ const CreateChecklistPopup: React.FC<CreateChecklistPopupProps> = ({ onCreateChe
}; };
type DetailsProps = { type DetailsProps = {
taskID: string;
projectURL: string; projectURL: string;
onTaskNameChange: (task: Task, newName: string) => void; onTaskNameChange: (task: Task, newName: string) => void;
onTaskDescriptionChange: (task: Task, newDescription: string) => void; onTaskDescriptionChange: (task: Task, newDescription: string) => void;
@ -190,7 +188,6 @@ const initialMemberPopupState = { taskID: '', isOpen: false, top: 0, left: 0 };
const Details: React.FC<DetailsProps> = ({ const Details: React.FC<DetailsProps> = ({
projectURL, projectURL,
taskID,
onTaskNameChange, onTaskNameChange,
onTaskDescriptionChange, onTaskDescriptionChange,
onDeleteTask, onDeleteTask,
@ -199,6 +196,7 @@ const Details: React.FC<DetailsProps> = ({
refreshCache, refreshCache,
}) => { }) => {
const { user } = useCurrentUser(); const { user } = useCurrentUser();
const { taskID } = useParams<{ taskID: string }>();
const { showPopup, hidePopup } = usePopup(); const { showPopup, hidePopup } = usePopup();
const history = useHistory(); const history = useHistory();
const [deleteTaskComment] = useDeleteTaskCommentMutation({ const [deleteTaskComment] = useDeleteTaskCommentMutation({
@ -206,11 +204,11 @@ const Details: React.FC<DetailsProps> = ({
updateApolloCache<FindTaskQuery>( updateApolloCache<FindTaskQuery>(
client, client,
FindTaskDocument, FindTaskDocument,
cache => (cache) =>
produce(cache, draftCache => { produce(cache, (draftCache) => {
if (response.data) { if (response.data) {
draftCache.findTask.comments = cache.findTask.comments.filter( draftCache.findTask.comments = cache.findTask.comments.filter(
c => c.id !== response.data?.deleteTaskComment.commentID, (c) => c.id !== response.data?.deleteTaskComment.commentID,
); );
} }
}), }),
@ -223,8 +221,8 @@ const Details: React.FC<DetailsProps> = ({
updateApolloCache<FindTaskQuery>( updateApolloCache<FindTaskQuery>(
client, client,
FindTaskDocument, FindTaskDocument,
cache => (cache) =>
produce(cache, draftCache => { produce(cache, (draftCache) => {
if (response.data) { if (response.data) {
draftCache.findTask.comments.push({ draftCache.findTask.comments.push({
...response.data.createTaskComment.comment, ...response.data.createTaskComment.comment,
@ -241,18 +239,18 @@ const Details: React.FC<DetailsProps> = ({
updateApolloCache<FindTaskQuery>( updateApolloCache<FindTaskQuery>(
client, client,
FindTaskDocument, FindTaskDocument,
cache => (cache) =>
produce(cache, draftCache => { produce(cache, (draftCache) => {
if (response.data) { if (response.data) {
const { prevChecklistID, taskChecklistID, checklistItem } = response.data.updateTaskChecklistItemLocation; const { prevChecklistID, taskChecklistID, checklistItem } = response.data.updateTaskChecklistItemLocation;
if (taskChecklistID !== prevChecklistID) { if (taskChecklistID !== prevChecklistID) {
const oldIdx = cache.findTask.checklists.findIndex(c => c.id === prevChecklistID); const oldIdx = cache.findTask.checklists.findIndex((c) => c.id === prevChecklistID);
const newIdx = cache.findTask.checklists.findIndex(c => c.id === taskChecklistID); const newIdx = cache.findTask.checklists.findIndex((c) => c.id === taskChecklistID);
if (oldIdx > -1 && newIdx > -1) { if (oldIdx > -1 && newIdx > -1) {
const item = cache.findTask.checklists[oldIdx].items.find(i => i.id === checklistItem.id); const item = cache.findTask.checklists[oldIdx].items.find((i) => i.id === checklistItem.id);
if (item) { if (item) {
draftCache.findTask.checklists[oldIdx].items = cache.findTask.checklists[oldIdx].items.filter( draftCache.findTask.checklists[oldIdx].items = cache.findTask.checklists[oldIdx].items.filter(
i => i.id !== checklistItem.id, (i) => i.id !== checklistItem.id,
); );
draftCache.findTask.checklists[newIdx].items.push({ draftCache.findTask.checklists[newIdx].items.push({
...item, ...item,
@ -269,12 +267,12 @@ const Details: React.FC<DetailsProps> = ({
}, },
}); });
const [setTaskChecklistItemComplete] = useSetTaskChecklistItemCompleteMutation({ const [setTaskChecklistItemComplete] = useSetTaskChecklistItemCompleteMutation({
update: client => { update: (client) => {
updateApolloCache<FindTaskQuery>( updateApolloCache<FindTaskQuery>(
client, client,
FindTaskDocument, FindTaskDocument,
cache => (cache) =>
produce(cache, draftCache => { produce(cache, (draftCache) => {
const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists); const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists);
draftCache.findTask.badges.checklist = { draftCache.findTask.badges.checklist = {
__typename: 'ChecklistBadge', __typename: 'ChecklistBadge',
@ -291,11 +289,11 @@ const Details: React.FC<DetailsProps> = ({
updateApolloCache<FindTaskQuery>( updateApolloCache<FindTaskQuery>(
client, client,
FindTaskDocument, FindTaskDocument,
cache => (cache) =>
produce(cache, draftCache => { produce(cache, (draftCache) => {
const { checklists } = cache.findTask; const { checklists } = cache.findTask;
draftCache.findTask.checklists = checklists.filter( draftCache.findTask.checklists = checklists.filter(
c => c.id !== deleteData.data?.deleteTaskChecklist.taskChecklist.id, (c) => c.id !== deleteData.data?.deleteTaskChecklist.taskChecklist.id,
); );
const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists); const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists);
draftCache.findTask.badges.checklist = { draftCache.findTask.badges.checklist = {
@ -317,8 +315,8 @@ const Details: React.FC<DetailsProps> = ({
updateApolloCache<FindTaskQuery>( updateApolloCache<FindTaskQuery>(
client, client,
FindTaskDocument, FindTaskDocument,
cache => (cache) =>
produce(cache, draftCache => { produce(cache, (draftCache) => {
if (createData.data) { if (createData.data) {
const item = createData.data.createTaskChecklist; const item = createData.data.createTaskChecklist;
draftCache.findTask.checklists.push({ ...item }); draftCache.findTask.checklists.push({ ...item });
@ -334,14 +332,14 @@ const Details: React.FC<DetailsProps> = ({
updateApolloCache<FindTaskQuery>( updateApolloCache<FindTaskQuery>(
client, client,
FindTaskDocument, FindTaskDocument,
cache => (cache) =>
produce(cache, draftCache => { produce(cache, (draftCache) => {
if (deleteData.data) { if (deleteData.data) {
const item = deleteData.data.deleteTaskChecklistItem.taskChecklistItem; const item = deleteData.data.deleteTaskChecklistItem.taskChecklistItem;
const targetIdx = cache.findTask.checklists.findIndex(c => c.id === item.taskChecklistID); const targetIdx = cache.findTask.checklists.findIndex((c) => c.id === item.taskChecklistID);
if (targetIdx > -1) { if (targetIdx > -1) {
draftCache.findTask.checklists[targetIdx].items = cache.findTask.checklists[targetIdx].items.filter( draftCache.findTask.checklists[targetIdx].items = cache.findTask.checklists[targetIdx].items.filter(
c => item.id !== c.id, (c) => item.id !== c.id,
); );
} }
const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists); const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists);
@ -361,12 +359,12 @@ const Details: React.FC<DetailsProps> = ({
updateApolloCache<FindTaskQuery>( updateApolloCache<FindTaskQuery>(
client, client,
FindTaskDocument, FindTaskDocument,
cache => (cache) =>
produce(cache, draftCache => { produce(cache, (draftCache) => {
if (newTaskItem.data) { if (newTaskItem.data) {
const item = newTaskItem.data.createTaskChecklistItem; const item = newTaskItem.data.createTaskChecklistItem;
const { checklists } = cache.findTask; const { checklists } = cache.findTask;
const idx = checklists.findIndex(c => c.id === item.taskChecklistID); const idx = checklists.findIndex((c) => c.id === item.taskChecklistID);
if (idx !== -1) { if (idx !== -1) {
draftCache.findTask.checklists[idx].items.push({ ...item }); draftCache.findTask.checklists[idx].items.push({ ...item });
const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists); const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists);
@ -384,7 +382,7 @@ const Details: React.FC<DetailsProps> = ({
}); });
const { loading, data, refetch } = useFindTaskQuery({ const { loading, data, refetch } = useFindTaskQuery({
variables: { taskID }, variables: { taskID },
pollInterval: 3000, pollInterval: polling.TASK_DETAILS,
fetchPolicy: 'cache-and-network', fetchPolicy: 'cache-and-network',
}); });
const [setTaskComplete] = useSetTaskCompleteMutation(); const [setTaskComplete] = useSetTaskCompleteMutation();
@ -415,6 +413,7 @@ const Details: React.FC<DetailsProps> = ({
width={1070} width={1070}
onClose={() => { onClose={() => {
history.push(projectURL); history.push(projectURL);
hidePopup();
}} }}
renderContent={() => { renderContent={() => {
return data ? ( return data ? (
@ -424,7 +423,7 @@ const Details: React.FC<DetailsProps> = ({
updateTaskComment({ variables: { commentID, message } }); updateTaskComment({ variables: { commentID, message } });
}} }}
editableComment={editableComment} editableComment={editableComment}
me={data.me.user} me={data.me ? data.me.user : null}
onCommentShowActions={(commentID, $targetRef) => { onCommentShowActions={(commentID, $targetRef) => {
showPopup( showPopup(
$targetRef, $targetRef,
@ -444,7 +443,7 @@ const Details: React.FC<DetailsProps> = ({
onCreateComment={(task, message) => { onCreateComment={(task, message) => {
createTaskComment({ variables: { taskID: task.id, message } }); createTaskComment({ variables: { taskID: task.id, message } });
}} }}
onChecklistDrop={checklist => { onChecklistDrop={(checklist) => {
updateTaskChecklistLocation({ updateTaskChecklistLocation({
variables: { taskChecklistID: checklist.id, position: checklist.position }, variables: { taskChecklistID: checklist.id, position: checklist.position },
@ -486,7 +485,7 @@ const Details: React.FC<DetailsProps> = ({
}} }}
onTaskNameChange={onTaskNameChange} onTaskNameChange={onTaskNameChange}
onTaskDescriptionChange={onTaskDescriptionChange} onTaskDescriptionChange={onTaskDescriptionChange}
onToggleTaskComplete={task => { onToggleTaskComplete={(task) => {
setTaskComplete({ variables: { taskID: task.id, complete: !task.complete } }); setTaskComplete({ variables: { taskID: task.id, complete: !task.complete } });
}} }}
onDeleteTask={onDeleteTask} onDeleteTask={onDeleteTask}
@ -531,7 +530,7 @@ const Details: React.FC<DetailsProps> = ({
createTaskChecklistItem({ variables: { taskChecklistID, name, position } }); createTaskChecklistItem({ variables: { taskChecklistID, name, position } });
}} }}
onMemberProfile={($targetRef, memberID) => { onMemberProfile={($targetRef, memberID) => {
const member = data.findTask.assigned.find(m => m.id === memberID); const member = data.findTask.assigned.find((m) => m.id === memberID);
if (member) { if (member) {
showPopup( showPopup(
$targetRef, $targetRef,
@ -541,7 +540,7 @@ const Details: React.FC<DetailsProps> = ({
bio="None" bio="None"
onRemoveFromTask={() => { onRemoveFromTask={() => {
if (user) { if (user) {
unassignTask({ variables: { taskID: data.findTask.id, userID: user.id } }); unassignTask({ variables: { taskID: data.findTask.id, userID: user ?? '' } });
} }
}} }}
/> />
@ -581,7 +580,7 @@ const Details: React.FC<DetailsProps> = ({
}} }}
> >
<CreateChecklistPopup <CreateChecklistPopup
onCreateChecklist={checklistData => { onCreateChecklist={(checklistData) => {
let position = 65535; let position = 65535;
if (data.findTask.checklists) { if (data.findTask.checklists) {
const [lastChecklist] = data.findTask.checklists.slice(-1); const [lastChecklist] = data.findTask.checklists.slice(-1);
@ -631,7 +630,7 @@ const Details: React.FC<DetailsProps> = ({
> >
<DueDateManager <DueDateManager
task={task} task={task}
onRemoveDueDate={t => { onRemoveDueDate={(t) => {
updateTaskDueDate({ variables: { taskID: t.id, dueDate: null, hasTime: false } }); updateTaskDueDate({ variables: { taskID: t.id, dueDate: null, hasTime: false } });
// hidePopup(); // hidePopup();
}} }}

View File

@ -8,12 +8,14 @@ import {
FindProjectDocument, FindProjectDocument,
useCreateProjectLabelMutation, useCreateProjectLabelMutation,
FindProjectQuery, FindProjectQuery,
useToggleTaskLabelMutation,
useLabelsQuery,
} from 'shared/generated/graphql'; } from 'shared/generated/graphql';
import LabelManager from 'shared/components/PopupMenu/LabelManager'; import LabelManager from 'shared/components/PopupMenu/LabelManager';
import LabelEditor from 'shared/components/PopupMenu/LabelEditor'; import LabelEditor from 'shared/components/PopupMenu/LabelEditor';
type LabelManagerEditorProps = { type LabelManagerEditorProps = {
labels: React.RefObject<Array<ProjectLabel>>; taskID?: string;
taskLabels: null | React.RefObject<Array<TaskLabel>>; taskLabels: null | React.RefObject<Array<TaskLabel>>;
projectID: string; projectID: string;
labelColors: Array<LabelColor>; labelColors: Array<LabelColor>;
@ -21,7 +23,7 @@ type LabelManagerEditorProps = {
}; };
const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
labels: labelsRef, taskID,
projectID, projectID,
labelColors, labelColors,
onLabelToggle, onLabelToggle,
@ -29,13 +31,19 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
}) => { }) => {
const [currentLabel, setCurrentLabel] = useState(''); const [currentLabel, setCurrentLabel] = useState('');
const { setTab, hidePopup } = usePopup(); const { setTab, hidePopup } = usePopup();
const [toggleTaskLabel] = useToggleTaskLabelMutation();
const [createProjectLabel] = useCreateProjectLabelMutation({ const [createProjectLabel] = useCreateProjectLabelMutation({
onCompleted: (data) => {
if (taskID) {
toggleTaskLabel({ variables: { taskID, projectLabelID: data.createProjectLabel.id } });
}
},
update: (client, newLabelData) => { update: (client, newLabelData) => {
updateApolloCache<FindProjectQuery>( updateApolloCache<FindProjectQuery>(
client, client,
FindProjectDocument, FindProjectDocument,
cache => (cache) =>
produce(cache, draftCache => { produce(cache, (draftCache) => {
if (newLabelData.data) { if (newLabelData.data) {
draftCache.findProject.labels.push({ ...newLabelData.data.createProjectLabel }); draftCache.findProject.labels.push({ ...newLabelData.data.createProjectLabel });
} }
@ -52,38 +60,39 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
updateApolloCache<FindProjectQuery>( updateApolloCache<FindProjectQuery>(
client, client,
FindProjectDocument, FindProjectDocument,
cache => (cache) =>
produce(cache, draftCache => { produce(cache, (draftCache) => {
draftCache.findProject.labels = cache.findProject.labels.filter( draftCache.findProject.labels = cache.findProject.labels.filter(
label => label.id !== newLabelData.data?.deleteProjectLabel.id, (label) => label.id !== newLabelData.data?.deleteProjectLabel.id,
); );
}), }),
{ projectID }, { projectID },
); );
}, },
}); });
const labels = labelsRef.current ? labelsRef.current : []; const { data } = useLabelsQuery({ variables: { projectID } });
const labels = data ? data.findProject.labels : [];
const taskLabels = taskLabelsRef && taskLabelsRef.current ? taskLabelsRef.current : []; const taskLabels = taskLabelsRef && taskLabelsRef.current ? taskLabelsRef.current : [];
const [currentTaskLabels, setCurrentTaskLabels] = useState(taskLabels); const [currentTaskLabels, setCurrentTaskLabels] = useState(taskLabels);
return ( return (
<> <>
<Popup title="Labels" tab={0} onClose={() => hidePopup()}> <Popup title="Labels" tab={0} onClose={() => hidePopup()}>
<LabelManager <LabelManager
labels={labels} labels={data ? data.findProject.labels : []}
taskLabels={currentTaskLabels} taskLabels={currentTaskLabels}
onLabelCreate={() => { onLabelCreate={() => {
setTab(2); setTab(2);
}} }}
onLabelEdit={labelId => { onLabelEdit={(labelId) => {
setCurrentLabel(labelId); setCurrentLabel(labelId);
setTab(1); setTab(1);
}} }}
onLabelToggle={labelId => { onLabelToggle={(labelId) => {
if (onLabelToggle) { if (onLabelToggle) {
if (currentTaskLabels.find(t => t.projectLabel.id === labelId)) { if (currentTaskLabels.find((t) => t.projectLabel.id === labelId)) {
setCurrentTaskLabels(currentTaskLabels.filter(t => t.projectLabel.id !== labelId)); setCurrentTaskLabels(currentTaskLabels.filter((t) => t.projectLabel.id !== labelId));
} else { } else if (data) {
const newProjectLabel = labels.find(l => l.id === labelId); const newProjectLabel = data.findProject.labels.find((l) => l.id === labelId);
if (newProjectLabel) { if (newProjectLabel) {
setCurrentTaskLabels([ setCurrentTaskLabels([
...currentTaskLabels, ...currentTaskLabels,
@ -103,14 +112,14 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
<Popup onClose={() => hidePopup()} title="Edit label" tab={1}> <Popup onClose={() => hidePopup()} title="Edit label" tab={1}>
<LabelEditor <LabelEditor
labelColors={labelColors} labelColors={labelColors}
label={labels.find(label => label.id === currentLabel) ?? null} label={labels.find((label) => label.id === currentLabel) ?? null}
onLabelEdit={(projectLabelID, name, color) => { onLabelEdit={(projectLabelID, name, color) => {
if (projectLabelID) { if (projectLabelID) {
updateProjectLabel({ variables: { projectLabelID, labelColorID: color.id, name: name ?? '' } }); updateProjectLabel({ variables: { projectLabelID, labelColorID: color.id, name: name ?? '' } });
} }
setTab(0); setTab(0);
}} }}
onLabelDelete={labelID => { onLabelDelete={(labelID) => {
deleteProjectLabel({ variables: { projectLabelID: labelID } }); deleteProjectLabel({ variables: { projectLabelID: labelID } });
setTab(0); setTab(0);
}} }}

View File

@ -0,0 +1,16 @@
import React from 'react';
import { Cross } from 'shared/icons';
import * as S from './Styles';
const OptionValue = ({ data, removeProps }: any) => {
return (
<S.OptionValueWrapper>
<S.OptionValueLabel>{data.label}</S.OptionValueLabel>
<S.OptionValueRemove {...removeProps}>
<Cross width={14} height={14} />
</S.OptionValueRemove>
</S.OptionValueWrapper>
);
};
export default OptionValue;

View File

@ -0,0 +1,64 @@
import styled from 'styled-components';
import Button from 'shared/components/Button';
export const OptionWrapper = styled.div<{ isFocused: boolean }>`
cursor: pointer;
padding: 4px 8px;
${props => props.isFocused && `background: rgba(${props.theme.colors.primary});`}
display: flex;
align-items: center;
`;
export const OptionContent = styled.div`
display: flex;
flex-direction: column;
margin-left: 12px;
`;
export const OptionLabel = styled.span<{ fontSize: number; quiet: boolean }>`
display: flex;
align-items: center;
font-size: ${p => p.fontSize}px;
color: rgba(${p => (p.quiet ? p.theme.colors.text.primary : p.theme.colors.text.primary)});
`;
export const OptionValueWrapper = styled.div`
background: rgba(${props => props.theme.colors.bg.primary});
border-radius: 4px;
margin: 2px;
padding: 3px 6px 3px 4px;
display: flex;
align-items: center;
`;
export const OptionValueLabel = styled.span`
font-size: 12px;
color: rgba(${props => props.theme.colors.text.secondary});
`;
export const OptionValueRemove = styled.button`
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
outline: none;
padding: 0;
margin: 0;
margin-left: 4px;
`;
export const InviteButton = styled(Button)`
margin-top: 12px;
height: 32px;
padding: 4px 12px;
width: 100%;
justify-content: center;
`;
export const InviteContainer = styled.div`
min-height: 300px;
display: flex;
flex-direction: column;
`;

View File

@ -0,0 +1,39 @@
import React from 'react';
import TaskAssignee from 'shared/components/TaskAssignee';
import * as S from './Styles';
type UserOptionProps = {
innerProps: any;
isDisabled: boolean;
isFocused: boolean;
label: string;
data: any;
getValue: any;
};
const UserOption: React.FC<UserOptionProps> = ({ isDisabled, isFocused, innerProps, label, data }) => {
return !isDisabled ? (
<S.OptionWrapper {...innerProps} isFocused={isFocused}>
<TaskAssignee
size={32}
member={{
id: '',
fullName: data.value.label,
profileIcon: data.value.profileIcon,
}}
/>
<S.OptionContent>
<S.OptionLabel fontSize={16} quiet={false}>
{label}
</S.OptionLabel>
{data.value.type === 2 && (
<S.OptionLabel fontSize={14} quiet>
Joined
</S.OptionLabel>
)}
</S.OptionContent>
</S.OptionWrapper>
) : null;
};
export default UserOption;

View File

@ -0,0 +1,82 @@
import gql from 'graphql-tag';
import isValidEmail from 'shared/utils/email';
type MemberFilterOptions = {
projectID?: null | string;
teamID?: null | string;
organization?: boolean;
};
export default async function(client: any, projectID: string, options: MemberFilterOptions, input: string, cb: any) {
if (input && input.trim().length < 3) {
return [];
}
const res = await client.query({
query: gql`
query {
searchMembers(input: {searchFilter:"${input}", projectID:"${projectID}"}) {
id
similarity
status
user {
id
fullName
email
profileIcon {
url
initials
bgColor
}
}
}
}
`,
});
let results: any = [];
const emails: Array<string> = [];
if (res.data && res.data.searchMembers) {
results = [
...res.data.searchMembers.map((m: any) => {
if (m.status === 'INVITED') {
return {
label: m.id,
value: {
id: m.id,
type: 2,
profileIcon: {
bgColor: '#ccc',
initials: m.id.charAt(0),
},
},
};
}
emails.push(m.user.email);
return {
label: m.user.fullName,
value: { id: m.user.id, type: 0, profileIcon: m.user.profileIcon },
};
}),
];
}
if (isValidEmail(input) && !emails.find(e => e === input)) {
results = [
...results,
{
label: input,
value: {
id: input,
type: 1,
profileIcon: {
bgColor: '#ccc',
initials: input.charAt(0),
},
},
},
];
}
return results;
}

View File

@ -0,0 +1,82 @@
import React, { useState } from 'react';
import AsyncSelect from 'react-select/async';
import { useApolloClient } from '@apollo/react-hooks';
import { colourStyles } from 'shared/components/Select';
import { Popup } from 'shared/components/PopupMenu';
import OptionValue from './OptionValue';
import UserOption from './UserOption';
import fetchMembers from './fetchMembers';
import * as S from './Styles';
type InviteUserData = {
email?: string;
userID?: string;
};
type UserManagementPopupProps = {
projectID: string;
users: Array<User>;
projectMembers: Array<TaskUser>;
onInviteProjectMembers: (data: Array<InviteUserData>) => void;
};
const UserManagementPopup: React.FC<UserManagementPopupProps> = ({
projectID,
users,
projectMembers,
onInviteProjectMembers,
}) => {
const client = useApolloClient();
const [invitedUsers, setInvitedUsers] = useState<Array<any> | null>(null);
return (
<Popup tab={0} title="Invite a user">
<S.InviteContainer>
<AsyncSelect
getOptionValue={option => option.value.id}
placeholder="Email address or username"
noOptionsMessage={() => null}
onChange={(e: any) => {
setInvitedUsers(e);
}}
isMulti
autoFocus
cacheOptions
styles={colourStyles}
defaultOption
components={{
MultiValue: OptionValue,
Option: UserOption,
IndicatorSeparator: null,
DropdownIndicator: null,
}}
loadOptions={(i, cb) => fetchMembers(client, projectID, {}, i, cb)}
/>
</S.InviteContainer>
<S.InviteButton
onClick={() => {
if (invitedUsers) {
onInviteProjectMembers(
invitedUsers.map(user => {
if (user.value.type === 0) {
return {
userID: user.value.id,
};
}
return {
email: user.value.id,
};
}),
);
}
}}
disabled={invitedUsers === null}
hoverVariant="none"
fontSize="16px"
>
Send Invite
</S.InviteButton>
</Popup>
);
};
export default UserManagementPopup;

View File

@ -1,10 +1,9 @@
// LOC830 // LOC830
import React, { useState, useRef, useEffect, useContext } from 'react'; import React, { useRef, useEffect } from 'react';
import updateApolloCache from 'shared/utils/cache'; import updateApolloCache from 'shared/utils/cache';
import GlobalTopNavbar, { ProjectPopup } from 'App/TopNavbar'; import GlobalTopNavbar from 'App/TopNavbar';
import styled from 'styled-components/macro'; import ProjectPopup from 'App/TopNavbar/ProjectPopup';
import AsyncSelect from 'react-select/async'; import { usePopup } from 'shared/components/PopupMenu';
import { usePopup, Popup } from 'shared/components/PopupMenu';
import { import {
useParams, useParams,
Route, Route,
@ -23,408 +22,68 @@ import {
useFindProjectQuery, useFindProjectQuery,
useDeleteInvitedProjectMemberMutation, useDeleteInvitedProjectMemberMutation,
useUpdateTaskNameMutation, useUpdateTaskNameMutation,
useCreateTaskMutation,
useDeleteTaskMutation, useDeleteTaskMutation,
useUpdateTaskLocationMutation,
useUpdateTaskGroupLocationMutation,
useCreateTaskGroupMutation,
useUpdateTaskDescriptionMutation, useUpdateTaskDescriptionMutation,
FindProjectDocument, FindProjectDocument,
FindProjectQuery, FindProjectQuery,
} from 'shared/generated/graphql'; } from 'shared/generated/graphql';
import produce from 'immer'; import produce from 'immer';
import UserContext, { useCurrentUser } from 'App/context';
import Input from 'shared/components/Input';
import Member from 'shared/components/Member';
import EmptyBoard from 'shared/components/EmptyBoard';
import NOOP from 'shared/utils/noop'; import NOOP from 'shared/utils/noop';
import { Lock, Cross } from 'shared/icons'; import useStateWithLocalStorage from 'shared/hooks/useStateWithLocalStorage';
import Button from 'shared/components/Button'; import localStorage from 'shared/utils/localStorage';
import { useApolloClient } from '@apollo/react-hooks'; import polling from 'shared/utils/polling';
import TaskAssignee from 'shared/components/TaskAssignee';
import gql from 'graphql-tag';
import { colourStyles } from 'shared/components/Select';
import Board, { BoardLoading } from './Board'; import Board, { BoardLoading } from './Board';
import Details from './Details'; import Details from './Details';
import LabelManagerEditor from './LabelManagerEditor'; import LabelManagerEditor from './LabelManagerEditor';
import { mixin } from '../../shared/utils/styles'; import UserManagementPopup from './UserManagementPopup';
const CARD_LABEL_VARIANT_STORAGE_KEY = 'card_label_variant';
const RFC2822_EMAIL = /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/;
const useStateWithLocalStorage = (localStorageKey: string): [string, React.Dispatch<React.SetStateAction<string>>] => {
const [value, setValue] = React.useState<string>(localStorage.getItem(localStorageKey) || '');
React.useEffect(() => {
localStorage.setItem(localStorageKey, value);
}, [value]);
return [value, setValue];
};
const SearchInput = styled(Input)`
margin: 0;
`;
const UserMember = styled(Member)`
padding: 4px 0;
cursor: pointer;
&:hover {
background: ${props => mixin.rgba(props.theme.colors.bg.primary, 0.4)};
}
border-radius: 6px;
`;
const MemberList = styled.div`
margin: 8px 0;
`;
type InviteUserData = {
email?: string;
suerID?: string;
};
type UserManagementPopupProps = {
projectID: string;
users: Array<User>;
projectMembers: Array<TaskUser>;
onInviteProjectMembers: (data: Array<InviteUserData>) => void;
};
const VisibiltyPrivateIcon = styled(Lock)`
padding-right: 4px;
`;
const VisibiltyButtonText = styled.span`
color: rgba(${props => props.theme.colors.text.primary});
`;
const ShareActions = styled.div`
border-top: 1px solid #414561;
margin-top: 8px;
padding-top: 8px;
display: flex;
align-items: center;
justify-content: space-between;
`;
const VisibiltyButton = styled.button`
cursor: pointer;
margin: 2px 4px;
padding: 2px 4px;
align-items: center;
justify-content: center;
border-bottom: 1px solid transparent;
&:hover ${VisibiltyButtonText} {
color: rgba(${props => props.theme.colors.text.secondary});
}
&:hover ${VisibiltyPrivateIcon} {
fill: rgba(${props => props.theme.colors.text.secondary});
stroke: rgba(${props => props.theme.colors.text.secondary});
}
&:hover {
border-bottom: 1px solid rgba(${props => props.theme.colors.primary});
}
`;
type MemberFilterOptions = {
projectID?: null | string;
teamID?: null | string;
organization?: boolean;
};
const fetchMembers = async (client: any, projectID: string, options: MemberFilterOptions, input: string, cb: any) => {
if (input && input.trim().length < 3) {
return [];
}
const res = await client.query({
query: gql`
query {
searchMembers(input: {searchFilter:"${input}", projectID:"${projectID}"}) {
id
similarity
status
user {
id
fullName
email
profileIcon {
url
initials
bgColor
}
}
}
}
`,
});
let results: any = [];
const emails: Array<string> = [];
if (res.data && res.data.searchMembers) {
results = [
...res.data.searchMembers.map((m: any) => {
if (m.status === 'INVITED') {
return {
label: m.id,
value: {
id: m.id,
type: 2,
profileIcon: {
bgColor: '#ccc',
initials: m.id.charAt(0),
},
},
};
}
emails.push(m.user.email);
return {
label: m.user.fullName,
value: { id: m.user.id, type: 0, profileIcon: m.user.profileIcon },
};
}),
];
}
if (RFC2822_EMAIL.test(input) && !emails.find(e => e === input)) {
results = [
...results,
{
label: input,
value: {
id: input,
type: 1,
profileIcon: {
bgColor: '#ccc',
initials: input.charAt(0),
},
},
},
];
}
return results;
};
type UserOptionProps = {
innerProps: any;
isDisabled: boolean;
isFocused: boolean;
label: string;
data: any;
getValue: any;
};
const OptionWrapper = styled.div<{ isFocused: boolean }>`
cursor: pointer;
padding: 4px 8px;
${props => props.isFocused && `background: rgba(${props.theme.colors.primary});`}
display: flex;
align-items: center;
`;
const OptionContent = styled.div`
display: flex;
flex-direction: column;
margin-left: 12px;
`;
const OptionLabel = styled.span<{ fontSize: number; quiet: boolean }>`
display: flex;
align-items: center;
font-size: ${p => p.fontSize}px;
color: rgba(${p => (p.quiet ? p.theme.colors.text.primary : p.theme.colors.text.primary)});
`;
const UserOption: React.FC<UserOptionProps> = ({ isDisabled, isFocused, innerProps, label, data }) => {
return !isDisabled ? (
<OptionWrapper {...innerProps} isFocused={isFocused}>
<TaskAssignee
size={32}
member={{
id: '',
fullName: data.value.label,
profileIcon: data.value.profileIcon,
}}
/>
<OptionContent>
<OptionLabel fontSize={16} quiet={false}>
{label}
</OptionLabel>
{data.value.type === 2 && (
<OptionLabel fontSize={14} quiet>
Joined
</OptionLabel>
)}
</OptionContent>
</OptionWrapper>
) : null;
};
const OptionValueWrapper = styled.div`
background: rgba(${props => props.theme.colors.bg.primary});
border-radius: 4px;
margin: 2px;
padding: 3px 6px 3px 4px;
display: flex;
align-items: center;
`;
const OptionValueLabel = styled.span`
font-size: 12px;
color: rgba(${props => props.theme.colors.text.secondary});
`;
const OptionValueRemove = styled.button`
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
outline: none;
padding: 0;
margin: 0;
margin-left: 4px;
`;
const OptionValue = ({ data, removeProps }: any) => {
return (
<OptionValueWrapper>
<OptionValueLabel>{data.label}</OptionValueLabel>
<OptionValueRemove {...removeProps}>
<Cross width={14} height={14} />
</OptionValueRemove>
</OptionValueWrapper>
);
};
const InviteButton = styled(Button)`
margin-top: 12px;
height: 32px;
padding: 4px 12px;
width: 100%;
justify-content: center;
`;
const InviteContainer = styled.div`
min-height: 300px;
display: flex;
flex-direction: column;
`;
const UserManagementPopup: React.FC<UserManagementPopupProps> = ({
projectID,
users,
projectMembers,
onInviteProjectMembers,
}) => {
const client = useApolloClient();
const [invitedUsers, setInvitedUsers] = useState<Array<any> | null>(null);
return (
<Popup tab={0} title="Invite a user">
<InviteContainer>
<AsyncSelect
getOptionValue={option => option.value.id}
placeholder="Email address or username"
noOptionsMessage={() => null}
onChange={(e: any) => {
setInvitedUsers(e);
}}
isMulti
autoFocus
cacheOptions
styles={colourStyles}
defaultOption
components={{
MultiValue: OptionValue,
Option: UserOption,
IndicatorSeparator: null,
DropdownIndicator: null,
}}
loadOptions={(i, cb) => fetchMembers(client, projectID, {}, i, cb)}
/>
</InviteContainer>
<InviteButton
onClick={() => {
if (invitedUsers) {
onInviteProjectMembers(
invitedUsers.map(user => {
if (user.value.type === 0) {
return {
userID: user.value.id,
};
}
return {
email: user.value.id,
};
}),
);
}
}}
disabled={invitedUsers === null}
hoverVariant="none"
fontSize="16px"
>
Send Invite
</InviteButton>
</Popup>
);
};
type TaskRouteProps = { type TaskRouteProps = {
taskID: string; taskID: string;
}; };
interface QuickCardEditorState {
isOpen: boolean;
target: React.RefObject<HTMLElement> | null;
taskID: string | null;
taskGroupID: string | null;
}
interface ProjectParams { interface ProjectParams {
projectID: string; projectID: string;
} }
const initialQuickCardEditorState: QuickCardEditorState = {
taskID: null,
taskGroupID: null,
isOpen: false,
target: null,
};
const Project = () => { const Project = () => {
const { projectID } = useParams<ProjectParams>(); const { projectID } = useParams<ProjectParams>();
const history = useHistory(); const history = useHistory();
const match = useRouteMatch(); const match = useRouteMatch();
const location = useLocation();
const { showPopup, hidePopup } = usePopup();
const labelsRef = useRef<Array<ProjectLabel>>([]);
const [value, setValue] = useStateWithLocalStorage(localStorage.CARD_LABEL_VARIANT_STORAGE_KEY);
const taskLabelsRef = useRef<Array<TaskLabel>>([]);
const [updateTaskDescription] = useUpdateTaskDescriptionMutation(); const [updateTaskDescription] = useUpdateTaskDescriptionMutation();
const taskLabelsRef = useRef<Array<TaskLabel>>([]); const [updateProjectMemberRole] = useUpdateProjectMemberRoleMutation();
const [updateTaskName] = useUpdateTaskNameMutation();
const { data, error } = useFindProjectQuery({
variables: { projectID },
pollInterval: polling.PROJECT,
});
const [toggleTaskLabel] = useToggleTaskLabelMutation({ const [toggleTaskLabel] = useToggleTaskLabelMutation({
onCompleted: newTaskLabel => { onCompleted: (newTaskLabel) => {
taskLabelsRef.current = newTaskLabel.toggleTaskLabel.task.labels; taskLabelsRef.current = newTaskLabel.toggleTaskLabel.task.labels;
}, },
}); });
const [value, setValue] = useStateWithLocalStorage(CARD_LABEL_VARIANT_STORAGE_KEY);
const [updateProjectMemberRole] = useUpdateProjectMemberRoleMutation();
const [deleteTask] = useDeleteTaskMutation({ const [deleteTask] = useDeleteTaskMutation({
update: (client, resp) => update: (client, resp) =>
updateApolloCache<FindProjectQuery>( updateApolloCache<FindProjectQuery>(
client, client,
FindProjectDocument, FindProjectDocument,
cache => (cache) =>
produce(cache, draftCache => { produce(cache, (draftCache) => {
if (resp.data) { if (resp.data) {
const taskGroupIdx = draftCache.findProject.taskGroups.findIndex( const taskGroupIdx = draftCache.findProject.taskGroups.findIndex(
tg => tg.tasks.findIndex(t => t.id === resp.data?.deleteTask.taskID) !== -1, (tg) => tg.tasks.findIndex((t) => t.id === resp.data?.deleteTask.taskID) !== -1,
); );
if (taskGroupIdx !== -1) { if (taskGroupIdx !== -1) {
draftCache.findProject.taskGroups[taskGroupIdx].tasks = cache.findProject.taskGroups[ draftCache.findProject.taskGroups[taskGroupIdx].tasks = cache.findProject.taskGroups[
taskGroupIdx taskGroupIdx
].tasks.filter(t => t.id !== resp.data?.deleteTask.taskID); ].tasks.filter((t) => t.id !== resp.data?.deleteTask.taskID);
} }
} }
}), }),
@ -432,20 +91,13 @@ const Project = () => {
), ),
}); });
const [updateTaskName] = useUpdateTaskNameMutation();
const { loading, data, error } = useFindProjectQuery({
variables: { projectID },
pollInterval: 3000,
});
const [updateProjectName] = useUpdateProjectNameMutation({ const [updateProjectName] = useUpdateProjectNameMutation({
update: (client, newName) => { update: (client, newName) => {
updateApolloCache<FindProjectQuery>( updateApolloCache<FindProjectQuery>(
client, client,
FindProjectDocument, FindProjectDocument,
cache => (cache) =>
produce(cache, draftCache => { produce(cache, (draftCache) => {
draftCache.findProject.name = newName.data?.updateProjectName.name ?? ''; draftCache.findProject.name = newName.data?.updateProjectName.name ?? '';
}), }),
{ projectID }, { projectID },
@ -458,8 +110,8 @@ const Project = () => {
updateApolloCache<FindProjectQuery>( updateApolloCache<FindProjectQuery>(
client, client,
FindProjectDocument, FindProjectDocument,
cache => (cache) =>
produce(cache, draftCache => { produce(cache, (draftCache) => {
if (response.data) { if (response.data) {
draftCache.findProject.members = [ draftCache.findProject.members = [
...cache.findProject.members, ...cache.findProject.members,
@ -480,10 +132,10 @@ const Project = () => {
updateApolloCache<FindProjectQuery>( updateApolloCache<FindProjectQuery>(
client, client,
FindProjectDocument, FindProjectDocument,
cache => (cache) =>
produce(cache, draftCache => { produce(cache, (draftCache) => {
draftCache.findProject.invitedMembers = cache.findProject.invitedMembers.filter( draftCache.findProject.invitedMembers = cache.findProject.invitedMembers.filter(
m => m.email !== response.data?.deleteInvitedProjectMember.invitedMember.email ?? '', (m) => m.email !== response.data?.deleteInvitedProjectMember.invitedMember.email ?? '',
); );
}), }),
{ projectID }, { projectID },
@ -495,10 +147,10 @@ const Project = () => {
updateApolloCache<FindProjectQuery>( updateApolloCache<FindProjectQuery>(
client, client,
FindProjectDocument, FindProjectDocument,
cache => (cache) =>
produce(cache, draftCache => { produce(cache, (draftCache) => {
draftCache.findProject.members = cache.findProject.members.filter( draftCache.findProject.members = cache.findProject.members.filter(
m => m.id !== response.data?.deleteProjectMember.member.id, (m) => m.id !== response.data?.deleteProjectMember.member.id,
); );
}), }),
{ projectID }, { projectID },
@ -506,20 +158,12 @@ const Project = () => {
}, },
}); });
const { user } = useCurrentUser();
const location = useLocation();
const { showPopup, hidePopup } = usePopup();
const $labelsRef = useRef<HTMLDivElement>(null);
const labelsRef = useRef<Array<ProjectLabel>>([]);
useEffect(() => { useEffect(() => {
if (data) { if (data) {
document.title = `${data.findProject.name} | Taskcafé`; document.title = `${data.findProject.name} | Taskcafé`;
} }
}, [data]); }, [data]);
if (error) {
history.push('/projects');
}
if (data) { if (data) {
labelsRef.current = data.findProject.labels; labelsRef.current = data.findProject.labels;
@ -529,26 +173,26 @@ const Project = () => {
onChangeRole={(userID, roleCode) => { onChangeRole={(userID, roleCode) => {
updateProjectMemberRole({ variables: { userID, roleCode, projectID } }); updateProjectMemberRole({ variables: { userID, roleCode, projectID } });
}} }}
onChangeProjectOwner={uid => { onChangeProjectOwner={() => {
hidePopup(); hidePopup();
}} }}
onRemoveFromBoard={userID => { onRemoveFromBoard={(userID) => {
deleteProjectMember({ variables: { userID, projectID } }); deleteProjectMember({ variables: { userID, projectID } });
hidePopup(); hidePopup();
}} }}
onRemoveInvitedFromBoard={email => { onRemoveInvitedFromBoard={(email) => {
deleteInvitedProjectMember({ variables: { projectID, email } }); deleteInvitedProjectMember({ variables: { projectID, email } });
hidePopup(); hidePopup();
}} }}
onSaveProjectName={projectName => { onSaveProjectName={(projectName) => {
updateProjectName({ variables: { projectID, name: projectName } }); updateProjectName({ variables: { projectID, name: projectName } });
}} }}
onInviteUser={$target => { onInviteUser={($target) => {
showPopup( showPopup(
$target, $target,
<UserManagementPopup <UserManagementPopup
projectID={projectID} projectID={projectID}
onInviteProjectMembers={members => { onInviteProjectMembers={(members) => {
inviteProjectMembers({ variables: { projectID, members } }); inviteProjectMembers({ variables: { projectID, members } });
hidePopup(); hidePopup();
}} }}
@ -557,7 +201,14 @@ const Project = () => {
/>, />,
); );
}} }}
popupContent={<ProjectPopup history={history} name={data.findProject.name} projectID={projectID} />} popupContent={
<ProjectPopup // eslint-disable-line
history={history}
publicOn={data.findProject.publicOn}
name={data.findProject.name}
projectID={projectID}
/>
}
menuType={[{ name: 'Board', link: location.pathname }]} menuType={[{ name: 'Board', link: location.pathname }]}
currentTab={0} currentTab={0}
projectMembers={data.findProject.members} projectMembers={data.findProject.members}
@ -582,12 +233,12 @@ const Project = () => {
/> />
<Route <Route
path={`${match.path}/board/c/:taskID`} path={`${match.path}/board/c/:taskID`}
render={(routeProps: RouteComponentProps<TaskRouteProps>) => ( render={() => {
return (
<Details <Details
refreshCache={NOOP} refreshCache={NOOP}
availableMembers={data.findProject.members} availableMembers={data.findProject.members}
projectURL={`${match.url}/board`} projectURL={`${match.url}/board`}
taskID={routeProps.match.params.taskID}
onTaskNameChange={(updatedTask, newName) => { onTaskNameChange={(updatedTask, newName) => {
updateTaskName({ variables: { taskID: updatedTask.id, name: newName } }); updateTaskName({ variables: { taskID: updatedTask.id, name: newName } });
}} }}
@ -604,7 +255,7 @@ const Project = () => {
}, },
}); });
}} }}
onDeleteTask={deletedTask => { onDeleteTask={(deletedTask) => {
deleteTask({ variables: { taskID: deletedTask.id } }); deleteTask({ variables: { taskID: deletedTask.id } });
history.push(`${match.url}/board`); history.push(`${match.url}/board`);
}} }}
@ -613,18 +264,19 @@ const Project = () => {
showPopup( showPopup(
$targetRef, $targetRef,
<LabelManagerEditor <LabelManagerEditor
onLabelToggle={labelID => { onLabelToggle={(labelID) => {
toggleTaskLabel({ variables: { taskID: task.id, projectLabelID: labelID } }); toggleTaskLabel({ variables: { taskID: task.id, projectLabelID: labelID } });
}} }}
taskID={task.id}
labelColors={data.labelColors} labelColors={data.labelColors}
labels={labelsRef}
taskLabels={taskLabelsRef} taskLabels={taskLabelsRef}
projectID={projectID} projectID={projectID}
/>, />,
); );
}} }}
/> />
)} );
}}
/> />
</> </>
); );

View File

@ -12,18 +12,19 @@ import {
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import NewProject from 'shared/components/NewProject'; import NewProject from 'shared/components/NewProject';
import { PermissionLevel, PermissionObjectType, useCurrentUser } from 'App/context'; import { useCurrentUser } from 'App/context';
import Button from 'shared/components/Button'; import Button from 'shared/components/Button';
import { usePopup, Popup } from 'shared/components/PopupMenu'; import { usePopup, Popup } from 'shared/components/PopupMenu';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import Input from 'shared/components/Input'; import ControlledInput from 'shared/components/ControlledInput';
import updateApolloCache from 'shared/utils/cache'; import updateApolloCache from 'shared/utils/cache';
import produce from 'immer'; import produce from 'immer';
import NOOP from 'shared/utils/noop'; import NOOP from 'shared/utils/noop';
import theme from 'App/ThemeStyles'; import theme from 'App/ThemeStyles';
import polling from 'shared/utils/polling';
import { mixin } from '../shared/utils/styles'; import { mixin } from '../shared/utils/styles';
type CreateTeamData = { teamName: string }; type CreateTeamData = { name: string };
type CreateTeamFormProps = { type CreateTeamFormProps = {
onCreateTeam: (teamName: string) => void; onCreateTeam: (teamName: string) => void;
@ -35,28 +36,30 @@ const CreateTeamButton = styled(Button)`
width: 100%; width: 100%;
`; `;
const ErrorText = styled.span`
font-size: 14px;
color: ${(props) => props.theme.colors.danger};
`;
const CreateTeamForm: React.FC<CreateTeamFormProps> = ({ onCreateTeam }) => { const CreateTeamForm: React.FC<CreateTeamFormProps> = ({ onCreateTeam }) => {
const { register, handleSubmit } = useForm<CreateTeamData>(); const {
register,
handleSubmit,
formState: { errors },
} = useForm<CreateTeamData>();
const createTeam = (data: CreateTeamData) => { const createTeam = (data: CreateTeamData) => {
onCreateTeam(data.teamName); onCreateTeam(data.name);
}; };
return ( return (
<CreateTeamFormContainer onSubmit={handleSubmit(createTeam)}> <CreateTeamFormContainer onSubmit={handleSubmit(createTeam)}>
<Input {errors.name && <ErrorText>{errors.name.message}</ErrorText>}
width="100%" <ControlledInput width="100%" label="Team name" variant="alternate" {...register('name')} />
label="Team name"
id="teamName"
name="teamName"
variant="alternate"
ref={register({ required: 'Team name is required' })}
/>
<CreateTeamButton type="submit">Create</CreateTeamButton> <CreateTeamButton type="submit">Create</CreateTeamButton>
</CreateTeamFormContainer> </CreateTeamFormContainer>
); );
}; };
const ProjectAddTile = styled.div` const ProjectAddTile = styled.div`
background-color: ${props => mixin.rgba(props.theme.colors.bg.primary, 0.4)}; background-color: ${(props) => mixin.rgba(props.theme.colors.bg.primary, 0.4)};
background-size: cover; background-size: cover;
background-position: 50%; background-position: 50%;
color: #fff; color: #fff;
@ -70,7 +73,7 @@ const ProjectAddTile = styled.div`
`; `;
const ProjectTile = styled(Link)<{ color: string }>` const ProjectTile = styled(Link)<{ color: string }>`
background-color: ${props => props.color}; background-color: ${(props) => props.color};
background-size: cover; background-size: cover;
background-position: 50%; background-position: 50%;
color: #fff; color: #fff;
@ -141,7 +144,7 @@ const ProjectTileName = styled.div<{ centered?: boolean }>`
max-height: 40px; max-height: 40px;
width: 100%; width: 100%;
word-wrap: break-word; word-wrap: break-word;
${props => props.centered && 'text-align: center;'} ${(props) => props.centered && 'text-align: center;'}
`; `;
const Wrapper = styled.div` const Wrapper = styled.div`
@ -179,7 +182,7 @@ const SectionActionLink = styled(Link)`
const ProjectSectionTitle = styled.h3` const ProjectSectionTitle = styled.h3`
font-size: 16px; font-size: 16px;
color: ${props => props.theme.colors.text.primary}; color: ${(props) => props.theme.colors.text.primary};
`; `;
const ProjectsContainer = styled.div` const ProjectsContainer = styled.div`
@ -203,14 +206,14 @@ type ShowNewProject = {
const Projects = () => { const Projects = () => {
const { showPopup, hidePopup } = usePopup(); const { showPopup, hidePopup } = usePopup();
const { loading, data } = useGetProjectsQuery({ pollInterval: 3000, fetchPolicy: 'cache-and-network' }); const { loading, data } = useGetProjectsQuery({ pollInterval: polling.PROJECTS, fetchPolicy: 'cache-and-network' });
useEffect(() => { useEffect(() => {
document.title = 'Taskcafé'; document.title = 'Taskcafé';
}, []); }, []);
const [createProject] = useCreateProjectMutation({ const [createProject] = useCreateProjectMutation({
update: (client, newProject) => { update: (client, newProject) => {
updateApolloCache<GetProjectsQuery>(client, GetProjectsDocument, cache => updateApolloCache<GetProjectsQuery>(client, GetProjectsDocument, (cache) =>
produce(cache, draftCache => { produce(cache, (draftCache) => {
if (newProject.data) { if (newProject.data) {
draftCache.projects.push({ ...newProject.data.createProject }); draftCache.projects.push({ ...newProject.data.createProject });
} }
@ -223,8 +226,8 @@ const Projects = () => {
const { user } = useCurrentUser(); const { user } = useCurrentUser();
const [createTeam] = useCreateTeamMutation({ const [createTeam] = useCreateTeamMutation({
update: (client, createData) => { update: (client, createData) => {
updateApolloCache<GetProjectsQuery>(client, GetProjectsDocument, cache => updateApolloCache<GetProjectsQuery>(client, GetProjectsDocument, (cache) =>
produce(cache, draftCache => { produce(cache, (draftCache) => {
if (createData.data) { if (createData.data) {
draftCache.teams.push({ ...createData.data?.createTeam }); draftCache.teams.push({ ...createData.data?.createTeam });
} }
@ -238,7 +241,7 @@ const Projects = () => {
const { projects, teams, organizations } = data; const { projects, teams, organizations } = data;
const organizationID = organizations[0].id ?? null; const organizationID = organizations[0].id ?? null;
const personalProjects = projects const personalProjects = projects
.filter(p => p.team === null) .filter((p) => p.team === null)
.sort((a, b) => { .sort((a, b) => {
const textA = a.name.toUpperCase(); const textA = a.name.toUpperCase();
const textB = b.name.toUpperCase(); const textB = b.name.toUpperCase();
@ -250,12 +253,12 @@ const Projects = () => {
const textB = b.name.toUpperCase(); const textB = b.name.toUpperCase();
return textA < textB ? -1 : textA > textB ? 1 : 0; // eslint-disable-line no-nested-ternary return textA < textB ? -1 : textA > textB ? 1 : 0; // eslint-disable-line no-nested-ternary
}) })
.map(team => { .map((team) => {
return { return {
id: team.id, id: team.id,
name: team.name, name: team.name,
projects: projects projects: projects
.filter(project => project.team && project.team.id === team.id) .filter((project) => project.team && project.team.id === team.id)
.sort((a, b) => { .sort((a, b) => {
const textA = a.name.toUpperCase(); const textA = a.name.toUpperCase();
const textB = b.name.toUpperCase(); const textB = b.name.toUpperCase();
@ -268,10 +271,10 @@ const Projects = () => {
<GlobalTopNavbar onSaveProjectName={NOOP} projectID={null} name={null} /> <GlobalTopNavbar onSaveProjectName={NOOP} projectID={null} name={null} />
<Wrapper> <Wrapper>
<ProjectsContainer> <ProjectsContainer>
{user.roles.org === 'admin' && ( {true && ( // TODO: add permision check
<AddTeamButton <AddTeamButton
variant="outline" variant="outline"
onClick={$target => { onClick={($target) => {
showPopup( showPopup(
$target, $target,
<Popup <Popup
@ -282,7 +285,7 @@ const Projects = () => {
}} }}
> >
<CreateTeamForm <CreateTeamForm
onCreateTeam={teamName => { onCreateTeam={(teamName) => {
if (organizationID) { if (organizationID) {
createTeam({ variables: { name: teamName, organizationID } }); createTeam({ variables: { name: teamName, organizationID } });
hidePopup(); hidePopup();
@ -325,12 +328,12 @@ const Projects = () => {
</ProjectListItem> </ProjectListItem>
</ProjectList> </ProjectList>
</div> </div>
{projectTeams.map(team => { {projectTeams.map((team) => {
return ( return (
<div key={team.id}> <div key={team.id}>
<ProjectSectionTitleWrapper> <ProjectSectionTitleWrapper>
<ProjectSectionTitle>{team.name}</ProjectSectionTitle> <ProjectSectionTitle>{team.name}</ProjectSectionTitle>
{user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, team.id) && ( {true && ( // TODO: add permision check
<SectionActions> <SectionActions>
<SectionActionLink to={`/teams/${team.id}`}> <SectionActionLink to={`/teams/${team.id}`}>
<SectionAction variant="outline">Projects</SectionAction> <SectionAction variant="outline">Projects</SectionAction>
@ -355,7 +358,7 @@ const Projects = () => {
</ProjectTile> </ProjectTile>
</ProjectListItem> </ProjectListItem>
))} ))}
{user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, team.id) && ( {true && ( // TODO: add permision check
<ProjectListItem> <ProjectListItem>
<ProjectAddTile <ProjectAddTile
onClick={() => { onClick={() => {

View File

@ -35,7 +35,7 @@ const UsersRegister = () => {
}, },
}), }),
}) })
.then(async x => { .then(async (x) => {
const response = await x.json(); const response = await x.json();
const { setup } = response; const { setup } = response;
console.log(response); console.log(response);

View File

@ -2,8 +2,9 @@ import React, { useState } from 'react';
import Input from 'shared/components/Input'; import Input from 'shared/components/Input';
import updateApolloCache from 'shared/utils/cache'; import updateApolloCache from 'shared/utils/cache';
import produce from 'immer'; import produce from 'immer';
import polling from 'shared/utils/polling';
import Button from 'shared/components/Button'; import Button from 'shared/components/Button';
import { useCurrentUser, PermissionLevel, PermissionObjectType } from 'App/context'; import { useCurrentUser } from 'App/context';
import Select from 'shared/components/Select'; import Select from 'shared/components/Select';
import { import {
useGetTeamQuery, useGetTeamQuery,
@ -35,7 +36,7 @@ const UserMember = styled(Member)`
padding: 4px 0; padding: 4px 0;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background: ${props => mixin.rgba(props.theme.colors.bg.primary, 0.4)}; background: ${(props) => mixin.rgba(props.theme.colors.bg.primary, 0.4)};
} }
border-radius: 6px; border-radius: 6px;
`; `;
@ -56,8 +57,8 @@ const UserManagementPopup: React.FC<UserManagementPopupProps> = ({ users, teamMe
<SearchInput width="100%" variant="alternate" placeholder="Email address or name" name="search" /> <SearchInput width="100%" variant="alternate" placeholder="Email address or name" name="search" />
<TeamMemberList> <TeamMemberList>
{users {users
.filter(u => u.id !== teamMembers.find(p => p.id === u.id)?.id) .filter((u) => u.id !== teamMembers.find((p) => p.id === u.id)?.id)
.map(user => ( .map((user) => (
<UserMember <UserMember
key={user.id} key={user.id}
onCardMemberClick={() => onAddTeamMember(user.id)} onCardMemberClick={() => onAddTeamMember(user.id)}
@ -115,7 +116,7 @@ export const MiniProfileActionItem = styled.span<{ disabled?: boolean }>`
position: relative; position: relative;
text-decoration: none; text-decoration: none;
${props => ${(props) =>
props.disabled props.disabled
? css` ? css`
user-select: none; user-select: none;
@ -136,7 +137,7 @@ export const Content = styled.div`
export const CurrentPermission = styled.span` export const CurrentPermission = styled.span`
margin-left: 4px; margin-left: 4px;
color: ${props => mixin.rgba(props.theme.colors.text.secondary, 0.4)}; color: ${(props) => mixin.rgba(props.theme.colors.text.secondary, 0.4)};
`; `;
export const Separator = styled.div` export const Separator = styled.div`
@ -147,13 +148,13 @@ export const Separator = styled.div`
export const WarningText = styled.span` export const WarningText = styled.span`
display: flex; display: flex;
color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.4)}; color: ${(props) => mixin.rgba(props.theme.colors.text.primary, 0.4)};
padding: 6px; padding: 6px;
`; `;
export const DeleteDescription = styled.div` export const DeleteDescription = styled.div`
font-size: 14px; font-size: 14px;
color: ${props => props.theme.colors.text.primary}; color: ${(props) => props.theme.colors.text.primary};
`; `;
export const RemoveMemberButton = styled(Button)` export const RemoveMemberButton = styled(Button)`
@ -220,13 +221,13 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
<MiniProfileActions> <MiniProfileActions>
<MiniProfileActionWrapper> <MiniProfileActionWrapper>
{permissions {permissions
.filter(p => (subject.role && subject.role.code === 'owner') || p.code !== 'owner') .filter((p) => (subject.role && subject.role.code === 'owner') || p.code !== 'owner')
.map(perm => ( .map((perm) => (
<MiniProfileActionItem <MiniProfileActionItem
disabled={subject.role && perm.code !== subject.role.code && !canChangeRole} disabled={subject.role && perm.code !== subject.role.code && !canChangeRole}
key={perm.code} key={perm.code}
onClick={() => { onClick={() => {
if (onChangeRole && subject.role && perm.code !== subject.role.code) { if (subject.role && perm.code !== subject.role.code) {
switch (perm.code) { switch (perm.code) {
case 'owner': case 'owner':
onChangeRole(RoleCode.Owner); onChangeRole(RoleCode.Owner);
@ -275,8 +276,8 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
<Select <Select
label="New projects owner" label="New projects owner"
value={orphanedProjectOwner} value={orphanedProjectOwner}
onChange={value => setOrphanedProjectOwner(value)} onChange={(value) => setOrphanedProjectOwner(value)}
options={members.filter(m => m.id !== subject.id).map(m => ({ label: m.fullName, value: m.id }))} options={members.filter((m) => m.id !== subject.id).map((m) => ({ label: m.fullName, value: m.id }))}
/> />
</> </>
)} )}
@ -306,14 +307,14 @@ const MemberItemOption = styled(Button)`
`; `;
const MemberList = styled.div` const MemberList = styled.div`
border-top: 1px solid ${props => props.theme.colors.border}; border-top: 1px solid ${(props) => props.theme.colors.border};
`; `;
const MemberListItem = styled.div` const MemberListItem = styled.div`
display: flex; display: flex;
flex-flow: row wrap; flex-flow: row wrap;
justify-content: space-between; justify-content: space-between;
border-bottom: 1px solid ${props => props.theme.colors.border}; border-bottom: 1px solid ${(props) => props.theme.colors.border};
min-height: 40px; min-height: 40px;
padding: 12px 0 12px 40px; padding: 12px 0 12px 40px;
position: relative; position: relative;
@ -337,11 +338,11 @@ const MemberProfile = styled(TaskAssignee)`
`; `;
const MemberItemName = styled.p` const MemberItemName = styled.p`
color: ${props => props.theme.colors.text.secondary}; color: ${(props) => props.theme.colors.text.secondary};
`; `;
const MemberItemUsername = styled.p` const MemberItemUsername = styled.p`
color: ${props => props.theme.colors.text.primary}; color: ${(props) => props.theme.colors.text.primary};
`; `;
const MemberListHeader = styled.div` const MemberListHeader = styled.div`
@ -350,12 +351,12 @@ const MemberListHeader = styled.div`
`; `;
const ListTitle = styled.h3` const ListTitle = styled.h3`
font-size: 18px; font-size: 18px;
color: ${props => props.theme.colors.text.secondary}; color: ${(props) => props.theme.colors.text.secondary};
margin-bottom: 12px; margin-bottom: 12px;
`; `;
const ListDesc = styled.span` const ListDesc = styled.span`
font-size: 16px; font-size: 16px;
color: ${props => props.theme.colors.text.primary}; color: ${(props) => props.theme.colors.text.primary};
`; `;
const FilterSearch = styled(Input)` const FilterSearch = styled(Input)`
margin: 0; margin: 0;
@ -387,11 +388,11 @@ const FilterTabItem = styled.li`
font-weight: 700; font-weight: 700;
text-decoration: none; text-decoration: none;
padding: 6px 8px; padding: 6px 8px;
color: ${props => props.theme.colors.text.primary}; color: ${(props) => props.theme.colors.text.primary};
&:hover { &:hover {
border-radius: 6px; border-radius: 6px;
background: ${props => props.theme.colors.primary}; background: ${(props) => props.theme.colors.primary};
color: ${props => props.theme.colors.text.secondary}; color: ${(props) => props.theme.colors.text.secondary};
} }
`; `;
@ -422,9 +423,9 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
const { loading, data } = useGetTeamQuery({ const { loading, data } = useGetTeamQuery({
variables: { teamID }, variables: { teamID },
fetchPolicy: 'cache-and-network', fetchPolicy: 'cache-and-network',
pollInterval: 3000, pollInterval: polling.MEMBERS,
}); });
const { user, setUserRoles } = useCurrentUser(); const { user } = useCurrentUser();
const warning = 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”.'; '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 [createTeamMember] = useCreateTeamMemberMutation({ const [createTeamMember] = useCreateTeamMemberMutation({
@ -432,8 +433,8 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
updateApolloCache<GetTeamQuery>( updateApolloCache<GetTeamQuery>(
client, client,
GetTeamDocument, GetTeamDocument,
cache => (cache) =>
produce(cache, draftCache => { produce(cache, (draftCache) => {
if (response.data) { if (response.data) {
draftCache.findTeam.members.push({ draftCache.findTeam.members.push({
...response.data.createTeamMember.teamMember, ...response.data.createTeamMember.teamMember,
@ -446,26 +447,16 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
); );
}, },
}); });
const [updateTeamMemberRole] = useUpdateTeamMemberRoleMutation({ const [updateTeamMemberRole] = useUpdateTeamMemberRoleMutation();
onCompleted: r => {
if (user) {
setUserRoles(
produce(user.roles, draftRoles => {
draftRoles.teams.set(r.updateTeamMemberRole.teamID, r.updateTeamMemberRole.member.role.code);
}),
);
}
},
});
const [deleteTeamMember] = useDeleteTeamMemberMutation({ const [deleteTeamMember] = useDeleteTeamMemberMutation({
update: (client, response) => { update: (client, response) => {
updateApolloCache<GetTeamQuery>( updateApolloCache<GetTeamQuery>(
client, client,
GetTeamDocument, GetTeamDocument,
cache => (cache) =>
produce(cache, draftCache => { produce(cache, (draftCache) => {
draftCache.findTeam.members = cache.findTeam.members.filter( draftCache.findTeam.members = cache.findTeam.members.filter(
member => member.id !== response.data?.deleteTeamMember.userID, (member) => member.id !== response.data?.deleteTeamMember.userID,
); );
}), }),
{ teamID }, { teamID },
@ -491,15 +482,15 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
</ListDesc> </ListDesc>
<ListActions> <ListActions>
<FilterSearch width="250px" variant="alternate" placeholder="Filter by name" /> <FilterSearch width="250px" variant="alternate" placeholder="Filter by name" />
{user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, data.findTeam.id) && ( {true && ( // TODO: add permission check
<InviteMemberButton <InviteMemberButton
onClick={$target => { onClick={($target) => {
showPopup( showPopup(
$target, $target,
<UserManagementPopup <UserManagementPopup
users={data.users} users={data.users}
teamMembers={data.findTeam.members} teamMembers={data.findTeam.members}
onAddTeamMember={userID => { onAddTeamMember={(userID) => {
createTeamMember({ variables: { userID, teamID } }); createTeamMember({ variables: { userID, teamID } });
}} }}
/>, />,
@ -513,7 +504,7 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
</ListActions> </ListActions>
</MemberListHeader> </MemberListHeader>
<MemberList> <MemberList>
{data.findTeam.members.map(member => ( {data.findTeam.members.map((member) => (
<MemberListItem> <MemberListItem>
<MemberProfile showRoleIcons size={32} onMemberProfile={NOOP} member={member} /> <MemberProfile showRoleIcons size={32} onMemberProfile={NOOP} member={member} />
<MemberListItemDetails> <MemberListItemDetails>
@ -524,22 +515,23 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
<MemberItemOption variant="flat">On 2 projects</MemberItemOption> <MemberItemOption variant="flat">On 2 projects</MemberItemOption>
<MemberItemOption <MemberItemOption
variant="outline" variant="outline"
onClick={$target => { onClick={($target) => {
showPopup( showPopup(
$target, $target,
<TeamRoleManagerPopup <TeamRoleManagerPopup
currentUserID={user.id ?? ''} currentUserID={user ?? ''}
subject={member} subject={member}
members={data.findTeam.members} members={data.findTeam.members}
warning={member.role && member.role.code === 'owner' ? warning : null} warning={member.role && member.role.code === 'owner' ? warning : null}
canChangeRole={user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID)} // canChangeRole={user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID)} TODO: add permission check
onChangeRole={roleCode => { canChangeRole
onChangeRole={(roleCode) => {
updateTeamMemberRole({ variables: { userID: member.id, teamID, roleCode } }); updateTeamMemberRole({ variables: { userID: member.id, teamID, roleCode } });
}} }}
onRemoveFromTeam={ onRemoveFromTeam={
member.role && member.role.code === 'owner' member.role && member.role.code === 'owner'
? undefined ? undefined
: newOwnerID => { : (newOwnerID) => {
deleteTeamMember({ variables: { teamID, newOwnerID, userID: member.id } }); deleteTeamMember({ variables: { teamID, newOwnerID, userID: member.id } });
hidePopup(); hidePopup();
} }

View File

@ -9,6 +9,7 @@ import {
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import Input from 'shared/components/Input'; import Input from 'shared/components/Input';
import theme from 'App/ThemeStyles'; import theme from 'App/ThemeStyles';
import polling from 'shared/utils/polling';
const FilterSearch = styled(Input)` const FilterSearch = styled(Input)`
margin: 0; margin: 0;
@ -158,7 +159,7 @@ const TeamProjects: React.FC<TeamProjectsProps> = ({ teamID }) => {
const { loading, data } = useGetTeamQuery({ const { loading, data } = useGetTeamQuery({
variables: { teamID }, variables: { teamID },
fetchPolicy: 'cache-and-network', fetchPolicy: 'cache-and-network',
pollInterval: 3000, pollInterval: polling.TEAM_PROJECTS,
}); });
if (data) { if (data) {
return ( return (

View File

@ -13,7 +13,7 @@ import { usePopup, Popup } from 'shared/components/PopupMenu';
import { History } from 'history'; import { History } from 'history';
import produce from 'immer'; import produce from 'immer';
import { TeamSettings, DeleteConfirm, DELETE_INFO } from 'shared/components/ProjectSettings'; import { TeamSettings, DeleteConfirm, DELETE_INFO } from 'shared/components/ProjectSettings';
import { PermissionObjectType, PermissionLevel, useCurrentUser } from 'App/context'; import { useCurrentUser } from 'App/context';
import NOOP from 'shared/utils/noop'; import NOOP from 'shared/utils/noop';
import Members from './Members'; import Members from './Members';
import Projects from './Projects'; import Projects from './Projects';
@ -95,9 +95,12 @@ const Teams = () => {
const [currentTab, setCurrentTab] = useState(0); const [currentTab, setCurrentTab] = useState(0);
const match = useRouteMatch(); const match = useRouteMatch();
if (data && user) { if (data && user) {
/*
TODO: re-add permission check
if (!user.isVisible(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID)) { if (!user.isVisible(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID)) {
return <Redirect to="/" />; return <Redirect to="/" />;
} }
*/
return ( return (
<> <>
<GlobalTopNavbar <GlobalTopNavbar

View File

@ -1,20 +1,15 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import axios from 'axios'; import { ApolloClient } from '@apollo/client';
import createAuthRefreshInterceptor from 'axios-auth-refresh'; import { ApolloProvider } from '@apollo/client/react';
import { ApolloProvider } from '@apollo/react-hooks';
import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { onError } from 'apollo-link-error';
import { enableMapSet } from 'immer'; import { enableMapSet } from 'immer';
import { ApolloLink, Observable, fromPromise } from 'apollo-link';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import updateLocale from 'dayjs/plugin/updateLocale'; import updateLocale from 'dayjs/plugin/updateLocale';
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter'; import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
import customParseFormat from 'dayjs/plugin/customParseFormat'; import customParseFormat from 'dayjs/plugin/customParseFormat';
import isBetween from 'dayjs/plugin/isBetween'; import isBetween from 'dayjs/plugin/isBetween';
import weekday from 'dayjs/plugin/weekday'; import weekday from 'dayjs/plugin/weekday';
import { getAccessToken, getNewToken, setAccessToken } from 'shared/utils/accessToken';
import cache from './App/cache'; import cache from './App/cache';
import App from './App'; import App from './App';
@ -34,131 +29,8 @@ dayjs.updateLocale('en', {
}, },
}); });
let forward$; const client = new ApolloClient({ uri: '/graphql', cache });
let isRefreshing = false; console.log('cloient', client);
let pendingRequests: any = [];
const refreshAuthLogic = (failedRequest: any) =>
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();
});
createAuthRefreshInterceptor(axios, refreshAuthLogic);
const resolvePendingRequests = () => {
pendingRequests.map((callback: any) => callback());
pendingRequests = [];
};
const resolvePromise = (resolve: () => void) => {
pendingRequests.push(() => resolve());
};
const resetPendingRequests = () => {
pendingRequests = [];
};
const setRefreshing = (newVal: boolean) => {
isRefreshing = newVal;
};
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
for (const err of graphQLErrors) {
if (err.extensions && err.extensions.code) {
switch (err.extensions.code) {
case 'UNAUTHENTICATED':
if (!isRefreshing) {
setRefreshing(true);
forward$ = fromPromise(
getNewToken()
.then((response: any) => {
setAccessToken(response.accessToken);
resolvePendingRequests();
return response.accessToken;
})
.catch(() => {
resetPendingRequests();
// TODO
// Handle token refresh errors e.g clear stored tokens, redirect to login, ...
return undefined;
})
.finally(() => {
setRefreshing(false);
}),
).filter(value => Boolean(value));
} else {
forward$ = fromPromise(new Promise(resolvePromise));
}
return forward$.flatMap(() => forward(operation));
default:
// pass
}
}
}
}
if (networkError) {
console.log(`[Network error]: ${networkError}`); // eslint-disable-line no-console
}
return undefined;
});
const requestLink = new ApolloLink(
(operation, forward) =>
new Observable((observer: any) => {
let handle: any;
Promise.resolve(operation)
.then((op: any) => {
const accessToken = getAccessToken();
if (accessToken) {
op.setContext({
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
}
})
.then(() => {
handle = forward(operation).subscribe({
next: observer.next.bind(observer),
error: observer.error.bind(observer),
complete: observer.complete.bind(observer),
});
})
.catch(observer.error.bind(observer));
return () => {
if (handle) {
handle.unsubscribe();
}
};
}),
);
const client = new ApolloClient({
link: ApolloLink.from([
onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
graphQLErrors.forEach(
({ message, locations, path }) =>
console.log(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`), // eslint-disable-line no-console
);
}
if (networkError) {
console.log(`[Network error]: ${networkError}`); // eslint-disable-line no-console
}
}),
errorLink,
requestLink,
new HttpLink({
uri: '/graphql',
credentials: 'same-origin',
}),
]),
cache,
});
ReactDOM.render( ReactDOM.render(
<ApolloProvider client={client}> <ApolloProvider client={client}>

View File

@ -1,5 +1,5 @@
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
import TextareaAutosize from 'react-autosize-textarea/lib'; import TextareaAutosize from 'react-autosize-textarea';
import { mixin } from 'shared/utils/styles'; import { mixin } from 'shared/utils/styles';
import Button from 'shared/components/Button'; import Button from 'shared/components/Button';

View File

@ -49,6 +49,7 @@ export const NameEditor: React.FC<NameEditorProps> = ({ onSave: handleSave, onCa
<ListNameEditorWrapper> <ListNameEditorWrapper>
<ListNameEditor <ListNameEditor
ref={$editorRef} ref={$editorRef}
height={40}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
value={listName} value={listName}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setListName(e.currentTarget.value)} onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setListName(e.currentTarget.value)}

View File

@ -10,6 +10,215 @@ import Button from 'shared/components/Button';
import NOOP from 'shared/utils/noop'; import NOOP from 'shared/utils/noop';
import { mixin } from 'shared/utils/styles'; import { mixin } from 'shared/utils/styles';
const UserSelect = styled(Select)`
margin: 8px 0;
padding: 8px 0;
`;
const NewUserPassInput = styled(Input)`
margin: 8px 0;
`;
const InviteMemberButton = styled(Button)`
padding: 7px 12px;
`;
const UserPassBar = styled.div`
display: flex;
padding-top: 8px;
`;
const UserPassConfirmButton = styled(Button)`
width: 100%;
padding: 7px 12px;
`;
const UserPassButton = styled(Button)`
width: 50%;
padding: 7px 12px;
& ~ & {
margin-left: 6px;
}
`;
const MemberItemOptions = styled.div``;
const MemberItemOption = styled(Button)`
padding: 7px 9px;
margin: 4px 0 4px 8px;
float: left;
min-width: 95px;
`;
const MemberList = styled.div`
border-top: 1px solid ${(props) => props.theme.colors.border};
`;
const MemberListItem = styled.div`
display: flex;
flex-flow: row wrap;
justify-content: space-between;
border-bottom: 1px solid ${(props) => props.theme.colors.border};
min-height: 40px;
padding: 12px 0 12px 40px;
position: relative;
`;
const MemberListItemDetails = styled.div`
float: left;
flex: 1 0 auto;
padding-left: 8px;
`;
const InviteIcon = styled(UserPlus)`
padding-right: 4px;
`;
const MemberProfile = styled(TaskAssignee)`
position: absolute;
top: 16px;
left: 0;
margin: 0;
`;
const MemberItemName = styled.p`
color: ${(props) => props.theme.colors.text.secondary};
`;
const MemberItemUsername = styled.p`
color: ${(props) => props.theme.colors.text.primary};
`;
const MemberListHeader = styled.div`
display: flex;
flex-direction: column;
`;
const ListTitle = styled.h3`
font-size: 18px;
color: ${(props) => props.theme.colors.text.secondary};
margin-bottom: 12px;
`;
const ListDesc = styled.span`
font-size: 16px;
color: ${(props) => props.theme.colors.text.primary};
`;
const FilterSearch = styled(Input)`
margin: 0;
`;
const ListActions = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 8px;
margin-bottom: 18px;
`;
const MemberListWrapper = styled.div`
flex: 1 1;
`;
const Container = styled.div`
padding: 2.2rem;
display: flex;
width: 100%;
max-width: 1400px;
position: relative;
margin: 0 auto;
`;
const TabNav = styled.div`
float: left;
width: 220px;
height: 100%;
display: block;
position: relative;
`;
const TabNavContent = styled.ul`
display: block;
width: auto;
border-bottom: 0 !important;
border-right: 1px solid rgba(0, 0, 0, 0.05);
`;
const TabNavItem = styled.li`
padding: 0.35rem 0.3rem;
height: 48px;
display: block;
position: relative;
`;
const TabNavItemButton = styled.button<{ active: boolean }>`
cursor: pointer;
display: flex;
align-items: center;
padding-top: 10px !important;
padding-bottom: 10px !important;
padding-left: 12px !important;
padding-right: 8px !important;
width: 100%;
position: relative;
color: ${(props) => (props.active ? `${props.theme.colors.secondary}` : props.theme.colors.text.primary)};
&:hover {
color: ${(props) => `${props.theme.colors.primary}`};
}
&:hover svg {
fill: ${(props) => props.theme.colors.primary};
}
`;
const TabItemUser = styled(User)<{ active: boolean }>`
fill: ${(props) => (props.active ? `${props.theme.colors.primary}` : props.theme.colors.text.primary)}
stroke: ${(props) => (props.active ? `${props.theme.colors.primary}` : props.theme.colors.text.primary)}
`;
const TabNavItemSpan = styled.span`
text-align: left;
padding-left: 9px;
font-size: 14px;
`;
const TabNavLine = styled.span<{ top: number }>`
left: auto;
right: 0;
width: 2px;
height: 48px;
transform: scaleX(1);
top: ${(props) => props.top}px;
background: linear-gradient(
30deg,
${(props) => props.theme.colors.primary},
${(props) => props.theme.colors.primary}
);
box-shadow: 0 0 8px 0 ${(props) => props.theme.colors.primary};
display: block;
position: absolute;
transition: all 0.2s ease;
`;
const TabContentWrapper = styled.div`
position: relative;
display: block;
overflow: hidden;
width: 100%;
margin-left: 1rem;
`;
const TabContent = styled.div`
position: relative;
width: 100%;
display: block;
padding: 0;
padding: 1.5rem;
background-color: #10163a;
border-radius: 0.5rem;
`;
const items = [{ name: 'Members' }];
export const RoleCheckmark = styled(Checkmark)` export const RoleCheckmark = styled(Checkmark)`
padding-left: 4px; padding-left: 4px;
`; `;
@ -54,7 +263,7 @@ export const MiniProfileActionItem = styled.span<{ disabled?: boolean }>`
position: relative; position: relative;
text-decoration: none; text-decoration: none;
${props => ${(props) =>
props.disabled props.disabled
? css` ? css`
user-select: none; user-select: none;
@ -75,7 +284,7 @@ export const Content = styled.div`
export const CurrentPermission = styled.span` export const CurrentPermission = styled.span`
margin-left: 4px; margin-left: 4px;
color: ${props => mixin.rgba(props.theme.colors.text.secondary, 0.4)}; color: ${(props) => mixin.rgba(props.theme.colors.text.secondary, 0.4)};
`; `;
export const Separator = styled.div` export const Separator = styled.div`
@ -86,13 +295,13 @@ export const Separator = styled.div`
export const WarningText = styled.span` export const WarningText = styled.span`
display: flex; display: flex;
color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.4)}; color: ${(props) => mixin.rgba(props.theme.colors.text.primary, 0.4)};
padding: 6px; padding: 6px;
`; `;
export const DeleteDescription = styled.div` export const DeleteDescription = styled.div`
font-size: 14px; font-size: 14px;
color: ${props => props.theme.colors.text.primary}; color: ${(props) => props.theme.colors.text.primary};
`; `;
export const RemoveMemberButton = styled(Button)` export const RemoveMemberButton = styled(Button)`
@ -161,8 +370,8 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
<MiniProfileActions> <MiniProfileActions>
<MiniProfileActionWrapper> <MiniProfileActionWrapper>
{permissions {permissions
.filter(p => (user.role && user.role.code === 'owner') || p.code !== 'owner') .filter((p) => (user.role && user.role.code === 'owner') || p.code !== 'owner')
.map(perm => ( .map((perm) => (
<MiniProfileActionItem <MiniProfileActionItem
disabled={user.role && perm.code !== user.role.code && !canChangeRole} disabled={user.role && perm.code !== user.role.code && !canChangeRole}
key={perm.code} key={perm.code}
@ -213,9 +422,9 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
Choose a new user to take over ownership of the users teams & projects. Choose a new user to take over ownership of the users teams & projects.
</DeleteDescription> </DeleteDescription>
<UserSelect <UserSelect
onChange={v => setDeleteUser(v)} onChange={(v) => setDeleteUser(v)}
value={deleteUser} value={deleteUser}
options={users.map(u => ({ label: u.fullName, value: u.id }))} options={users.map((u) => ({ label: u.fullName, value: u.id }))}
/> />
</> </>
)} )}
@ -240,7 +449,7 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
Removing this user from the organzation will remove them from assigned tasks, projects, and teams. Removing this user from the organzation will remove them from assigned tasks, projects, and teams.
</DeleteDescription> </DeleteDescription>
<DeleteDescription>{`The user is the owner of ${user.owned.projects.length} projects & ${user.owned.teams.length} teams.`}</DeleteDescription> <DeleteDescription>{`The user is the owner of ${user.owned.projects.length} projects & ${user.owned.teams.length} teams.`}</DeleteDescription>
<UserSelect onChange={NOOP} value={null} options={users.map(u => ({ label: u.fullName, value: u.id }))} /> <UserSelect onChange={NOOP} value={null} options={users.map((u) => ({ label: u.fullName, value: u.id }))} />
<UserPassConfirmButton <UserPassConfirmButton
onClick={() => { onClick={() => {
// onDeleteUser(); // onDeleteUser();
@ -293,211 +502,6 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
); );
}; };
const UserSelect = styled(Select)`
margin: 8px 0;
padding: 8px 0;
`;
const NewUserPassInput = styled(Input)`
margin: 8px 0;
`;
const InviteMemberButton = styled(Button)`
padding: 7px 12px;
`;
const UserPassBar = styled.div`
display: flex;
padding-top: 8px;
`;
const UserPassConfirmButton = styled(Button)`
width: 100%;
padding: 7px 12px;
`;
const UserPassButton = styled(Button)`
width: 50%;
padding: 7px 12px;
& ~ & {
margin-left: 6px;
}
`;
const MemberItemOptions = styled.div``;
const MemberItemOption = styled(Button)`
padding: 7px 9px;
margin: 4px 0 4px 8px;
float: left;
min-width: 95px;
`;
const MemberList = styled.div`
border-top: 1px solid ${props => props.theme.colors.border};
`;
const MemberListItem = styled.div`
display: flex;
flex-flow: row wrap;
justify-content: space-between;
border-bottom: 1px solid ${props => props.theme.colors.border};
min-height: 40px;
padding: 12px 0 12px 40px;
position: relative;
`;
const MemberListItemDetails = styled.div`
float: left;
flex: 1 0 auto;
padding-left: 8px;
`;
const InviteIcon = styled(UserPlus)`
padding-right: 4px;
`;
const MemberProfile = styled(TaskAssignee)`
position: absolute;
top: 16px;
left: 0;
margin: 0;
`;
const MemberItemName = styled.p`
color: ${props => props.theme.colors.text.secondary};
`;
const MemberItemUsername = styled.p`
color: ${props => props.theme.colors.text.primary};
`;
const MemberListHeader = styled.div`
display: flex;
flex-direction: column;
`;
const ListTitle = styled.h3`
font-size: 18px;
color: ${props => props.theme.colors.text.secondary};
margin-bottom: 12px;
`;
const ListDesc = styled.span`
font-size: 16px;
color: ${props => props.theme.colors.text.primary};
`;
const FilterSearch = styled(Input)`
margin: 0;
`;
const ListActions = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 8px;
margin-bottom: 18px;
`;
const MemberListWrapper = styled.div`
flex: 1 1;
`;
const Container = styled.div`
padding: 2.2rem;
display: flex;
width: 100%;
max-width: 1400px;
position: relative;
margin: 0 auto;
`;
const TabNav = styled.div`
float: left;
width: 220px;
height: 100%;
display: block;
position: relative;
`;
const TabNavContent = styled.ul`
display: block;
width: auto;
border-bottom: 0 !important;
border-right: 1px solid rgba(0, 0, 0, 0.05);
`;
const TabNavItem = styled.li`
padding: 0.35rem 0.3rem;
height: 48px;
display: block;
position: relative;
`;
const TabNavItemButton = styled.button<{ active: boolean }>`
cursor: pointer;
display: flex;
align-items: center;
padding-top: 10px !important;
padding-bottom: 10px !important;
padding-left: 12px !important;
padding-right: 8px !important;
width: 100%;
position: relative;
color: ${props => (props.active ? `${props.theme.colors.secondary}` : props.theme.colors.text.primary)};
&:hover {
color: ${props => `${props.theme.colors.primary}`};
}
&:hover svg {
fill: ${props => props.theme.colors.primary};
}
`;
const TabItemUser = styled(User)<{ active: boolean }>`
fill: ${props => (props.active ? `${props.theme.colors.primary}` : props.theme.colors.text.primary)}
stroke: ${props => (props.active ? `${props.theme.colors.primary}` : props.theme.colors.text.primary)}
`;
const TabNavItemSpan = styled.span`
text-align: left;
padding-left: 9px;
font-size: 14px;
`;
const TabNavLine = styled.span<{ top: number }>`
left: auto;
right: 0;
width: 2px;
height: 48px;
transform: scaleX(1);
top: ${props => props.top}px;
background: linear-gradient(30deg, ${props => props.theme.colors.primary}, ${props => props.theme.colors.primary});
box-shadow: 0 0 8px 0 ${props => props.theme.colors.primary};
display: block;
position: absolute;
transition: all 0.2s ease;
`;
const TabContentWrapper = styled.div`
position: relative;
display: block;
overflow: hidden;
width: 100%;
margin-left: 1rem;
`;
const TabContent = styled.div`
position: relative;
width: 100%;
display: block;
padding: 0;
padding: 1.5rem;
background-color: #10163a;
border-radius: 0.5rem;
`;
const items = [{ name: 'Members' }];
type NavItemProps = { type NavItemProps = {
active: boolean; active: boolean;
name: string; name: string;
@ -591,7 +595,7 @@ const Admin: React.FC<AdminProps> = ({
<FilterSearch width="250px" variant="alternate" placeholder="Filter by name" /> <FilterSearch width="250px" variant="alternate" placeholder="Filter by name" />
{canInviteUser && ( {canInviteUser && (
<InviteMemberButton <InviteMemberButton
onClick={$target => { onClick={($target) => {
onAddUser($target); onAddUser($target);
}} }}
> >
@ -602,7 +606,7 @@ const Admin: React.FC<AdminProps> = ({
</ListActions> </ListActions>
</MemberListHeader> </MemberListHeader>
<MemberList> <MemberList>
{users.map(member => { {users.map((member) => {
const projectTotal = member.owned.projects.length + member.member.projects.length; const projectTotal = member.owned.projects.length + member.member.projects.length;
return ( return (
<MemberListItem> <MemberListItem>
@ -615,7 +619,7 @@ const Admin: React.FC<AdminProps> = ({
<MemberItemOption variant="flat">{`On ${projectTotal} projects`}</MemberItemOption> <MemberItemOption variant="flat">{`On ${projectTotal} projects`}</MemberItemOption>
<MemberItemOption <MemberItemOption
variant="outline" variant="outline"
onClick={$target => { onClick={($target) => {
showPopup( showPopup(
$target, $target,
<TeamRoleManagerPopup <TeamRoleManagerPopup
@ -626,7 +630,7 @@ const Admin: React.FC<AdminProps> = ({
onUpdateUserPassword(user, password); onUpdateUserPassword(user, password);
}} }}
canChangeRole={(member.role && member.role.code !== 'owner') ?? false} canChangeRole={(member.role && member.role.code !== 'owner') ?? false}
onChangeRole={roleCode => { onChangeRole={(roleCode) => {
updateUserRole({ variables: { userID: member.id, roleCode } }); updateUserRole({ variables: { userID: member.id, roleCode } });
}} }}
onDeleteUser={onDeleteUser} onDeleteUser={onDeleteUser}
@ -640,7 +644,7 @@ const Admin: React.FC<AdminProps> = ({
</MemberListItem> </MemberListItem>
); );
})} })}
{invitedUsers.map(member => { {invitedUsers.map((member) => {
return ( return (
<MemberListItem> <MemberListItem>
<MemberProfile <MemberProfile
@ -664,7 +668,7 @@ const Admin: React.FC<AdminProps> = ({
<MemberItemOptions> <MemberItemOptions>
<MemberItemOption <MemberItemOption
variant="outline" variant="outline"
onClick={$target => { onClick={($target) => {
showPopup( showPopup(
$target, $target,
<TeamRoleManagerPopup <TeamRoleManagerPopup

View File

@ -10,6 +10,7 @@ export const CardMember = styled(TaskAssignee)<{ zIndex: number }>`
z-index: ${props => props.zIndex}; z-index: ${props => props.zIndex};
position: relative; position: relative;
`; `;
export const ChecklistIcon = styled(CheckSquareOutline)<{ color: 'success' | 'normal' }>` export const ChecklistIcon = styled(CheckSquareOutline)<{ color: 'success' | 'normal' }>`
${props => ${props =>
props.color === 'success' && props.color === 'success' &&
@ -18,6 +19,7 @@ export const ChecklistIcon = styled(CheckSquareOutline)<{ color: 'success' | 'no
stroke: ${props.theme.colors.success}; stroke: ${props.theme.colors.success};
`} `}
`; `;
export const ClockIcon = styled(Clock)<{ color: string }>` export const ClockIcon = styled(Clock)<{ color: string }>`
fill: ${props => props.color}; fill: ${props => props.color};
`; `;
@ -26,7 +28,7 @@ export const EditorTextarea = styled(TextareaAutosize)`
overflow: hidden; overflow: hidden;
overflow-wrap: break-word; overflow-wrap: break-word;
resize: none; resize: none;
height: 90px; height: 54px;
width: 100%; width: 100%;
background: none; background: none;

View File

@ -58,12 +58,14 @@ type Props = {
onCardTitleChange?: (name: string) => void; onCardTitleChange?: (name: string) => void;
labelVariant?: CardLabelVariant; labelVariant?: CardLabelVariant;
toggleLabels?: boolean; toggleLabels?: boolean;
isPublic?: boolean;
toggleDirection?: 'shrink' | 'expand'; toggleDirection?: 'shrink' | 'expand';
}; };
const Card = React.forwardRef( const Card = React.forwardRef(
( (
{ {
isPublic = false,
wrapperProps, wrapperProps,
onContextMenu, onContextMenu,
taskID, taskID,
@ -120,9 +122,11 @@ const Card = React.forwardRef(
} }
}; };
const onTaskContext = (e: React.MouseEvent) => { const onTaskContext = (e: React.MouseEvent) => {
if (!isPublic) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
onOpenComposer(); onOpenComposer();
}
}; };
const onOperationClick = (e: React.MouseEvent<HTMLOrSVGElement>) => { const onOperationClick = (e: React.MouseEvent<HTMLOrSVGElement>) => {
e.preventDefault(); e.preventDefault();
@ -145,7 +149,7 @@ const Card = React.forwardRef(
{...wrapperProps} {...wrapperProps}
> >
<ListCardInnerContainer ref={$innerCardRef}> <ListCardInnerContainer ref={$innerCardRef}>
{isActive && !editable && ( {!isPublic && isActive && !editable && (
<ListCardOperation <ListCardOperation
onClick={e => { onClick={e => {
e.stopPropagation(); e.stopPropagation();

View File

@ -16,7 +16,7 @@ const InputWrapper = styled.div<{ width: string }>`
`; `;
const InputLabel = styled.span<{ width: string }>` const InputLabel = styled.span<{ width: string }>`
width: ${props => props.width}; width: ${(props) => props.width};
padding: 0.7rem !important; padding: 0.7rem !important;
color: #c2c6dc; color: #c2c6dc;
left: 0; left: 0;
@ -40,13 +40,13 @@ const InputInput = styled.input<{
focusBg: string; focusBg: string;
borderColor: string; borderColor: string;
}>` }>`
width: ${props => props.width}; width: ${(props) => props.width};
font-size: 14px; font-size: 14px;
border: 1px solid rgba(0, 0, 0, 0.2); border: 1px solid rgba(0, 0, 0, 0.2);
border-color: ${props => props.borderColor}; border-color: ${(props) => props.borderColor};
background: #262c49; background: #262c49;
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.15); box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.15);
${props => (props.hasIcon ? 'padding: 0.7rem 1rem 0.7rem 3rem;' : 'padding: 0.7rem;')} ${(props) => (props.hasIcon ? 'padding: 0.7rem 1rem 0.7rem 3rem;' : 'padding: 0.7rem;')}
line-height: 16px; line-height: 16px;
color: #c2c6dc; color: #c2c6dc;
position: relative; position: relative;
@ -55,13 +55,13 @@ const InputInput = styled.input<{
&:focus { &:focus {
box-shadow: 0 3px 10px 0 rgba(0, 0, 0, 0.15); box-shadow: 0 3px 10px 0 rgba(0, 0, 0, 0.15);
border: 1px solid rgba(115, 103, 240); border: 1px solid rgba(115, 103, 240);
background: ${props => props.focusBg}; background: ${(props) => props.focusBg};
} }
&:focus ~ ${InputLabel} { &:focus ~ ${InputLabel} {
color: ${props => props.theme.colors.primary}; color: ${(props) => props.theme.colors.primary};
transform: translate(-3px, -90%); transform: translate(-3px, -90%);
} }
${props => ${(props) =>
props.hasValue && props.hasValue &&
css` css`
& ~ ${InputLabel} { & ~ ${InputLabel} {
@ -94,11 +94,13 @@ type ControlledInputProps = {
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void; onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
value?: string; value?: string;
onClick?: (e: React.MouseEvent<HTMLInputElement>) => void; onClick?: (e: React.MouseEvent<HTMLInputElement>) => void;
disabled?: boolean;
}; };
const ControlledInput = ({ const ControlledInput = ({
width = 'auto', width = 'auto',
variant = 'normal', variant = 'normal',
disabled = false,
type = 'text', type = 'text',
autocomplete, autocomplete,
autoFocus = false, autoFocus = false,
@ -126,8 +128,9 @@ const ControlledInput = ({
return ( return (
<InputWrapper className={className} width={width}> <InputWrapper className={className} width={width}>
<InputInput <InputInput
disabled={disabled}
hasValue={hasValue} hasValue={hasValue}
onChange={e => { onChange={(e) => {
if (onChange) { if (onChange) {
setHasValue(e.currentTarget.value !== '' || floatingLabel); setHasValue(e.currentTarget.value !== '' || floatingLabel);
onChange(e); onChange(e);

View File

@ -7,6 +7,8 @@ import 'react-datepicker/dist/react-datepicker.css';
import { getYear, getMonth } from 'date-fns'; import { getYear, getMonth } from 'date-fns';
import { useForm, Controller } from 'react-hook-form'; import { useForm, Controller } from 'react-hook-form';
import NOOP from 'shared/utils/noop'; import NOOP from 'shared/utils/noop';
import { Clock, Cross } from 'shared/icons';
import Select from 'react-select/src/Select';
import { import {
Wrapper, Wrapper,
@ -23,8 +25,6 @@ import {
ActionClock, ActionClock,
ActionLabel, ActionLabel,
} from './Styles'; } from './Styles';
import { Clock, Cross } from 'shared/icons';
import Select from 'react-select/src/Select';
type DueDateManagerProps = { type DueDateManagerProps = {
task: Task; task: Task;
@ -59,7 +59,7 @@ const HeaderSelectLabel = styled.div`
color: #c2c6dc; color: #c2c6dc;
&:hover { &:hover {
background: ${props => props.theme.colors.primary}; background: ${(props) => props.theme.colors.primary};
color: #c2c6dc; color: #c2c6dc;
} }
`; `;
@ -78,12 +78,12 @@ const HeaderSelect = styled.select`
& option { & option {
color: #c2c6dc; color: #c2c6dc;
background: ${props => props.theme.colors.bg.primary}; background: ${(props) => props.theme.colors.bg.primary};
} }
& option:hover { & option:hover {
background: ${props => props.theme.colors.bg.secondary}; background: ${(props) => props.theme.colors.bg.secondary};
border: 1px solid ${props => props.theme.colors.primary}; border: 1px solid ${(props) => props.theme.colors.primary};
outline: none !important; outline: none !important;
box-shadow: none; box-shadow: none;
color: #c2c6dc; color: #c2c6dc;
@ -115,7 +115,7 @@ const HeaderButton = styled.button`
border: none; border: none;
border-radius: 3px; border-radius: 3px;
&:hover { &:hover {
background: ${props => props.theme.colors.primary}; background: ${(props) => props.theme.colors.primary};
color: #fff; color: #fff;
} }
`; `;
@ -133,7 +133,14 @@ const HeaderActions = styled.div`
const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange, onRemoveDueDate, onCancel }) => { const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange, onRemoveDueDate, onCancel }) => {
const currentDueDate = task.dueDate ? dayjs(task.dueDate).toDate() : null; const currentDueDate = task.dueDate ? dayjs(task.dueDate).toDate() : null;
const { register, handleSubmit, errors, setValue, setError, formState, control } = useForm<DueDateFormData>(); const {
register,
handleSubmit,
setValue,
setError,
formState: { errors },
control,
} = useForm<DueDateFormData>();
const [startDate, setStartDate] = useState<Date | null>(currentDueDate); const [startDate, setStartDate] = useState<Date | null>(currentDueDate);
const [endDate, setEndDate] = useState<Date | null>(currentDueDate); const [endDate, setEndDate] = useState<Date | null>(currentDueDate);
@ -183,27 +190,16 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
}; };
const [isRange, setIsRange] = useState(false); const [isRange, setIsRange] = useState(false);
const CustomTimeInput = forwardRef(({ value, onClick, onChange, onBlur, onFocus }: any, $ref: any) => {
return (
<DueDateInput
id="endTime"
value={value}
name="endTime"
onChange={onChange}
width="100%"
variant="alternate"
label="Time"
onClick={onClick}
/>
);
});
return ( return (
<Wrapper> <Wrapper>
<DateRangeInputs> <DateRangeInputs>
<DatePicker <DatePicker
selected={startDate} selected={startDate}
onChange={date => setStartDate(date)} onChange={(date) => {
if (!Array.isArray(date)) {
setStartDate(date);
}
}}
popperClassName="picker-hidden" popperClassName="picker-hidden"
dateFormat="yyyy-MM-dd" dateFormat="yyyy-MM-dd"
disabledKeyboardNavigation disabledKeyboardNavigation
@ -214,7 +210,11 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
<DatePicker <DatePicker
selected={startDate} selected={startDate}
isClearable isClearable
onChange={date => setStartDate(date)} onChange={(date) => {
if (!Array.isArray(date)) {
setStartDate(date);
}
}}
popperClassName="picker-hidden" popperClassName="picker-hidden"
dateFormat="yyyy-MM-dd" dateFormat="yyyy-MM-dd"
placeholderText="Select from date" placeholderText="Select from date"
@ -225,7 +225,11 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
</DateRangeInputs> </DateRangeInputs>
<DatePicker <DatePicker
selected={startDate} selected={startDate}
onChange={date => setStartDate(date)} onChange={(date) => {
if (!Array.isArray(date)) {
setStartDate(date);
}
}}
startDate={startDate} startDate={startDate}
useWeekdaysShort useWeekdaysShort
renderCustomHeader={({ renderCustomHeader={({
@ -247,7 +251,7 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
value={months[getMonth(date)]} value={months[getMonth(date)]}
onChange={({ target: { value } }) => changeMonth(months.indexOf(value))} onChange={({ target: { value } }) => changeMonth(months.indexOf(value))}
> >
{months.map(option => ( {months.map((option) => (
<option key={option} value={option}> <option key={option} value={option}>
{option} {option}
</option> </option>
@ -257,7 +261,7 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
<HeaderSelectLabel> <HeaderSelectLabel>
{date.getFullYear()} {date.getFullYear()}
<HeaderSelect value={getYear(date)} onChange={({ target: { value } }) => changeYear(parseInt(value, 10))}> <HeaderSelect value={getYear(date)} onChange={({ target: { value } }) => changeYear(parseInt(value, 10))}>
{years.map(option => ( {years.map((option) => (
<option key={option} value={option}> <option key={option} value={option}>
{option} {option}
</option> </option>
@ -279,8 +283,10 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
<ActionLabel>Due Time</ActionLabel> <ActionLabel>Due Time</ActionLabel>
<DatePicker <DatePicker
selected={startDate} selected={startDate}
onChange={date => { onChange={(date) => {
if (!Array.isArray(date)) {
setStartDate(date); setStartDate(date);
}
}} }}
showTimeSelect showTimeSelect
showTimeSelectOnly showTimeSelectOnly

View File

@ -72,6 +72,9 @@ export const HeaderName = styled(TextareaAutosize)`
box-shadow: none; box-shadow: none;
font-weight: 600; font-weight: 600;
margin: -4px 0; margin: -4px 0;
&:disabled {
opacity: 1;
}
letter-spacing: normal; letter-spacing: normal;
word-spacing: normal; word-spacing: normal;

View File

@ -24,6 +24,7 @@ type Props = {
onOpenComposer: (id: string) => void; onOpenComposer: (id: string) => void;
wrapperProps?: any; wrapperProps?: any;
headerProps?: any; headerProps?: any;
isPublic: boolean;
index?: number; index?: number;
onExtraMenuOpen: (taskGroupID: string, $targetRef: React.RefObject<HTMLElement>) => void; onExtraMenuOpen: (taskGroupID: string, $targetRef: React.RefObject<HTMLElement>) => void;
}; };
@ -37,6 +38,7 @@ const List = React.forwardRef(
isComposerOpen, isComposerOpen,
onOpenComposer, onOpenComposer,
children, children,
isPublic,
wrapperProps, wrapperProps,
headerProps, headerProps,
onExtraMenuOpen, onExtraMenuOpen,
@ -86,39 +88,37 @@ const List = React.forwardRef(
<Container ref={$wrapperRef} {...wrapperProps}> <Container ref={$wrapperRef} {...wrapperProps}>
<Wrapper> <Wrapper>
<Header {...headerProps} isEditing={isEditingTitle}> <Header {...headerProps} isEditing={isEditingTitle}>
<HeaderEditTarget onClick={onClick} isHidden={isEditingTitle} /> {!isPublic && <HeaderEditTarget onClick={onClick} isHidden={isEditingTitle} />}
<HeaderName <HeaderName
ref={$listNameRef} ref={$listNameRef}
disabled={isPublic}
onBlur={onBlur} onBlur={onBlur}
onChange={onChange} onChange={onChange}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
spellCheck={false} spellCheck={false}
value={listName} value={listName}
/> />
{!isPublic && (
<ListExtraMenuButtonWrapper ref={$extraActionsRef} onClick={handleExtraMenuOpen}> <ListExtraMenuButtonWrapper ref={$extraActionsRef} onClick={handleExtraMenuOpen}>
<Ellipsis size={16} color="#c2c6dc" /> <Ellipsis vertical={false} size={16} color="#c2c6dc" />
</ListExtraMenuButtonWrapper> </ListExtraMenuButtonWrapper>
)}
</Header> </Header>
{children && children} {children && children}
{!isPublic && (
<AddCardContainer hidden={isComposerOpen}> <AddCardContainer hidden={isComposerOpen}>
<AddCardButton onClick={() => onOpenComposer(id)}> <AddCardButton onClick={() => onOpenComposer(id)}>
<Plus width={12} height={12} /> <Plus width={12} height={12} />
<AddCardButtonText>Add another card</AddCardButtonText> <AddCardButtonText>Add another card</AddCardButtonText>
</AddCardButton> </AddCardButton>
</AddCardContainer> </AddCardContainer>
)}
</Wrapper> </Wrapper>
</Container> </Container>
); );
}, },
); );
List.defaultProps = {
children: null,
isComposerOpen: false,
wrapperProps: {},
headerProps: {},
};
List.displayName = 'List'; List.displayName = 'List';
export default List; export default List;

View File

@ -151,6 +151,7 @@ interface SimpleProps {
onCardMemberClick: OnCardMemberClick; onCardMemberClick: OnCardMemberClick;
onCardLabelClick: () => void; onCardLabelClick: () => void;
cardLabelVariant: CardLabelVariant; cardLabelVariant: CardLabelVariant;
isPublic?: boolean;
taskStatusFilter?: TaskStatusFilter; taskStatusFilter?: TaskStatusFilter;
taskMetaFilters?: TaskMetaFilters; taskMetaFilters?: TaskMetaFilters;
taskSorting?: TaskSorting; taskSorting?: TaskSorting;
@ -188,6 +189,7 @@ const SimpleLists: React.FC<SimpleProps> = ({
onExtraMenuOpen, onExtraMenuOpen,
onCardMemberClick, onCardMemberClick,
taskStatusFilter = initTaskStatusFilter, taskStatusFilter = initTaskStatusFilter,
isPublic = false,
taskMetaFilters = initTaskMetaFilters, taskMetaFilters = initTaskMetaFilters,
taskSorting = initTaskSorting, taskSorting = initTaskSorting,
}) => { }) => {
@ -300,6 +302,7 @@ const SimpleLists: React.FC<SimpleProps> = ({
onOpenComposer={id => setCurrentComposer(id)} onOpenComposer={id => setCurrentComposer(id)}
isComposerOpen={currentComposer === taskGroup.id} isComposerOpen={currentComposer === taskGroup.id}
onSaveName={name => onChangeTaskGroupName(taskGroup.id, name)} onSaveName={name => onChangeTaskGroupName(taskGroup.id, name)}
isPublic={isPublic}
ref={columnDragProvided.innerRef} ref={columnDragProvided.innerRef}
wrapperProps={columnDragProvided.draggableProps} wrapperProps={columnDragProvided.draggableProps}
headerProps={columnDragProvided.dragHandleProps} headerProps={columnDragProvided.dragHandleProps}
@ -328,6 +331,7 @@ const SimpleLists: React.FC<SimpleProps> = ({
<Card <Card
toggleDirection={toggleDirection} toggleDirection={toggleDirection}
toggleLabels={toggleLabels} toggleLabels={toggleLabels}
isPublic={isPublic}
labelVariant={cardLabelVariant} labelVariant={cardLabelVariant}
wrapperProps={{ wrapperProps={{
...taskProvided.draggableProps, ...taskProvided.draggableProps,
@ -396,11 +400,13 @@ const SimpleLists: React.FC<SimpleProps> = ({
)} )}
</Droppable> </Droppable>
</DragDropContext> </DragDropContext>
{!isPublic && (
<AddList <AddList
onSave={listName => { onSave={listName => {
onCreateTaskGroup(listName); onCreateTaskGroup(listName);
}} }}
/> />
)}
</BoardWrapper> </BoardWrapper>
</BoardContainer> </BoardContainer>
); );

View File

@ -25,7 +25,12 @@ import {
const Login = ({ onSubmit }: LoginProps) => { const Login = ({ onSubmit }: LoginProps) => {
const [isComplete, setComplete] = useState(true); const [isComplete, setComplete] = useState(true);
const { register, handleSubmit, errors, setError, formState } = useForm<LoginFormData>(); const {
register,
handleSubmit,
setError,
formState: { errors },
} = useForm<LoginFormData>();
const loginSubmit = (data: LoginFormData) => { const loginSubmit = (data: LoginFormData) => {
setComplete(false); setComplete(false);
onSubmit(data, setComplete, setError); onSubmit(data, setComplete, setError);
@ -47,12 +52,7 @@ const Login = ({ onSubmit }: LoginProps) => {
<Form onSubmit={handleSubmit(loginSubmit)}> <Form onSubmit={handleSubmit(loginSubmit)}>
<FormLabel htmlFor="username"> <FormLabel htmlFor="username">
Username Username
<FormTextInput <FormTextInput type="text" {...register('username', { required: 'Username is required' })} />
type="text"
id="username"
name="username"
ref={register({ required: 'Username is required' })}
/>
<FormIcon> <FormIcon>
<User width={20} height={20} /> <User width={20} height={20} />
</FormIcon> </FormIcon>
@ -60,12 +60,7 @@ const Login = ({ onSubmit }: LoginProps) => {
{errors.username && <FormError>{errors.username.message}</FormError>} {errors.username && <FormError>{errors.username.message}</FormError>}
<FormLabel htmlFor="password"> <FormLabel htmlFor="password">
Password Password
<FormTextInput <FormTextInput type="password" {...register('password', { required: 'Password is required' })} />
type="password"
id="password"
name="password"
ref={register({ required: 'Password is required' })}
/>
<FormIcon> <FormIcon>
<Lock width={20} height={20} /> <Lock width={20} height={20} />
</FormIcon> </FormIcon>

View File

@ -98,8 +98,8 @@ const ProjectName = styled.input`
font-weight: 400; font-weight: 400;
&:focus { &:focus {
background: ${props => mixin.darken(props.theme.colors.bg.secondary, 0.15)}; background: ${(props) => mixin.darken(props.theme.colors.bg.secondary, 0.15)};
box-shadow: ${props => props.theme.colors.primary} 0px 0px 0px 1px; box-shadow: ${(props) => props.theme.colors.primary} 0px 0px 0px 1px;
} }
`; `;
const ProjectNameLabel = styled.label` const ProjectNameLabel = styled.label`
@ -210,8 +210,8 @@ const CreateButton = styled.button`
&:hover { &:hover {
color: #fff; color: #fff;
background: ${props => props.theme.colors.primary}; background: ${(props) => props.theme.colors.primary};
border-color: ${props => props.theme.colors.primary}; border-color: ${(props) => props.theme.colors.primary};
} }
`; `;
type NewProjectProps = { type NewProjectProps = {
@ -224,7 +224,7 @@ type NewProjectProps = {
const NewProject: React.FC<NewProjectProps> = ({ initialTeamID, teams, onClose, onCreateProject }) => { const NewProject: React.FC<NewProjectProps> = ({ initialTeamID, teams, onClose, onCreateProject }) => {
const [projectName, setProjectName] = useState(''); const [projectName, setProjectName] = useState('');
const [team, setTeam] = useState<null | string>(initialTeamID); const [team, setTeam] = useState<null | string>(initialTeamID);
const options = [{ label: 'No team', value: 'no-team' }, ...teams.map(t => ({ label: t.name, value: t.id }))]; const options = [{ label: 'No team', value: 'no-team' }, ...teams.map((t) => ({ label: t.name, value: t.id }))];
return ( return (
<Overlay> <Overlay>
<Content> <Content>
@ -234,7 +234,7 @@ const NewProject: React.FC<NewProjectProps> = ({ initialTeamID, teams, onClose,
onClose(); onClose();
}} }}
> >
<ArrowLeft color="#c2c6dc" /> <ArrowLeft width={16} height={16} color="#c2c6dc" />
</HeaderLeft> </HeaderLeft>
<HeaderRight <HeaderRight
onClick={() => { onClick={() => {
@ -263,7 +263,7 @@ const NewProject: React.FC<NewProjectProps> = ({ initialTeamID, teams, onClose,
onChange={(e: any) => { onChange={(e: any) => {
setTeam(e.value); setTeam(e.value);
}} }}
value={options.find(d => d.value === team)} value={options.find((d) => d.value === team)}
styles={colourStyles} styles={colourStyles}
classNamePrefix="teamSelect" classNamePrefix="teamSelect"
options={options} options={options}

View File

@ -218,7 +218,7 @@ export const PopupProvider: React.FC = ({ children }) => {
const setTab = (newTab: number, options?: PopupOptions) => { const setTab = (newTab: number, options?: PopupOptions) => {
setState((prevState: PopupState) => setState((prevState: PopupState) =>
produce(prevState, draftState => { produce(prevState, (draftState) => {
draftState.previousTab = currentState.currentTab; draftState.previousTab = currentState.currentTab;
draftState.currentTab = newTab; draftState.currentTab = newTab;
if (options) { if (options) {
@ -296,7 +296,7 @@ const PopupMenu: React.FC<Props> = ({ width, title, top, left, onClose, noHeader
<Wrapper padding borders> <Wrapper padding borders>
{onPrevious && ( {onPrevious && (
<PreviousButton onClick={onPrevious}> <PreviousButton onClick={onPrevious}>
<AngleLeft color="#c2c6dc" /> <AngleLeft size={16} color="#c2c6dc" />
</PreviousButton> </PreviousButton>
)} )}
{noHeader ? ( {noHeader ? (
@ -332,7 +332,7 @@ export const Popup: React.FC<PopupProps> = ({ borders = true, padding = true, ti
setTab(0); setTab(0);
}} }}
> >
<AngleLeft color="#c2c6dc" /> <AngleLeft size={16} color="#c2c6dc" />
</PreviousButton> </PreviousButton>
)} )}
{title && ( {title && (

View File

@ -2,15 +2,15 @@ import React, { useRef } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
export const Container = styled.div<{ size: number | string; bgColor: string | null; backgroundURL: string | null }>` export const Container = styled.div<{ size: number | string; bgColor: string | null; backgroundURL: string | null }>`
width: ${props => props.size}px; width: ${(props) => props.size}px;
height: ${props => props.size}px; height: ${(props) => props.size}px;
border-radius: 9999px; border-radius: 9999px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: #fff; color: #fff;
font-weight: 700; font-weight: 700;
background: ${props => (props.backgroundURL ? `url(${props.backgroundURL})` : props.bgColor)}; background: ${(props) => (props.backgroundURL ? `url(${props.backgroundURL})` : props.bgColor)};
background-position: center; background-position: center;
background-size: contain; background-size: contain;
`; `;
@ -22,6 +22,10 @@ type ProfileIconProps = {
}; };
const ProfileIcon: React.FC<ProfileIconProps> = ({ user, onProfileClick, size }) => { const ProfileIcon: React.FC<ProfileIconProps> = ({ user, onProfileClick, size }) => {
let realSize = size;
if (size === null) {
realSize = 28;
}
const $profileRef = useRef<HTMLDivElement>(null); const $profileRef = useRef<HTMLDivElement>(null);
return ( return (
<Container <Container
@ -29,7 +33,7 @@ const ProfileIcon: React.FC<ProfileIconProps> = ({ user, onProfileClick, size })
onClick={() => { onClick={() => {
onProfileClick($profileRef, user); onProfileClick($profileRef, user);
}} }}
size={size} size={realSize}
backgroundURL={user.profileIcon.url ?? null} backgroundURL={user.profileIcon.url ?? null}
bgColor={user.profileIcon.bgColor ?? null} bgColor={user.profileIcon.bgColor ?? null}
> >
@ -38,8 +42,4 @@ const ProfileIcon: React.FC<ProfileIconProps> = ({ user, onProfileClick, size })
); );
}; };
ProfileIcon.defaultProps = {
size: 28,
};
export default ProfileIcon; export default ProfileIcon;

View File

@ -38,12 +38,17 @@ export const ListSeparator = styled.hr`
`; `;
type Props = { type Props = {
publicOn: null | string;
onDeleteProject: () => void; onDeleteProject: () => void;
onToggleProjectVisible: (visible: boolean) => void;
}; };
const ProjectSettings: React.FC<Props> = ({ onDeleteProject }) => { const ProjectSettings: React.FC<Props> = ({ publicOn, onDeleteProject, onToggleProjectVisible }) => {
return ( return (
<> <>
<ListActionsWrapper> <ListActionsWrapper>
<ListActionItemWrapper onClick={() => onToggleProjectVisible(publicOn === null)}>
<ListActionItem>{`Make ${publicOn === null ? 'public' : 'private'}`}</ListActionItem>
</ListActionItemWrapper>
<ListActionItemWrapper onClick={() => onDeleteProject()}> <ListActionItemWrapper onClick={() => onDeleteProject()}>
<ListActionItem>Delete Project</ListActionItem> <ListActionItem>Delete Project</ListActionItem>
</ListActionItemWrapper> </ListActionItemWrapper>
@ -127,5 +132,18 @@ const DeleteConfirm: React.FC<DeleteConfirmProps> = ({ description, deletedItems
); );
}; };
export { DeleteConfirm }; type PublicConfirmProps = {
onConfirm: () => void;
};
const PublicConfirm: React.FC<PublicConfirmProps> = ({ onConfirm }) => {
return (
<ConfirmWrapper>
<ConfirmDescription>Public projects can be accessed by anyone with a link to the project.</ConfirmDescription>
<ConfirmDeleteButton onClick={() => onConfirm()}>Make public</ConfirmDeleteButton>
</ConfirmWrapper>
);
};
export { DeleteConfirm, PublicConfirm };
export default ProjectSettings; export default ProjectSettings;

View File

@ -26,7 +26,12 @@ const INITIALS_PATTERN = /[a-zA-Z]{2,3}/i;
const Register = ({ onSubmit, registered = false }: RegisterProps) => { const Register = ({ onSubmit, registered = false }: RegisterProps) => {
const [isComplete, setComplete] = useState(true); const [isComplete, setComplete] = useState(true);
const { register, handleSubmit, errors, setError } = useForm<RegisterFormData>(); const {
register,
handleSubmit,
formState: { errors },
setError,
} = useForm<RegisterFormData>();
const loginSubmit = (data: RegisterFormData) => { const loginSubmit = (data: RegisterFormData) => {
setComplete(false); setComplete(false);
onSubmit(data, setComplete, setError); onSubmit(data, setComplete, setError);
@ -55,12 +60,7 @@ const Register = ({ onSubmit, registered = false }: RegisterProps) => {
<Form onSubmit={handleSubmit(loginSubmit)}> <Form onSubmit={handleSubmit(loginSubmit)}>
<FormLabel htmlFor="fullname"> <FormLabel htmlFor="fullname">
Full name Full name
<FormTextInput <FormTextInput type="text" {...register('fullname', { required: 'Full name is required' })} />
type="text"
id="fullname"
name="fullname"
ref={register({ required: 'Full name is required' })}
/>
<FormIcon> <FormIcon>
<User width={20} height={20} /> <User width={20} height={20} />
</FormIcon> </FormIcon>
@ -68,12 +68,7 @@ const Register = ({ onSubmit, registered = false }: RegisterProps) => {
{errors.username && <FormError>{errors.username.message}</FormError>} {errors.username && <FormError>{errors.username.message}</FormError>}
<FormLabel htmlFor="username"> <FormLabel htmlFor="username">
Username Username
<FormTextInput <FormTextInput type="text" {...register('username', { required: 'Username is required' })} />
type="text"
id="username"
name="username"
ref={register({ required: 'Username is required' })}
/>
<FormIcon> <FormIcon>
<User width={20} height={20} /> <User width={20} height={20} />
</FormIcon> </FormIcon>
@ -83,9 +78,7 @@ const Register = ({ onSubmit, registered = false }: RegisterProps) => {
Email Email
<FormTextInput <FormTextInput
type="text" type="text"
id="email" {...register('email', {
name="email"
ref={register({
required: 'Email is required', required: 'Email is required',
pattern: { value: EMAIL_PATTERN, message: 'Must be a valid email' }, pattern: { value: EMAIL_PATTERN, message: 'Must be a valid email' },
})} })}
@ -99,9 +92,7 @@ const Register = ({ onSubmit, registered = false }: RegisterProps) => {
Initials Initials
<FormTextInput <FormTextInput
type="text" type="text"
id="initials" {...register('initials', {
name="initials"
ref={register({
required: 'Initials is required', required: 'Initials is required',
pattern: { pattern: {
value: INITIALS_PATTERN, value: INITIALS_PATTERN,
@ -116,12 +107,7 @@ const Register = ({ onSubmit, registered = false }: RegisterProps) => {
{errors.initials && <FormError>{errors.initials.message}</FormError>} {errors.initials && <FormError>{errors.initials.message}</FormError>}
<FormLabel htmlFor="password"> <FormLabel htmlFor="password">
Password Password
<FormTextInput <FormTextInput type="password" {...register('password', { required: 'Password is required' })} />
type="password"
id="password"
name="password"
ref={register({ required: 'Password is required' })}
/>
<FormIcon> <FormIcon>
<Lock width={20} height={20} /> <Lock width={20} height={20} />
</FormIcon> </FormIcon>
@ -131,9 +117,7 @@ const Register = ({ onSubmit, registered = false }: RegisterProps) => {
Password (Confirm) Password (Confirm)
<FormTextInput <FormTextInput
type="password" type="password"
id="password_confirm" {...register('password_confirm', { required: 'Password (confirm) is required' })}
name="password_confirm"
ref={register({ required: 'Password (confirm) is required' })}
/> />
<FormIcon> <FormIcon>
<Lock width={20} height={20} /> <Lock width={20} height={20} />

View File

@ -1,23 +1,23 @@
import React, { useState, useRef } from 'react'; import React, { useState, useRef } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { User } from 'shared/icons'; import { User } from 'shared/icons';
import Input from 'shared/components/Input';
import Button from 'shared/components/Button'; import Button from 'shared/components/Button';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import ControlledInput from 'shared/components/ControlledInput';
const PasswordInput = styled(Input)` const PasswordInput = styled(ControlledInput)`
margin-top: 30px; margin-top: 30px;
margin-bottom: 0; margin-bottom: 0;
`; `;
const UserInfoInput = styled(Input)` const UserInfoInput = styled(ControlledInput)`
margin-top: 30px; margin-top: 30px;
margin-bottom: 0; margin-bottom: 0;
`; `;
const FormError = styled.span` const FormError = styled.span`
font-size: 12px; font-size: 12px;
color: ${props => props.theme.colors.warning}; color: ${(props) => props.theme.colors.warning};
`; `;
const ProfileContainer = styled.div` const ProfileContainer = styled.div`
@ -42,7 +42,7 @@ const AvatarMask = styled.div<{ background: string }>`
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
background: ${props => props.background}; background: ${(props) => props.background};
border-radius: 50%; border-radius: 50%;
display: flex; display: flex;
align-items: center; align-items: center;
@ -152,12 +152,12 @@ const TabNavItemButton = styled.button<{ active: boolean }>`
width: 100%; width: 100%;
position: relative; position: relative;
color: ${props => (props.active ? `${props.theme.colors.primary}` : '#c2c6dc')}; color: ${(props) => (props.active ? `${props.theme.colors.primary}` : '#c2c6dc')};
&:hover { &:hover {
color: ${props => props.theme.colors.primary}; color: ${(props) => props.theme.colors.primary};
} }
&:hover svg { &:hover svg {
fill: ${props => props.theme.colors.primary}; fill: ${(props) => props.theme.colors.primary};
} }
`; `;
@ -173,10 +173,14 @@ const TabNavLine = styled.span<{ top: number }>`
width: 2px; width: 2px;
height: 48px; height: 48px;
transform: scaleX(1); transform: scaleX(1);
top: ${props => props.top}px; top: ${(props) => props.top}px;
background: linear-gradient(30deg, ${props => props.theme.colors.primary}, ${props => props.theme.colors.primary}); background: linear-gradient(
box-shadow: 0 0 8px 0 ${props => props.theme.colors.primary}; 30deg,
${(props) => props.theme.colors.primary},
${(props) => props.theme.colors.primary}
);
box-shadow: 0 0 8px 0 ${(props) => props.theme.colors.primary};
display: block; display: block;
position: absolute; position: absolute;
transition: all 0.2s ease; transition: all 0.2s ease;
@ -267,36 +271,36 @@ type ResetPasswordTabProps = {
}; };
const ResetPasswordTab: React.FC<ResetPasswordTabProps> = ({ onResetPassword }) => { const ResetPasswordTab: React.FC<ResetPasswordTabProps> = ({ onResetPassword }) => {
const [active, setActive] = useState(true); const [active, setActive] = useState(true);
const { register, handleSubmit, errors, setError, reset } = useForm<{ password: string; password_confirm: string }>(); const {
register,
handleSubmit,
formState: { errors },
setError,
reset,
} = useForm<{ password: string; passwordConfirm: string }>();
const done = () => { const done = () => {
reset(); reset();
setActive(true); setActive(true);
}; };
return ( return (
<form <form
onSubmit={handleSubmit(data => { onSubmit={handleSubmit((data) => {
if (data.password !== data.password_confirm) { if (data.password !== data.passwordConfirm) {
setError('password', { message: 'Passwords must match!', type: 'error' }); setError('password', { message: 'Passwords must match!', type: 'error' });
setError('password_confirm', { message: 'Passwords must match!', type: 'error' }); setError('passwordConfirm', { message: 'Passwords must match!', type: 'error' });
} else { } else {
onResetPassword(data.password, done); onResetPassword(data.password, done);
} }
})} })}
> >
<PasswordInput <PasswordInput width="100%" {...register('password', { required: 'Password is required' })} label="Password" />
width="100%"
ref={register({ required: 'Password is required' })}
label="Password"
name="password"
/>
{errors.password && <FormError>{errors.password.message}</FormError>} {errors.password && <FormError>{errors.password.message}</FormError>}
<PasswordInput <PasswordInput
width="100%" width="100%"
ref={register({ required: 'Password is required' })} {...register('passwordConfirm', { required: 'Password is required' })}
label="Password (confirm)" label="Password (confirm)"
name="password_confirm"
/> />
{errors.password_confirm && <FormError>{errors.password_confirm.message}</FormError>} {errors.passwordConfirm && <FormError>{errors.passwordConfirm.message}</FormError>}
<SettingActions> <SettingActions>
<SaveButton disabled={!active} type="submit"> <SaveButton disabled={!active} type="submit">
Save Change Save Change
@ -307,7 +311,7 @@ const ResetPasswordTab: React.FC<ResetPasswordTabProps> = ({ onResetPassword })
}; };
type UserInfoData = { type UserInfoData = {
full_name: string; fullName: string;
bio: string; bio: string;
initials: string; initials: string;
email: string; email: string;
@ -329,7 +333,11 @@ const UserInfoTab: React.FC<UserInfoTabProps> = ({
onChangeUserInfo, onChangeUserInfo,
}) => { }) => {
const [active, setActive] = useState(true); const [active, setActive] = useState(true);
const { register, handleSubmit, errors } = useForm<UserInfoData>(); const {
register,
handleSubmit,
formState: { errors },
} = useForm<UserInfoData>();
const done = () => { const done = () => {
setActive(true); setActive(true);
}; };
@ -341,26 +349,24 @@ const UserInfoTab: React.FC<UserInfoTabProps> = ({
profile={profile.profileIcon} profile={profile.profileIcon}
/> />
<form <form
onSubmit={handleSubmit(data => { onSubmit={handleSubmit((data) => {
setActive(false); setActive(false);
onChangeUserInfo(data, done); onChangeUserInfo(data, done);
})} })}
> >
<UserInfoInput <UserInfoInput
ref={register({ required: 'Full name is required' })} {...register('fullName', { required: 'Full name is required' })}
name="full_name"
defaultValue={profile.fullName} defaultValue={profile.fullName}
width="100%" width="100%"
label="Name" label="Name"
/> />
{errors.full_name && <FormError>{errors.full_name.message}</FormError>} {errors.fullName && <FormError>{errors.fullName.message}</FormError>}
<UserInfoInput <UserInfoInput
defaultValue={profile.profileIcon && profile.profileIcon.initials ? profile.profileIcon.initials : ''} defaultValue={profile.profileIcon && profile.profileIcon.initials ? profile.profileIcon.initials : ''}
ref={register({ {...register('initials', {
required: 'Initials is required', required: 'Initials is required',
pattern: { value: INITIALS_PATTERN, message: 'Intials must be between two to four characters' }, pattern: { value: INITIALS_PATTERN, message: 'Intials must be between two to four characters' },
})} })}
name="initials"
width="100%" width="100%"
label="Initials " label="Initials "
/> />
@ -368,8 +374,7 @@ const UserInfoTab: React.FC<UserInfoTabProps> = ({
<UserInfoInput disabled defaultValue={profile.username ?? ''} width="100%" label="Username " /> <UserInfoInput disabled defaultValue={profile.username ?? ''} width="100%" label="Username " />
<UserInfoInput <UserInfoInput
width="100%" width="100%"
name="email" {...register('email', {
ref={register({
required: 'Email is required', required: 'Email is required',
pattern: { value: EMAIL_PATTERN, message: 'Must be a valid email' }, pattern: { value: EMAIL_PATTERN, message: 'Must be a valid email' },
})} })}
@ -377,7 +382,7 @@ const UserInfoTab: React.FC<UserInfoTabProps> = ({
label="Email" label="Email"
/> />
{errors.email && <FormError>{errors.email.message}</FormError>} {errors.email && <FormError>{errors.email.message}</FormError>}
<UserInfoInput width="100%" name="bio" ref={register()} defaultValue={profile.bio ?? ''} label="Bio" /> <UserInfoInput width="100%" {...register('bio')} defaultValue={profile.bio ?? ''} label="Bio" />
{errors.bio && <FormError>{errors.bio.message}</FormError>} {errors.bio && <FormError>{errors.bio.message}</FormError>}
<SettingActions> <SettingActions>
<SaveButton disabled={!active} type="submit"> <SaveButton disabled={!active} type="submit">

View File

@ -69,7 +69,7 @@ const CommentCreator: React.FC<CommentCreatorProps> = ({
)} )}
<CommentEditorContainer> <CommentEditorContainer>
<CommentTextArea <CommentTextArea
showCommentActions={showCommentActions} $showCommentActions={showCommentActions}
placeholder="Write a comment..." placeholder="Write a comment..."
ref={$comment} ref={$comment}
disabled={disabled} disabled={disabled}

View File

@ -13,6 +13,7 @@ import {
Smile, Smile,
} from 'shared/icons'; } from 'shared/icons';
import { toArray } from 'react-emoji-render'; import { toArray } from 'react-emoji-render';
import { useCurrentUser } from 'App/context';
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import TaskAssignee from 'shared/components/TaskAssignee'; import TaskAssignee from 'shared/components/TaskAssignee';
import useOnOutsideClick from 'shared/hooks/onOutsideClick'; import useOnOutsideClick from 'shared/hooks/onOutsideClick';
@ -81,29 +82,29 @@ import {
ActivityItemComment, ActivityItemComment,
} from './Styles'; } from './Styles';
type TaskDetailsProps = {}; const TaskDetailsLoading: React.FC = () => {
const { user } = useCurrentUser();
const TaskDetailsLoading: React.FC<TaskDetailsProps> = () => {
return ( return (
<Container> <Container>
<LeftSidebar> <LeftSidebar>
<LeftSidebarContent> <LeftSidebarContent>
<LeftSidebarSection> <LeftSidebarSection>
<SidebarTitle>TASK GROUP</SidebarTitle> <SidebarTitle>TASK GROUP</SidebarTitle>
<SidebarButton loading> <SidebarButton $loading>
<SidebarSkeleton /> <SidebarSkeleton />
</SidebarButton> </SidebarButton>
<DueDateTitle>DUE DATE</DueDateTitle> <DueDateTitle>DUE DATE</DueDateTitle>
<SidebarButton loading> <SidebarButton $loading>
<SidebarSkeleton /> <SidebarSkeleton />
</SidebarButton> </SidebarButton>
</LeftSidebarSection> </LeftSidebarSection>
<AssignedUsersSection> <AssignedUsersSection>
<DueDateTitle>MEMBERS</DueDateTitle> <DueDateTitle>MEMBERS</DueDateTitle>
<SidebarButton loading> <SidebarButton $loading>
<SidebarSkeleton /> <SidebarSkeleton />
</SidebarButton> </SidebarButton>
</AssignedUsersSection> </AssignedUsersSection>
{user && (
<ExtraActionsSection> <ExtraActionsSection>
<DueDateTitle>ACTIONS</DueDateTitle> <DueDateTitle>ACTIONS</DueDateTitle>
<ActionButton disabled icon={<Tags width={12} height={12} />}> <ActionButton disabled icon={<Tags width={12} height={12} />}>
@ -114,6 +115,7 @@ const TaskDetailsLoading: React.FC<TaskDetailsProps> = () => {
</ActionButton> </ActionButton>
<ActionButton disabled>Cover</ActionButton> <ActionButton disabled>Cover</ActionButton>
</ExtraActionsSection> </ExtraActionsSection>
)}
</LeftSidebarContent> </LeftSidebarContent>
</LeftSidebar> </LeftSidebar>
<ContentContainer> <ContentContainer>
@ -125,6 +127,7 @@ const TaskDetailsLoading: React.FC<TaskDetailsProps> = () => {
<span>Mark complete</span> <span>Mark complete</span>
</MarkCompleteButton> </MarkCompleteButton>
</HeaderLeft> </HeaderLeft>
{user && (
<HeaderRight> <HeaderRight>
<HeaderActionIcon> <HeaderActionIcon>
<Paperclip width={16} height={16} /> <Paperclip width={16} height={16} />
@ -139,9 +142,10 @@ const TaskDetailsLoading: React.FC<TaskDetailsProps> = () => {
<Trash width={16} height={16} /> <Trash width={16} height={16} />
</HeaderActionIcon> </HeaderActionIcon>
</HeaderRight> </HeaderRight>
)}
</HeaderInnerContainer> </HeaderInnerContainer>
<TaskDetailsTitleWrapper loading> <TaskDetailsTitleWrapper $loading>
<TaskDetailsTitle value="" disabled loading /> <TaskDetailsTitle value="" disabled $loading />
</TaskDetailsTitleWrapper> </TaskDetailsTitleWrapper>
</HeaderContainer> </HeaderContainer>
<InnerContentContainer> <InnerContentContainer>
@ -151,9 +155,11 @@ const TaskDetailsLoading: React.FC<TaskDetailsProps> = () => {
</TabBarSection> </TabBarSection>
<ActivitySection /> <ActivitySection />
</InnerContentContainer> </InnerContentContainer>
{user && (
<CommentContainer> <CommentContainer>
<CommentCreator disabled onCreateComment={() => null} onMemberProfile={() => null} /> <CommentCreator disabled onCreateComment={() => null} onMemberProfile={() => null} />
</CommentContainer> </CommentContainer>
)}
</ContentContainer> </ContentContainer>
</Container> </Container>
); );

View File

@ -108,7 +108,7 @@ export const skeletonKeyframes = keyframes`
} }
`; `;
export const SidebarButton = styled.div<{ loading?: boolean }>` export const SidebarButton = styled.div<{ $loading?: boolean }>`
font-size: 14px; font-size: 14px;
color: ${props => props.theme.colors.text.primary}; color: ${props => props.theme.colors.text.primary};
min-height: 32px; min-height: 32px;
@ -116,7 +116,7 @@ export const SidebarButton = styled.div<{ loading?: boolean }>`
border-radius: 6px; border-radius: 6px;
${props => ${props =>
props.loading props.$loading
? css` ? css`
background: ${props.theme.colors.bg.primary}; background: ${props.theme.colors.bg.primary};
` `
@ -178,15 +178,15 @@ export const HeaderLeft = styled.div`
justify-content: flex-start; justify-content: flex-start;
`; `;
export const TaskDetailsTitleWrapper = styled.div<{ loading?: boolean }>` export const TaskDetailsTitleWrapper = styled.div<{ $loading?: boolean }>`
width: 100%; width: 100%;
margin: 8px 0 4px 0; margin: 8px 0 4px 0;
display: flex; display: flex;
border-radius: 6px; border-radius: 6px;
${props => props.loading && `background: ${props.theme.colors.bg.primary};`} ${props => props.$loading && `background: ${props.theme.colors.bg.primary};`}
`; `;
export const TaskDetailsTitle = styled(TextareaAutosize)<{ loading?: boolean }>` export const TaskDetailsTitle = styled(TextareaAutosize)<{ $loading?: boolean }>`
padding: 9px 8px 7px 8px; padding: 9px 8px 7px 8px;
border-color: transparent; border-color: transparent;
border-radius: 6px; border-radius: 6px;
@ -198,8 +198,11 @@ export const TaskDetailsTitle = styled(TextareaAutosize)<{ loading?: boolean }>`
font-weight: 700; font-weight: 700;
background: none; background: none;
&:disabled {
opacity: 1;
}
${props => ${props =>
props.loading props.$loading
? css` ? css`
background-image: linear-gradient(90deg, ${defaultBaseColor}, ${defaultHighlightColor}, ${defaultBaseColor}); background-image: linear-gradient(90deg, ${defaultBaseColor}, ${defaultHighlightColor}, ${defaultBaseColor});
background-size: 200px 100%; background-size: 200px 100%;
@ -207,7 +210,7 @@ export const TaskDetailsTitle = styled(TextareaAutosize)<{ loading?: boolean }>`
animation: ${skeletonKeyframes} 1.2s ease-in-out infinite; animation: ${skeletonKeyframes} 1.2s ease-in-out infinite;
` `
: css` : css`
&:hover { &:not(:disabled):hover {
border-color: #414561; border-color: #414561;
border-width: 1px; border-width: 1px;
border-style: solid; border-style: solid;
@ -534,7 +537,7 @@ export const CommentProfile = styled(TaskAssignee)`
align-items: normal; align-items: normal;
`; `;
export const CommentTextArea = styled(TextareaAutosize)<{ showCommentActions: boolean }>` export const CommentTextArea = styled(TextareaAutosize)<{ $showCommentActions: boolean }>`
width: 100%; width: 100%;
line-height: 28px; line-height: 28px;
padding: 4px 6px; padding: 4px 6px;
@ -546,7 +549,7 @@ export const CommentTextArea = styled(TextareaAutosize)<{ showCommentActions: bo
min-height: 36px; min-height: 36px;
max-height: 36px; max-height: 36px;
${props => ${props =>
props.showCommentActions props.$showCommentActions
? css` ? css`
min-height: 80px; min-height: 80px;
max-height: none; max-height: none;

View File

@ -1,4 +1,5 @@
import React, { useState, useRef } from 'react'; import React, { useState, useRef } from 'react';
import { useCurrentUser } from 'App/context';
import { import {
Plus, Plus,
User, User,
@ -81,7 +82,7 @@ import {
} from './Styles'; } from './Styles';
import Checklist, { ChecklistItem, ChecklistItems } from '../Checklist'; import Checklist, { ChecklistItem, ChecklistItems } from '../Checklist';
import onDragEnd from './onDragEnd'; import onDragEnd from './onDragEnd';
import { plugin as em } from './remark'; import plugin from './remark';
import ActivityMessage from './ActivityMessage'; import ActivityMessage from './ActivityMessage';
const parseEmojis = (value: string) => { const parseEmojis = (value: string) => {
@ -135,7 +136,7 @@ const StreamComment: React.FC<StreamCommentProps> = ({
onCreateComment={onUpdateComment} onCreateComment={onUpdateComment}
/> />
) : ( ) : (
<ReactMarkdown escapeHtml={false} plugins={[em]}> <ReactMarkdown skipHtml plugins={[plugin]}>
{DOMPurify.sanitize(comment.message, { FORBID_TAGS: ['style', 'img'] })} {DOMPurify.sanitize(comment.message, { FORBID_TAGS: ['style', 'img'] })}
</ReactMarkdown> </ReactMarkdown>
)} )}
@ -277,6 +278,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
onToggleChecklistItem, onToggleChecklistItem,
onMemberProfile, onMemberProfile,
}) => { }) => {
const { user } = useCurrentUser();
const [taskName, setTaskName] = useState(task.name); const [taskName, setTaskName] = useState(task.name);
const [editTaskDescription, setEditTaskDescription] = useState(() => { const [editTaskDescription, setEditTaskDescription] = useState(() => {
if (task.description) { if (task.description) {
@ -298,7 +300,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
const activityStream: Array<{ id: string; data: { time: string; type: 'comment' | 'activity' } }> = []; const activityStream: Array<{ id: string; data: { time: string; type: 'comment' | 'activity' } }> = [];
if (task.activity) { if (task.activity) {
task.activity.forEach(activity => { task.activity.forEach((activity) => {
activityStream.push({ activityStream.push({
id: activity.id, id: activity.id,
data: { data: {
@ -310,7 +312,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
} }
if (task.comments) { if (task.comments) {
task.comments.forEach(comment => { task.comments.forEach((comment) => {
activityStream.push({ activityStream.push({
id: comment.id, id: comment.id,
data: { data: {
@ -338,7 +340,9 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
<SidebarButton <SidebarButton
ref={$dueDateBtn} ref={$dueDateBtn}
onClick={() => { onClick={() => {
if (user) {
onOpenDueDatePopop(task, $dueDateBtn); onOpenDueDatePopop(task, $dueDateBtn);
}
}} }}
> >
{task.dueDate ? ( {task.dueDate ? (
@ -354,20 +358,24 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
<DueDateTitle>MEMBERS</DueDateTitle> <DueDateTitle>MEMBERS</DueDateTitle>
{task.assigned && task.assigned.length !== 0 ? ( {task.assigned && task.assigned.length !== 0 ? (
<MemberList> <MemberList>
{task.assigned.map(m => ( {task.assigned.map((m) => (
<TaskMember <TaskMember
key={m.id} key={m.id}
member={m} member={m}
size={32} size={32}
onMemberProfile={$target => { onMemberProfile={($target) => {
if (user) {
onMemberProfile($target, m.id); onMemberProfile($target, m.id);
}
}} }}
/> />
))} ))}
<AssignUserIcon <AssignUserIcon
ref={$addMemberBtn} ref={$addMemberBtn}
onClick={() => { onClick={() => {
if (user) {
onOpenAddMemberPopup(task, $addMemberBtn); onOpenAddMemberPopup(task, $addMemberBtn);
}
}} }}
> >
<Plus width={16} height={16} /> <Plus width={16} height={16} />
@ -377,7 +385,9 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
<AssignUsersButton <AssignUsersButton
ref={$noMemberBtn} ref={$noMemberBtn}
onClick={() => { onClick={() => {
if (user) {
onOpenAddMemberPopup(task, $noMemberBtn); onOpenAddMemberPopup(task, $noMemberBtn);
}
}} }}
> >
<AssignUserIcon> <AssignUserIcon>
@ -387,10 +397,11 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
</AssignUsersButton> </AssignUsersButton>
)} )}
</AssignedUsersSection> </AssignedUsersSection>
{user && (
<ExtraActionsSection> <ExtraActionsSection>
<DueDateTitle>ACTIONS</DueDateTitle> <DueDateTitle>ACTIONS</DueDateTitle>
<ActionButton <ActionButton
onClick={$target => { onClick={($target) => {
onOpenAddLabelPopup(task, $target); onOpenAddLabelPopup(task, $target);
}} }}
icon={<Tags width={12} height={12} />} icon={<Tags width={12} height={12} />}
@ -398,7 +409,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
Labels Labels
</ActionButton> </ActionButton>
<ActionButton <ActionButton
onClick={$target => { onClick={($target) => {
onOpenAddChecklistPopup(task, $target); onOpenAddChecklistPopup(task, $target);
}} }}
icon={<CheckSquareOutline width={12} height={12} />} icon={<CheckSquareOutline width={12} height={12} />}
@ -407,6 +418,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
</ActionButton> </ActionButton>
<ActionButton>Cover</ActionButton> <ActionButton>Cover</ActionButton>
</ExtraActionsSection> </ExtraActionsSection>
)}
</LeftSidebarContent> </LeftSidebarContent>
</LeftSidebar> </LeftSidebar>
<ContentContainer> <ContentContainer>
@ -414,15 +426,19 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
<HeaderInnerContainer> <HeaderInnerContainer>
<HeaderLeft> <HeaderLeft>
<MarkCompleteButton <MarkCompleteButton
disabled={user === null}
invert={task.complete ?? false} invert={task.complete ?? false}
onClick={() => { onClick={() => {
if (user) {
onToggleTaskComplete(task); onToggleTaskComplete(task);
}
}} }}
> >
<Checkmark width={8} height={8} /> <Checkmark width={8} height={8} />
<span>{task.complete ? 'Completed' : 'Mark complete'}</span> <span>{task.complete ? 'Completed' : 'Mark complete'}</span>
</MarkCompleteButton> </MarkCompleteButton>
</HeaderLeft> </HeaderLeft>
{user && (
<HeaderRight> <HeaderRight>
<HeaderActionIcon> <HeaderActionIcon>
<Paperclip width={16} height={16} /> <Paperclip width={16} height={16} />
@ -437,12 +453,14 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
<Trash width={16} height={16} /> <Trash width={16} height={16} />
</HeaderActionIcon> </HeaderActionIcon>
</HeaderRight> </HeaderRight>
)}
</HeaderInnerContainer> </HeaderInnerContainer>
<TaskDetailsTitleWrapper> <TaskDetailsTitleWrapper>
<TaskDetailsTitle <TaskDetailsTitle
value={taskName} value={taskName}
ref={$detailsTitle} ref={$detailsTitle}
onKeyDown={e => { disabled={user === null}
onKeyDown={(e) => {
if (e.keyCode === 13) { if (e.keyCode === 13) {
e.preventDefault(); e.preventDefault();
if ($detailsTitle && $detailsTitle.current) { if ($detailsTitle && $detailsTitle.current) {
@ -450,7 +468,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
} }
} }
}} }}
onChange={e => { onChange={(e) => {
setTaskName(e.currentTarget.value); setTaskName(e.currentTarget.value);
}} }}
onBlur={() => { onBlur={() => {
@ -463,12 +481,12 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
<Labels> <Labels>
{task.labels.length !== 0 && ( {task.labels.length !== 0 && (
<MetaDetailContent> <MetaDetailContent>
{task.labels.map(label => { {task.labels.map((label) => {
return ( return (
<TaskLabelItem <TaskLabelItem
key={label.projectLabel.id} key={label.projectLabel.id}
label={label} label={label}
onClick={$target => { onClick={($target) => {
onOpenAddLabelPopup(task, $target); onOpenAddLabelPopup(task, $target);
}} }}
/> />
@ -487,7 +505,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
<TaskDetailsEditor value={taskDescriptionRef.current} /> <TaskDetailsEditor value={taskDescriptionRef.current} />
) : ( ) : (
<EditorContainer <EditorContainer
onClick={e => { onClick={(e) => {
if (!editTaskDescription) { if (!editTaskDescription) {
setEditTaskDescription(true); setEditTaskDescription(true);
} }
@ -495,10 +513,10 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
> >
<Editor <Editor
defaultValue={task.description ?? ''} defaultValue={task.description ?? ''}
readOnly={user === null || !editTaskDescription}
theme={dark} theme={dark}
readOnly={!editTaskDescription}
autoFocus autoFocus
onChange={value => { onChange={(value) => {
setSaveTimeout(() => { setSaveTimeout(() => {
clearTimeout(saveTimeout); clearTimeout(saveTimeout);
return setTimeout(saveDescription, 2000); return setTimeout(saveDescription, 2000);
@ -513,9 +531,9 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
<ViewRawButton onClick={() => setShowRaw(!showRaw)}>{showRaw ? 'Show editor' : 'Show raw'}</ViewRawButton> <ViewRawButton onClick={() => setShowRaw(!showRaw)}>{showRaw ? 'Show editor' : 'Show raw'}</ViewRawButton>
</DescriptionContainer> </DescriptionContainer>
<ChecklistSection> <ChecklistSection>
<DragDropContext onDragEnd={result => onDragEnd(result, task, onChecklistDrop, onChecklistItemDrop)}> <DragDropContext onDragEnd={(result) => onDragEnd(result, task, onChecklistDrop, onChecklistItemDrop)}>
<Droppable direction="vertical" type="checklist" droppableId="root"> <Droppable direction="vertical" type="checklist" droppableId="root">
{dropProvided => ( {(dropProvided) => (
<ChecklistContainer {...dropProvided.droppableProps} ref={dropProvided.innerRef}> <ChecklistContainer {...dropProvided.droppableProps} ref={dropProvided.innerRef}>
{task.checklists && {task.checklists &&
task.checklists task.checklists
@ -523,7 +541,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
.sort((a, b) => a.position - b.position) .sort((a, b) => a.position - b.position)
.map((checklist, idx) => ( .map((checklist, idx) => (
<Draggable key={checklist.id} draggableId={checklist.id} index={idx}> <Draggable key={checklist.id} draggableId={checklist.id} index={idx}>
{provided => ( {(provided) => (
<Checklist <Checklist
ref={provided.innerRef} ref={provided.innerRef}
wrapperProps={provided.draggableProps} wrapperProps={provided.draggableProps}
@ -533,10 +551,10 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
checklistID={checklist.id} checklistID={checklist.id}
items={checklist.items} items={checklist.items}
onDeleteChecklist={onDeleteChecklist} onDeleteChecklist={onDeleteChecklist}
onChangeName={newName => onChangeChecklistName(checklist.id, newName)} onChangeName={(newName) => onChangeChecklistName(checklist.id, newName)}
onToggleItem={onToggleChecklistItem} onToggleItem={onToggleChecklistItem}
onDeleteItem={onDeleteItem} onDeleteItem={onDeleteItem}
onAddItem={n => { onAddItem={(n) => {
if (task.checklists) { if (task.checklists) {
let position = 65535; let position = 65535;
const [lastItem] = checklist.items const [lastItem] = checklist.items
@ -551,7 +569,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
onChangeItemName={onChangeItemName} onChangeItemName={onChangeItemName}
> >
<Droppable direction="vertical" type="checklistItem" droppableId={checklist.id}> <Droppable direction="vertical" type="checklistItem" droppableId={checklist.id}>
{checklistDrop => ( {(checklistDrop) => (
<> <>
<ChecklistItems ref={checklistDrop.innerRef} {...checklistDrop.droppableProps}> <ChecklistItems ref={checklistDrop.innerRef} {...checklistDrop.droppableProps}>
{checklist.items {checklist.items
@ -559,7 +577,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
.sort((a, b) => a.position - b.position) .sort((a, b) => a.position - b.position)
.map((item, itemIdx) => ( .map((item, itemIdx) => (
<Draggable key={item.id} draggableId={item.id} index={itemIdx}> <Draggable key={item.id} draggableId={item.id} index={itemIdx}>
{itemDrop => ( {(itemDrop) => (
<ChecklistItem <ChecklistItem
key={item.id} key={item.id}
itemID={item.id} itemID={item.id}
@ -597,30 +615,32 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
<TabBarItem>Activity</TabBarItem> <TabBarItem>Activity</TabBarItem>
</TabBarSection> </TabBarSection>
<ActivitySection> <ActivitySection>
{activityStream.map(stream => {activityStream.map((stream) =>
stream.data.type === 'comment' ? ( stream.data.type === 'comment' ? (
<StreamComment <StreamComment
onExtraActions={onCommentShowActions} onExtraActions={onCommentShowActions}
onCancelCommentEdit={onCancelCommentEdit} onCancelCommentEdit={onCancelCommentEdit}
onUpdateComment={message => onUpdateComment(stream.id, message)} onUpdateComment={(message) => onUpdateComment(stream.id, message)}
editable={stream.id === editableComment} editable={stream.id === editableComment}
comment={task.comments && task.comments.find(comment => comment.id === stream.id)} comment={task.comments && task.comments.find((comment) => comment.id === stream.id)}
/> />
) : ( ) : (
<StreamActivity activity={task.activity && task.activity.find(activity => activity.id === stream.id)} /> <StreamActivity
activity={task.activity && task.activity.find((activity) => activity.id === stream.id)}
/>
), ),
)} )}
</ActivitySection> </ActivitySection>
</InnerContentContainer> </InnerContentContainer>
<CommentContainer>
{me && ( {me && (
<CommentContainer>
<CommentCreator <CommentCreator
me={me} me={me}
onCreateComment={message => onCreateComment(task, message)} onCreateComment={(message) => onCreateComment(task, message)}
onMemberProfile={onMemberProfile} onMemberProfile={onMemberProfile}
/> />
)}
</CommentContainer> </CommentContainer>
)}
</ContentContainer> </ContentContainer>
</Container> </Container>
); );

View File

@ -1,6 +1,6 @@
import visit from 'unist-util-visit'; import { visit } from 'unist-util-visit';
import emoji from 'node-emoji'; import emoji from 'node-emoji';
import emoticon from 'emoticon'; import { emoticon } from 'emoticon';
import { Emoji } from 'emoji-mart'; import { Emoji } from 'emoji-mart';
import React from 'react'; import React from 'react';
@ -15,17 +15,17 @@ const DEFAULT_SETTINGS = {
}; };
function plugin(options) { function plugin(options) {
const settings = Object.assign({}, DEFAULT_SETTINGS, options); const settings = { ...DEFAULT_SETTINGS, ...options };
const pad = !!settings.padSpaceAfter; const pad = !!settings.padSpaceAfter;
const emoticonEnable = !!settings.emoticon; const emoticonEnable = !!settings.emoticon;
function getEmojiByShortCode(match) { function getEmojiByShortCode(match) {
// find emoji by shortcode - full match or with-out last char as it could be from text e.g. :-), // find emoji by shortcode - full match or with-out last char as it could be from text e.g. :-),
const iconFull = emoticon.find(e => e.emoticons.includes(match)); // full match const iconFull = emoticon.find((e) => e.emoticons.includes(match)); // full match
const iconPart = emoticon.find(e => e.emoticons.includes(match.slice(0, -1))); // second search pattern const iconPart = emoticon.find((e) => e.emoticons.includes(match.slice(0, -1))); // second search pattern
const trimmedChar = iconPart ? match.slice(-1) : ''; const trimmedChar = iconPart ? match.slice(-1) : '';
const addPad = pad ? ' ' : ''; const addPad = pad ? ' ' : '';
let icon = iconFull ? iconFull.emoji + addPad : iconPart && iconPart.emoji + addPad + trimmedChar; const icon = iconFull ? iconFull.emoji + addPad : iconPart && iconPart.emoji + addPad + trimmedChar;
return icon || match; return icon || match;
} }
@ -33,7 +33,7 @@ function plugin(options) {
console.log(match); console.log(match);
const got = emoji.get(match); const got = emoji.get(match);
if (pad && got !== match) { if (pad && got !== match) {
return got + ' '; return `${got} `;
} }
console.log(got); console.log(got);
@ -41,7 +41,7 @@ function plugin(options) {
} }
function transformer(tree) { function transformer(tree) {
visit(tree, 'paragraph', function(node) { visit(tree, 'paragraph', function (node) {
console.log(tree); console.log(tree);
// node.value = node.value.replace(RE_EMOJI, getEmoji); // node.value = node.value.replace(RE_EMOJI, getEmoji);
// jnode.type = 'html'; // jnode.type = 'html';
@ -65,4 +65,4 @@ function plugin(options) {
return transformer; return transformer;
} }
export { plugin }; export default plugin;

View File

@ -0,0 +1,69 @@
import React, { useRef, useState, useEffect } from 'react';
import { Home, Star, Bell, AngleDown, BarChart, CheckCircle, ListUnordered } from 'shared/icons';
import { Link } from 'react-router-dom';
import { RoleCode } from 'shared/generated/graphql';
import * as S from './Styles';
export type MenuItem = {
name: string;
link: string;
};
type NavBarProps = {
menuType?: Array<MenuItem> | null;
name: string | null;
match: string;
};
const NavBar: React.FC<NavBarProps> = ({ menuType, name, match }) => {
return (
<S.NavbarWrapper>
<S.NavbarHeader>
<S.ProjectActions>
<S.ProjectSwitch>
<S.ProjectSwitchInner>
<S.TaskcafeLogo innerColor="#9f46e4" outerColor="#000" width={32} height={32} />
</S.ProjectSwitchInner>
</S.ProjectSwitch>
<S.ProjectInfo>
<S.ProjectMeta>{name && <S.ProjectName>{name}</S.ProjectName>}</S.ProjectMeta>
{name && (
<S.ProjectTabs>
{menuType &&
menuType.map((menu, idx) => {
return (
<S.ProjectTab
key={menu.name}
to={menu.link}
exact
onClick={() => {
// TODO
}}
>
{menu.name}
</S.ProjectTab>
);
})}
</S.ProjectTabs>
)}
</S.ProjectInfo>
</S.ProjectActions>
<S.LogoContainer to="/">
<S.TaskcafeTitle>Taskcafé</S.TaskcafeTitle>
</S.LogoContainer>
<S.GlobalActions>
<Link
to={{
pathname: '/login',
state: { redirect: match },
}}
>
<S.SignIn>Sign In</S.SignIn>
</Link>
</S.GlobalActions>
</S.NavbarHeader>
</S.NavbarWrapper>
);
};
export default NavBar;

View File

@ -150,7 +150,7 @@ export const ProfileIcon = styled.div<{
export const ProjectMeta = styled.div<{ nameOnly?: boolean }>` export const ProjectMeta = styled.div<{ nameOnly?: boolean }>`
display: flex; display: flex;
${props => !props.nameOnly && 'padding-top: 9px;'} ${props => !props.nameOnly && 'padding-top: 9px;'}
margin-left: -14px; margin-left: -6px;
align-items: center; align-items: center;
max-width: 100%; max-width: 100%;
min-height: 51px; min-height: 51px;
@ -297,6 +297,16 @@ export const ProjectFinder = styled(Button)`
padding: 6px 12px; padding: 6px 12px;
`; `;
export const SignUp = styled(Button)`
margin-right: 8px;
padding: 6px 12px;
`;
export const SignIn = styled(Button)`
margin-right: 20px;
padding: 6px 12px;
`;
export const NavSeparator = styled.div` export const NavSeparator = styled.div`
width: 1px; width: 1px;
background: ${props => props.theme.colors.border}; background: ${props => props.theme.colors.border};

View File

@ -144,7 +144,7 @@ const ProjectHeading: React.FC<ProjectHeadingProps> = ({
</ProjectSettingsButton> </ProjectSettingsButton>
{onFavorite && ( {onFavorite && (
<ProjectSettingsButton onClick={() => onFavorite()}> <ProjectSettingsButton onClick={() => onFavorite()}>
<Star width={16} height={16} color="#c2c6dc" /> <Star filled width={16} height={16} color="#c2c6dc" />
</ProjectSettingsButton> </ProjectSettingsButton>
)} )}
</> </>
@ -228,7 +228,7 @@ const NavBar: React.FC<NavBarProps> = ({
<NavbarWrapper> <NavbarWrapper>
<NavbarHeader> <NavbarHeader>
<ProjectActions> <ProjectActions>
<ProjectSwitch ref={$finder} onClick={e => onOpenProjectFinder($finder)}> <ProjectSwitch ref={$finder} onClick={(e) => onOpenProjectFinder($finder)}>
<ProjectSwitchInner> <ProjectSwitchInner>
<TaskcafeLogo innerColor="#9f46e4" outerColor="#000" width={32} height={32} /> <TaskcafeLogo innerColor="#9f46e4" outerColor="#000" width={32} height={32} />
</ProjectSwitchInner> </ProjectSwitchInner>
@ -304,7 +304,7 @@ const NavBar: React.FC<NavBarProps> = ({
))} ))}
{canInviteUser && ( {canInviteUser && (
<InviteButton <InviteButton
onClick={$target => { onClick={($target) => {
if (onInviteUser) { if (onInviteUser) {
onInviteUser($target); onInviteUser($target);
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,7 @@ const FIND_PROJECT_QUERY = gql`
query findProject($projectID: UUID!) { query findProject($projectID: UUID!) {
findProject(input: { projectID: $projectID }) { findProject(input: { projectID: $projectID }) {
name name
publicOn
team { team {
id id
} }

View File

@ -0,0 +1,26 @@
import gql from 'graphql-tag';
import TASK_FRAGMENT from './fragments/task';
const FIND_PROJECT_QUERY = gql`
query labels($projectID: UUID!) {
findProject(input: { projectID: $projectID }) {
labels {
id
createdDate
name
labelColor {
id
name
colorHex
position
}
}
}
labelColors {
id
position
colorHex
name
}
}
`;

View File

@ -0,0 +1,14 @@
import gql from 'graphql-tag';
export const DELETE_PROJECT_MUTATION = gql`
mutation toggleProjectVisibility($projectID: UUID!, $isPublic: Boolean!) {
toggleProjectVisibility(input: { projectID: $projectID, isPublic: $isPublic }) {
project {
id
publicOn
}
}
}
`;
export default DELETE_PROJECT_MUTATION;

View File

@ -0,0 +1,13 @@
import React from 'react';
const useStateWithLocalStorage = (localStorageKey: string): [string, React.Dispatch<React.SetStateAction<string>>] => {
const [value, setValue] = React.useState<string>(localStorage.getItem(localStorageKey) || '');
React.useEffect(() => {
localStorage.setItem(localStorageKey, value);
}, [value]);
return [value, setValue];
};
export default useStateWithLocalStorage;

View File

@ -16,9 +16,4 @@ const AngleLeft = ({ size, color }: Props) => {
); );
}; };
AngleLeft.defaultProps = {
size: 16,
color: '#000',
};
export default AngleLeft; export default AngleLeft;

View File

@ -17,10 +17,4 @@ const ArrowLeft = ({ width, height, color }: Props) => {
); );
}; };
ArrowLeft.defaultProps = {
width: 16,
height: 16,
color: '#000',
};
export default ArrowLeft; export default ArrowLeft;

View File

@ -13,9 +13,4 @@ const Bell = ({ size, color }: Props) => {
); );
}; };
Bell.defaultProps = {
size: 16,
color: '#000',
};
export default Bell; export default Bell;

View File

@ -14,9 +14,4 @@ const Bin = ({ size, color }: Props) => {
); );
}; };
Bin.defaultProps = {
size: 16,
color: '#000',
};
export default Bin; export default Bin;

View File

@ -16,9 +16,4 @@ const Cog = ({ size, color }: Props) => {
); );
}; };
Cog.defaultProps = {
size: 16,
color: '#000',
};
export default Cog; export default Cog;

View File

@ -21,10 +21,4 @@ const Ellipsis = ({ size, color, vertical }: Props) => {
); );
}; };
Ellipsis.defaultProps = {
size: 16,
color: '#000',
vertical: false,
};
export default Ellipsis; export default Ellipsis;

View File

@ -13,9 +13,4 @@ const Exit = ({ size, color }: Props) => {
); );
}; };
Exit.defaultProps = {
size: 16,
color: '#000',
};
export default Exit; export default Exit;

View File

@ -13,9 +13,4 @@ const Question = ({ size, color }: Props) => {
); );
}; };
Question.defaultProps = {
size: 16,
color: '#000',
};
export default Question; export default Question;

View File

@ -13,9 +13,4 @@ const Stack = ({ size, color }: Props) => {
); );
}; };
Stack.defaultProps = {
size: 16,
color: '#000',
};
export default Stack; export default Stack;

View File

@ -25,11 +25,4 @@ const Star = ({ width, height, color, filled }: Props) => {
); );
}; };
Star.defaultProps = {
width: 24,
height: 16,
color: '#000',
filled: false,
};
export default Star; export default Star;

View File

@ -14,9 +14,4 @@ const Users = ({ size, color }: Props) => {
); );
}; };
Users.defaultProps = {
size: 16,
color: '#000',
};
export default Users; export default Users;

View File

@ -1,18 +0,0 @@
let accessToken = '';
export function setAccessToken(newToken: string) {
console.log(newToken);
accessToken = newToken;
}
export function getAccessToken() {
return accessToken;
}
export async function getNewToken() {
return fetch('/auth/refresh_token', {
method: 'POST',
credentials: 'include',
}).then(x => {
return x.json();
});
}

View File

@ -7,7 +7,7 @@ export function updateApolloCache<T>(
client: DataProxy, client: DataProxy,
document: DocumentNode, document: DocumentNode,
update: UpdateCacheFn<T>, update: UpdateCacheFn<T>,
variables?: object, variables?: any,
) { ) {
let queryArgs: DataProxy.Query<any, any>; let queryArgs: DataProxy.Query<any, any>;
if (variables) { if (variables) {

View File

@ -1,14 +1,15 @@
import theme from 'App/ThemeStyles'; import theme from 'App/ThemeStyles';
const colors = { const colors = {
almostBlack: '#181A1B', almostBlack: 'rgb(38, 44, 73)',
lightBlack: '#2F3336', lightBlack: 'rgb(16, 22, 58)',
almostWhite: '#E6E6E6', bgPrimary: 'rgb(16, 22, 58)',
almostWhite: 'rgb(194, 198, 220)',
white: '#FFF', white: '#FFF',
white10: 'rgba(255, 255, 255, 0.1)', white10: 'rgb(194, 198, 220)',
black: '#000', black: '#000',
black10: 'rgba(0, 0, 0, 0.1)', black10: 'rgba(0, 0, 0, 0.1)',
primary: '#1AB6FF', primary: 'rgb(115, 103, 240)',
greyLight: '#F4F7FA', greyLight: '#F4F7FA',
grey: '#E8EBED', grey: '#E8EBED',
greyMid: '#C5CCD3', greyMid: '#C5CCD3',
@ -17,15 +18,16 @@ const colors = {
export const base = { export const base = {
...colors, ...colors,
fontFamily: "'Droid Sans', sans-serif", fontFamily: 'Open Sans',
fontFamilyMono: "'SFMono-Regular',Consolas,'Liberation Mono', Menlo, Courier,monospace", fontFamilyMono: "'SFMono-Regular',Consolas,'Liberation Mono', Menlo, Courier,monospace",
fontWeight: 400, fontWeight: 400,
zIndex: 10000, zIndex: 1000000,
link: colors.primary, link: colors.primary,
placeholder: '#B1BECC', placeholder: '#B1BECC',
textSecondary: '#4E5C6E', textSecondary: '#fff',
textLight: colors.white, textLight: colors.white,
textHighlight: '#b3e7ff', textHighlight: '#b3e7ff',
textHighlightForeground: colors.white,
selected: colors.primary, selected: colors.primary,
codeComment: '#6a737d', codeComment: '#6a737d',
codePunctuation: '#5e6687', codePunctuation: '#5e6687',
@ -43,13 +45,17 @@ export const base = {
codeInserted: '#202746', codeInserted: '#202746',
codeImportant: '#c94922', codeImportant: '#c94922',
blockToolbarBackground: colors.white, blockToolbarBackground: colors.bgPrimary,
blockToolbarTrigger: colors.greyMid, blockToolbarTrigger: colors.primary,
blockToolbarTriggerIcon: colors.white, blockToolbarTriggerIcon: colors.white,
blockToolbarItem: colors.almostBlack, blockToolbarItem: colors.white,
blockToolbarText: colors.almostBlack, blockToolbarText: colors.white,
blockToolbarHoverBackground: colors.greyLight, blockToolbarHoverBackground: colors.primary,
blockToolbarDivider: colors.greyMid, blockToolbarDivider: colors.almostWhite,
blockToolbarIcon: undefined,
blockToolbarIconSelected: colors.white,
blockToolbarTextSelected: colors.white,
noticeInfoBackground: '#F5BE31', noticeInfoBackground: '#F5BE31',
noticeInfoText: colors.almostBlack, noticeInfoText: colors.almostBlack,
@ -58,18 +64,70 @@ export const base = {
noticeWarningBackground: '#FF5C80', noticeWarningBackground: '#FF5C80',
noticeWarningText: colors.white, noticeWarningText: colors.white,
}; };
export const BASE_TWO = {
...colors,
fontFamily:
"-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen, Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif",
fontFamilyMono: "'SFMono-Regular',Consolas,'Liberation Mono', Menlo, Courier,monospace",
fontWeight: 400,
zIndex: 1000000,
link: colors.primary,
placeholder: '#B1BECC',
textSecondary: '#fff',
textLight: colors.white,
textHighlight: '#b3e7ff',
textHighlightForeground: colors.white,
selected: colors.primary,
codeComment: '#6a737d',
codePunctuation: '#5e6687',
codeNumber: '#d73a49',
codeProperty: '#c08b30',
codeTag: '#3d8fd1',
codeString: '#032f62',
codeSelector: '#6679cc',
codeAttr: '#c76b29',
codeEntity: '#22a2c9',
codeKeyword: '#d73a49',
codeFunction: '#6f42c1',
codeStatement: '#22a2c9',
codePlaceholder: '#3d8fd1',
codeInserted: '#202746',
codeImportant: '#c94922',
blockToolbarBackground: colors.bgPrimary,
blockToolbarTrigger: colors.white,
blockToolbarTriggerIcon: colors.white,
blockToolbarItem: colors.white,
blockToolbarText: colors.white,
blockToolbarHoverBackground: colors.primary,
blockToolbarDivider: colors.almostWhite,
blockToolbarIcon: undefined,
blockToolbarIconSelected: colors.black,
blockToolbarTextSelected: colors.black,
noticeInfoBackground: '#F5BE31',
noticeInfoText: colors.almostBlack,
noticeTipBackground: '#9E5CF7',
noticeTipText: colors.white,
noticeWarningBackground: '#FF5C80',
noticeWarningText: colors.white,
};
export const dark = { export const dark = {
...base, ...base,
background: 'transparent', background: colors.almostBlack,
text: `${theme.colors.text.primary}`, text: colors.almostWhite,
code: `${theme.colors.text.primary}`, code: colors.almostWhite,
cursor: `${theme.colors.text.primary}`, cursor: colors.white,
divider: '#4E5C6E', divider: '#4E5C6E',
placeholder: '#52657A', placeholder: '#52657A',
toolbarBackground: colors.white, toolbarBackground: colors.bgPrimary,
toolbarInput: colors.black10, toolbarHoverBackground: colors.primary,
toolbarItem: colors.lightBlack, toolbarInput: colors.almostWhite,
toolbarItem: colors.white,
tableDivider: colors.lightBlack, tableDivider: colors.lightBlack,
tableSelected: colors.primary, tableSelected: colors.primary,
@ -81,6 +139,9 @@ export const dark = {
codeString: '#3d8fd1', codeString: '#3d8fd1',
horizontalRule: colors.lightBlack, horizontalRule: colors.lightBlack,
imageErrorBackground: 'rgba(0, 0, 0, 0.5)', imageErrorBackground: 'rgba(0, 0, 0, 0.5)',
scrollbarBackground: colors.black,
scrollbarThumb: colors.lightBlack,
}; };
export default dark; export default dark;

View File

@ -0,0 +1,5 @@
const RFC2822_EMAIL = /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/;
export default function isValidEmail(target: string) {
return RFC2822_EMAIL.test(target);
}

View File

@ -0,0 +1,5 @@
const localStorage = {
CARD_LABEL_VARIANT_STORAGE_KEY: 'card_label_variant',
};
export default localStorage;

View File

@ -0,0 +1,13 @@
function resolve(interval: number) {
if (process.env.REACT_APP_ENABLE_POLLING === 'true') return interval;
return 0;
}
const polling = {
PROJECTS: resolve(3000),
PROJECT: resolve(3000),
MEMBERS: resolve(3000),
TEAM_PROJECTS: resolve(3000),
TASK_DETAILS: resolve(3000),
};
export default polling;

View File

@ -27,10 +27,10 @@ export const color = {
}; };
export const font = { export const font = {
regular: 'font-family: "Droid Sans", Roboto, sans-serif; font-weight: normal;', regular: 'font-family: "Open Sans", Roboto, sans-serif; font-weight: normal;',
size: (size: number) => `font-size: ${size}px;`, size: (size: number) => `font-size: ${size}px;`,
bold: 'font-family: "Droid Sans", Roboto, sans-serif; font-weight: normal;', bold: 'font-family: "Open Sans", Roboto, sans-serif; font-weight: normal;',
medium: 'font-family: "Droid Sans", Roboto, sans-serif; font-weight: normal;', medium: 'font-family: "Open Sans", Roboto, sans-serif; font-weight: normal;',
}; };
export const mixin = { export const mixin = {

View File

@ -1,5 +0,0 @@
import { PermissionObjectType, PermissionLevel } from 'App/context';
export default function userCan(level: PermissionLevel, objectType: PermissionObjectType) {
return false;
}

View File

@ -59,11 +59,6 @@ type User = TaskUser & {
owned: RelatedList; owned: RelatedList;
}; };
type RefreshTokenResponse = {
accessToken: string;
setup?: null | { confirmToken: string };
};
type LoginFormData = { type LoginFormData = {
username: string; username: string;
password: string; password: string;

View File

@ -18,8 +18,9 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"jsx": "react", "jsx": "react-jsx",
"baseUrl": "src" "baseUrl": "src",
"noFallthroughCasesInSwitch": true
}, },
"include": [ "include": [
"src" "src"

File diff suppressed because it is too large Load Diff

6
go.mod
View File

@ -3,11 +3,11 @@ module github.com/jordanknott/taskcafe
go 1.13 go 1.13
require ( require (
github.com/99designs/gqlgen v0.11.3 github.com/99designs/gqlgen v0.13.0
github.com/RichardKnop/machinery v1.9.1 github.com/RichardKnop/machinery v1.9.1
github.com/brianvoe/gofakeit/v5 v5.11.2 github.com/brianvoe/gofakeit/v5 v5.11.2
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/go-chi/chi v3.3.2+incompatible github.com/go-chi/chi v3.3.2+incompatible
github.com/go-chi/cors v1.2.0
github.com/golang-migrate/migrate/v4 v4.11.0 github.com/golang-migrate/migrate/v4 v4.11.0
github.com/google/uuid v1.1.1 github.com/google/uuid v1.1.1
github.com/jinzhu/now v1.1.1 github.com/jinzhu/now v1.1.1
@ -24,7 +24,7 @@ require (
github.com/spf13/cobra v1.0.0 github.com/spf13/cobra v1.0.0
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.4.0 github.com/spf13/viper v1.4.0
github.com/vektah/gqlparser/v2 v2.0.1 github.com/vektah/gqlparser/v2 v2.1.0
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/mail.v2 v2.3.1 gopkg.in/mail.v2 v2.3.1

19
go.sum
View File

@ -40,8 +40,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/99designs/gqlgen v0.11.3 h1:oFSxl1DFS9X///uHV3y6CEfpcXWrDUxVblR4Xib2bs4= github.com/99designs/gqlgen v0.13.0 h1:haLTcUp3Vwp80xMVEg5KRNwzfUrgFdRmtBY8fuB8scA=
github.com/99designs/gqlgen v0.11.3/go.mod h1:RgX5GRRdDWNkh4pBrdzNpNPFVsdoUFY2+adM6nb1N+4= github.com/99designs/gqlgen v0.13.0/go.mod h1:NV130r6f4tpRWuAI+zsrSdooO/eWUv+Gyyoi3rEfXIk=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
@ -96,9 +96,6 @@ github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwj
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0= github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0=
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/brianvoe/gofakeit v1.2.0 h1:GGbzCqQx9ync4ObAUhRa3F/M73eL9VZL3X09WoTwphM=
github.com/brianvoe/gofakeit v3.18.0+incompatible h1:wDOmHc9DLG4nRjUVVaxA+CEglKOW72Y5+4WNxUIkjM8=
github.com/brianvoe/gofakeit v3.18.0+incompatible/go.mod h1:kfwdRA90vvNhPutZWfH7WPaDzUjz+CZFqG+rPkOjGOc=
github.com/brianvoe/gofakeit/v5 v5.11.2 h1:Ny5Nsf4z2023ZvYP8ujW8p5B1t5sxhdFaQ/0IYXbeSA= github.com/brianvoe/gofakeit/v5 v5.11.2 h1:Ny5Nsf4z2023ZvYP8ujW8p5B1t5sxhdFaQ/0IYXbeSA=
github.com/brianvoe/gofakeit/v5 v5.11.2/go.mod h1:/ZENnKqX+XrN8SORLe/fu5lZDIo1tuPncWuRD+eyhSI= github.com/brianvoe/gofakeit/v5 v5.11.2/go.mod h1:/ZENnKqX+XrN8SORLe/fu5lZDIo1tuPncWuRD+eyhSI=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@ -125,6 +122,7 @@ github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369 h1:XNT/Zf5l++1Pyg08/HV04ppB0gKxAqtZQBRYiYrUuYk= github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369 h1:XNT/Zf5l++1Pyg08/HV04ppB0gKxAqtZQBRYiYrUuYk=
@ -166,6 +164,8 @@ github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-chi/chi v3.3.2+incompatible h1:uQNcQN3NsV1j4ANsPh42P4ew4t6rnRbJb8frvpp31qQ= github.com/go-chi/chi v3.3.2+incompatible h1:uQNcQN3NsV1j4ANsPh42P4ew4t6rnRbJb8frvpp31qQ=
github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/cors v1.2.0 h1:tV1g1XENQ8ku4Bq3K9ub2AtgG+p16SmzeMSGTwrOKdE=
github.com/go-chi/cors v1.2.0/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@ -293,10 +293,10 @@ github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ=
github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
@ -550,6 +550,7 @@ github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA=
github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k= github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
@ -559,8 +560,8 @@ github.com/vanng822/go-premailer v0.0.0-20191214114701-be27abe028fe h1:9YnI5plmy
github.com/vanng822/go-premailer v0.0.0-20191214114701-be27abe028fe/go.mod h1:JTFJA/t820uFDoyPpErFQ3rb3amdZoPtxcKervG0OE4= github.com/vanng822/go-premailer v0.0.0-20191214114701-be27abe028fe/go.mod h1:JTFJA/t820uFDoyPpErFQ3rb3amdZoPtxcKervG0OE4=
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e h1:+w0Zm/9gaWpEAyDlU1eKOuk5twTjAjuevXqcJJw8hrg= github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e h1:+w0Zm/9gaWpEAyDlU1eKOuk5twTjAjuevXqcJJw8hrg=
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U= github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
github.com/vektah/gqlparser/v2 v2.0.1 h1:xgl5abVnsd4hkN9rk65OJID9bfcLSMuTaTcZj777q1o= github.com/vektah/gqlparser/v2 v2.1.0 h1:uiKJ+T5HMGGQM2kRKQ8Pxw8+Zq9qhhZhz/lieYvCMns=
github.com/vektah/gqlparser/v2 v2.0.1/go.mod h1:SyUiHgLATUR8BiYURfTirrTcGpcE+4XkV2se04Px1Ms= github.com/vektah/gqlparser/v2 v2.1.0/go.mod h1:SyUiHgLATUR8BiYURfTirrTcGpcE+4XkV2se04Px1Ms=
github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs= github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs=
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV0YCnDjqSL7/q/JyPhhJSPk= github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV0YCnDjqSL7/q/JyPhhJSPk=
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=

Some files were not shown because too many files have changed in this diff Show More