18 Commits

Author SHA1 Message Date
2de48e288b refactor: make theme more consistent 2020-12-17 22:51:20 -06:00
7b6624ecc3 feat: redesign project sharing & initial registration
redesigned the project sharing popup to be a multi select dropdown
that populates the options by using the input as a fuzzy search filter
on the current users & invited users.

users can now also be directly invited by email from the project share
window. if invited this way, then the user will receive an email
that sends them to a registration page, then a confirmation page.

the initial registration was always redone so that it uses a similar
system to the above in that it now will accept the first registered
user if there are no other accounts (besides 'system').
2020-12-17 22:39:14 -06:00
6c7203a4aa refactor: move default viper config values to commands/commands.go 2020-10-20 18:58:15 -05:00
86f2d90668 feat(cli): Reset Password Command
Introduce `reset-password` command.

Refs #71
2020-10-20 18:50:54 -05:00
92493deedf refactor: replace moment with dayjs 2020-10-20 16:06:16 -05:00
a288e06123 feat: add 'complete' sort option 2020-09-30 23:38:01 -07:00
ed4775faa5 docs(CONTRIBUTING): add section on unwanted PRs 2020-10-01 00:55:59 -05:00
0c7d2e2c9f feat(Login): add spinner on login 2020-09-23 15:40:35 -07:00
4277b7b2a8 feat: add personal projects
personal projects are projects that have no team.

they can only seen by the project members (one of which is whoever first
creates the project).
2020-09-19 20:23:16 -05:00
28a53f14ad docs(README): update docker badge to filter out nightly 2020-09-19 20:03:33 -05:00
0d4fb6a0d0 fix: member permissions now works correctly 2020-09-19 17:26:02 -05:00
0366b4c7f7 fix(CardComposer): add card button now creates a card 2020-09-18 20:33:15 -05:00
058749cb17 fix(commands/web): return error from ListenAndServe 2020-09-18 20:19:14 -05:00
3d95c6b600 docs(README): add docker pulls badge 2020-09-16 15:15:58 -05:00
c7538a98e5 fix: segfault on database connection failure 2020-09-12 18:23:23 -05:00
fe84f97f18 fix: url encode avatar filename when showing path
fixes #61
2020-09-12 18:12:12 -05:00
52c60abcd7 fix: secret key is no longer hard coded
the secret key for signing JWT tokens is now read from server.secret.

if that does not exist, then a random UUID v4 is generated and used
instead. a log warning is also shown.
2020-09-12 18:03:17 -05:00
9fdb3008db docs(bug_report): add note about server logs 2020-09-12 03:33:24 -05:00
161 changed files with 6587 additions and 1984 deletions

View File

@ -18,6 +18,8 @@ If applicable, add screenshots to help explain your problem.
**Additional context** **Additional context**
Add any other context about the problem here. Add any other context about the problem here.
Please send the Taskcafe web service logs if applicable.
<!-- <!--
Please read the contributing guide before working on any new pull requests! Please read the contributing guide before working on any new pull requests!

View File

@ -32,6 +32,10 @@ The `description` is a decriptive summary of the change the PR will make.
- One PR per fix or feature - One PR per fix or feature
- Setup & install [pre-commit hooks](https://pre-commit.com/#install) then install the hooks `pre-commit install && pre-commit install --hook-type commit-msg` - Setup & install [pre-commit hooks](https://pre-commit.com/#install) then install the hooks `pre-commit install && pre-commit install --hook-type commit-msg`
### Unwanted PRs
- Please do not submit pull requests containing only typo fixes, fixed spelling mistakes, or minor wording changes.
### Git Commit Message Style ### Git Commit Message Style
This project uses the [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) format. This project uses the [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) format.

View File

@ -9,11 +9,14 @@
<img alt="Releases" src="https://img.shields.io/github/v/release/JordanKnott/taskcafe" /> <img alt="Releases" src="https://img.shields.io/github/v/release/JordanKnott/taskcafe" />
</a> </a>
<a href="https://hub.docker.com/repository/docker/taskcafe/taskcafe"> <a href="https://hub.docker.com/repository/docker/taskcafe/taskcafe">
<img alt="Dockerhub" src="https://img.shields.io/docker/v/taskcafe/taskcafe?label=docker" /> <img alt="Dockerhub" src="https://img.shields.io/docker/v/taskcafe/taskcafe?label=docker&sort=semver" />
</a> </a>
<a href="https://goreportcard.com/report/github.com/JordanKnott/taskcafe"> <a href="https://goreportcard.com/report/github.com/JordanKnott/taskcafe">
<img alt="Go Report Card" src="https://goreportcard.com/badge/github.com/JordanKnott/taskcafe" /> <img alt="Go Report Card" src="https://goreportcard.com/badge/github.com/JordanKnott/taskcafe" />
</a> </a>
<a href="">
<img alt="Docker pulls" src="https://img.shields.io/docker/pulls/taskcafe/taskcafe" />
</a>
</p> </p>
<p align="center"> <p align="center">
Was this project useful? Please consider <a href="https://www.buymeacoffee.com/jordanknott">donating</a> to help me improve it! Was this project useful? Please consider <a href="https://www.buymeacoffee.com/jordanknott">donating</a> to help me improve it!

47
conf/air.toml Normal file
View File

@ -0,0 +1,47 @@
# Config file for [Air](https://github.com/cosmtrek/air) in TOML format
# Working directory
# . or absolute path, please note that the directories following must be under root.
root = "."
tmp_dir = "tmp"
[build]
# Just plain old shell command. You could use `make` as well.
cmd = "go build -o ./dist/taskcafe cmd/taskcafe/main.go"
# Binary file yields from `cmd`.
bin = "dist/taskcafe"
# Customize binary.
full_bin = "./dist/taskcafe web"
# Watch these filename extensions.
include_ext = ["go"]
# Ignore these filename extensions or directories.
exclude_dir = ["dist", "frontend"]
# Watch these directories if you specified.
include_dir = []
# Exclude files.
exclude_file = []
# This log file places in your tmp_dir.
log = "air.log"
# It's not necessary to trigger build each time file changes if it's too frequent.
delay = 1000 # ms
# Stop running old binary when build errors occur.
stop_on_error = true
# Send Interrupt signal before killing process (windows does not support this feature)
send_interrupt = false
# Delay after sending Interrupt signal
kill_delay = 500 # ms
[log]
# Show log time
time = false
[color]
# Customize each part's color. If no color found, use the raw app log.
main = "magenta"
watcher = "cyan"
build = "yellow"
runner = "green"
[misc]
# Delete tmp directory on exit
clean_on_exit = true

View File

@ -1,4 +1,4 @@
[general] [server]
hostname = '0.0.0.0:3333' hostname = '0.0.0.0:3333'
[email_notifications] [email_notifications]

View File

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

View File

@ -31,7 +31,9 @@
"@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"] }],
"no-case-declarations": "off", "no-case-declarations": "off",
"no-plusplus": "off",
"react/prop-types": 0, "react/prop-types": 0,
"no-continue": "off",
"react/jsx-props-no-spreading": "off", "react/jsx-props-no-spreading": "off",
"no-param-reassign": "off", "no-param-reassign": "off",
"import/extensions": [ "import/extensions": [

View File

@ -13,6 +13,7 @@
"@types/jwt-decode": "^2.2.1", "@types/jwt-decode": "^2.2.1",
"@types/lodash": "^4.14.149", "@types/lodash": "^4.14.149",
"@types/node": "^12.0.0", "@types/node": "^12.0.0",
"@types/query-string": "^6.3.0",
"@types/react": "^16.9.21", "@types/react": "^16.9.21",
"@types/react-beautiful-dnd": "^12.1.1", "@types/react-beautiful-dnd": "^12.1.1",
"@types/react-datepicker": "^2.11.0", "@types/react-datepicker": "^2.11.0",
@ -33,14 +34,15 @@
"axios-auth-refresh": "^2.2.7", "axios-auth-refresh": "^2.2.7",
"color": "^3.1.2", "color": "^3.1.2",
"date-fns": "^2.14.0", "date-fns": "^2.14.0",
"dayjs": "^1.9.1",
"graphql": "^15.0.0", "graphql": "^15.0.0",
"graphql-tag": "^2.10.3", "graphql-tag": "^2.10.3",
"history": "^4.10.1", "history": "^4.10.1",
"immer": "^6.0.3", "immer": "^6.0.3",
"jwt-decode": "^2.2.0", "jwt-decode": "^2.2.0",
"lodash": "^4.17.15", "lodash": "^4.17.20",
"moment": "^2.24.0",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"query-string": "^6.13.7",
"react": "^16.12.0", "react": "^16.12.0",
"react-autosize-textarea": "^7.0.0", "react-autosize-textarea": "^7.0.0",
"react-beautiful-dnd": "^13.0.0", "react-beautiful-dnd": "^13.0.0",
@ -52,9 +54,9 @@
"react-router-dom": "^5.1.2", "react-router-dom": "^5.1.2",
"react-scripts": "3.4.0", "react-scripts": "3.4.0",
"react-select": "^3.1.0", "react-select": "^3.1.0",
"rich-markdown-editor": "^10.6.5",
"react-timeago": "^4.4.0", "react-timeago": "^4.4.0",
"react-toastify": "^6.0.8", "react-toastify": "^6.0.8",
"rich-markdown-editor": "^10.6.5",
"styled-components": "^5.0.1", "styled-components": "^5.0.1",
"typescript": "~3.7.2" "typescript": "~3.7.2"
}, },

View File

@ -5,6 +5,7 @@ import GlobalTopNavbar from 'App/TopNavbar';
import { import {
useUsersQuery, useUsersQuery,
useDeleteUserAccountMutation, useDeleteUserAccountMutation,
useDeleteInvitedUserAccountMutation,
useCreateUserAccountMutation, useCreateUserAccountMutation,
UsersDocument, UsersDocument,
UsersQuery, UsersQuery,
@ -81,7 +82,7 @@ const AddUserInput = styled(Input)`
`; `;
const InputError = styled.span` const InputError = styled.span`
color: rgba(${props => props.theme.colors.danger}); color: ${props => props.theme.colors.danger};
font-size: 12px; font-size: 12px;
`; `;
@ -176,6 +177,17 @@ const AdminRoute = () => {
const { loading, data } = useUsersQuery(); const { loading, data } = useUsersQuery();
const { showPopup, hidePopup } = usePopup(); const { showPopup, hidePopup } = usePopup();
const { user } = useCurrentUser(); const { user } = useCurrentUser();
const [deleteInvitedUser] = useDeleteInvitedUserAccountMutation({
update: (client, response) => {
updateApolloCache<UsersQuery>(client, UsersDocument, cache =>
produce(cache, draftCache => {
draftCache.invitedUsers = cache.invitedUsers.filter(
u => u.id !== response.data.deleteInvitedUserAccount.invitedUser.id,
);
}),
);
},
});
const [deleteUser] = useDeleteUserAccountMutation({ const [deleteUser] = useDeleteUserAccountMutation({
update: (client, response) => { update: (client, response) => {
updateApolloCache<UsersQuery>(client, UsersDocument, cache => updateApolloCache<UsersQuery>(client, UsersDocument, cache =>
@ -215,11 +227,16 @@ const AdminRoute = () => {
<Admin <Admin
initialTab={0} initialTab={0}
users={data.users} users={data.users}
invitedUsers={data.invitedUsers}
canInviteUser={user.roles.org === 'admin'} canInviteUser={user.roles.org === 'admin'}
onInviteUser={NOOP} onInviteUser={NOOP}
onUpdateUserPassword={() => { onUpdateUserPassword={() => {
hidePopup(); hidePopup();
}} }}
onDeleteInvitedUser={invitedUserID => {
deleteInvitedUser({ variables: { invitedUserID } });
hidePopup();
}}
onDeleteUser={(userID, newOwnerID) => { onDeleteUser={(userID, newOwnerID) => {
deleteUser({ variables: { userID, newOwnerID } }); deleteUser({ variables: { userID, newOwnerID } });
hidePopup(); hidePopup();

View File

@ -1,16 +1,20 @@
import React from 'react'; import React, { useEffect, useState } from 'react';
import { Switch, Route } from 'react-router-dom'; import { Switch, Route, useHistory } from 'react-router-dom';
import * as H from 'history'; import * as H from 'history';
import Dashboard from 'Dashboard'; import Dashboard from 'Dashboard';
import Admin from 'Admin'; import Admin from 'Admin';
import Confirm from 'Confirm';
import Projects from 'Projects'; import Projects from 'Projects';
import Project from 'Projects/Project'; import Project from 'Projects/Project';
import Teams from 'Teams'; import Teams from 'Teams';
import Login from 'Auth'; import Login from 'Auth';
import Install from 'Install'; 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';
const MainContent = styled.div` const MainContent = styled.div`
padding: 0 0 0 0; padding: 0 0 0 0;
@ -21,6 +25,50 @@ const MainContent = styled.div`
flex-grow: 1; flex-grow: 1;
`; `;
const AuthorizedRoutes = () => {
const history = useHistory();
const [loading, setLoading] = useState(true);
const { setUser } = useCurrentUser();
useEffect(() => {
fetch('/auth/refresh_token', {
method: 'POST',
credentials: 'include',
}).then(async x => {
const { status } = x;
if (status === 400) {
history.replace('/login');
} else {
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);
});
}, []);
return loading ? null : (
<Switch>
<MainContent>
<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} />
</MainContent>
</Switch>
);
};
type RoutesProps = { type RoutesProps = {
history: H.History; history: H.History;
}; };
@ -28,15 +76,9 @@ type RoutesProps = {
const Routes: React.FC<RoutesProps> = () => ( const Routes: React.FC<RoutesProps> = () => (
<Switch> <Switch>
<Route exact path="/login" component={Login} /> <Route exact path="/login" component={Login} />
<Route exact path="/install" component={Install} /> <Route exact path="/register" component={Register} />
<MainContent> <Route exact path="/confirm" component={Confirm} />
<Route exact path="/" component={Dashboard} /> <AuthorizedRoutes />
<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} />
</MainContent>
</Switch> </Switch>
); );

View File

@ -1,26 +1,28 @@
import { DefaultTheme } from 'styled-components'; import { DefaultTheme } from 'styled-components';
import Color from 'color';
const theme: DefaultTheme = { const theme: DefaultTheme = {
borderRadius: { borderRadius: {
primary: '3px', primary: '3x',
alternate: '6px', alternate: '6px',
}, },
colors: { colors: {
primary: '115, 103, 240', multiColors: ['#e362e3', '#7a6ff0', '#37c5ab', '#aa62e3', '#e8384f'],
secondary: '216, 93, 216', primary: 'rgb(115, 103, 240)',
alternate: '65, 69, 97', secondary: 'rgb(216, 93, 216)',
success: '40, 199, 111', alternate: 'rgb(65, 69, 97)',
danger: '234, 84, 85', success: 'rgb(40, 199, 111)',
warning: '255, 159, 67', danger: 'rgb(234, 84, 85)',
dark: '30, 30, 30', warning: 'rgb(255, 159, 67)',
dark: 'rgb(30, 30, 30)',
text: { text: {
primary: '194, 198, 220', primary: 'rgb(194, 198, 220)',
secondary: '255, 255, 255', secondary: 'rgb(255, 255, 255)',
}, },
border: '65, 69, 97', border: 'rgb(65, 69, 97)',
bg: { bg: {
primary: '16, 22, 58', primary: 'rgb(16, 22, 58)',
secondary: '38, 44, 73', secondary: 'rgb(38, 44, 73)',
}, },
}, },
}; };

View File

@ -20,6 +20,7 @@ import MiniProfile from 'shared/components/MiniProfile';
import cache from 'App/cache'; import cache from 'App/cache';
import NOOP from 'shared/utils/noop'; import NOOP from 'shared/utils/noop';
import NotificationPopup, { NotificationItem } from 'shared/components/NotifcationPopup'; import NotificationPopup, { NotificationItem } from 'shared/components/NotifcationPopup';
import theme from './ThemeStyles';
const TeamContainer = styled.div` const TeamContainer = styled.div`
display: flex; display: flex;
@ -62,7 +63,7 @@ const TeamProjectBackground = styled.div<{ color: string }>`
opacity: 1; opacity: 1;
border-radius: 3px; border-radius: 3px;
&:before { &:before {
background: rgba(${props => props.theme.colors.bg.secondary}); background: ${props => props.theme.colors.bg.secondary};
bottom: 0; bottom: 0;
content: ''; content: '';
left: 0; left: 0;
@ -114,7 +115,7 @@ const TeamProjectContainer = styled.div`
margin: 0 4px 4px 0; margin: 0 4px 4px 0;
min-width: 0; min-width: 0;
&:hover ${TeamProjectTitle} { &:hover ${TeamProjectTitle} {
color: rgba(${props => props.theme.colors.text.secondary}); color: ${props => props.theme.colors.text.secondary};
} }
&:hover ${TeamProjectAvatar} { &:hover ${TeamProjectAvatar} {
opacity: 1; opacity: 1;
@ -124,7 +125,7 @@ const TeamProjectContainer = styled.div`
} }
`; `;
const colors = ['#e362e3', '#7a6ff0', '#37c5ab', '#aa62e3', '#e8384f']; const colors = [theme.colors.primary, theme.colors.secondary];
const ProjectFinder = () => { const ProjectFinder = () => {
const { loading, data } = useGetProjectsQuery(); const { loading, data } = useGetProjectsQuery();
@ -137,7 +138,7 @@ const ProjectFinder = () => {
return { return {
id: team.id, id: team.id,
name: team.name, name: team.name,
projects: projects.filter(project => project.team.id === team.id), projects: projects.filter(project => project.team && project.team.id === team.id),
}; };
}); });
return ( return (
@ -230,10 +231,12 @@ type GlobalTopNavbarProps = {
menuType?: Array<MenuItem>; menuType?: Array<MenuItem>;
onChangeRole?: (userID: string, roleCode: RoleCode) => void; onChangeRole?: (userID: string, roleCode: RoleCode) => void;
projectMembers?: null | Array<TaskUser>; projectMembers?: null | Array<TaskUser>;
projectInvitedMembers?: null | Array<InvitedUser>;
onSaveProjectName?: (projectName: string) => void; onSaveProjectName?: (projectName: string) => void;
onInviteUser?: ($target: React.RefObject<HTMLElement>) => void; onInviteUser?: ($target: React.RefObject<HTMLElement>) => void;
onSetTab?: (tab: number) => void; onSetTab?: (tab: number) => void;
onRemoveFromBoard?: (userID: string) => void; onRemoveFromBoard?: (userID: string) => void;
onRemoveInvitedFromBoard?: (email: string) => void;
}; };
const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
@ -246,8 +249,10 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
name, name,
popupContent, popupContent,
projectMembers, projectMembers,
projectInvitedMembers,
onInviteUser, onInviteUser,
onSaveProjectName, onSaveProjectName,
onRemoveInvitedFromBoard,
onRemoveFromBoard, onRemoveFromBoard,
}) => { }) => {
const { user, setUserRoles, setUser } = useCurrentUser(); const { user, setUserRoles, setUser } = useCurrentUser();
@ -324,7 +329,7 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
/> />
))} ))}
</NotificationPopup>, </NotificationPopup>,
{ width: 415, borders: false, diamondColor: '#7367f0' }, { width: 415, borders: false, diamondColor: theme.colors.primary },
); );
} }
}; };
@ -333,6 +338,34 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
return null; return null;
} }
const userIsTeamOrProjectAdmin = user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID); const userIsTeamOrProjectAdmin = user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID);
const onInvitedMemberProfile = ($targetRef: React.RefObject<HTMLElement>, email: string) => {
const member = projectInvitedMembers ? projectInvitedMembers.find(u => u.email === email) : null;
if (member) {
showPopup(
$targetRef,
<MiniProfile
onRemoveFromBoard={() => {
if (onRemoveInvitedFromBoard) {
onRemoveInvitedFromBoard(member.email);
}
}}
invited
user={{
id: member.email,
fullName: member.email,
bio: 'Invited',
profileIcon: {
bgColor: '#000',
url: null,
initials: member.email.charAt(0),
},
}}
bio=""
/>,
);
}
};
const onMemberProfile = ($targetRef: React.RefObject<HTMLElement>, memberID: string) => { const onMemberProfile = ($targetRef: React.RefObject<HTMLElement>, memberID: string) => {
const member = projectMembers ? projectMembers.find(u => u.id === memberID) : null; const member = projectMembers ? projectMembers.find(u => u.id === memberID) : null;
const warning = const warning =
@ -382,6 +415,7 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
canEditProjectName={userIsTeamOrProjectAdmin} canEditProjectName={userIsTeamOrProjectAdmin}
canInviteUser={userIsTeamOrProjectAdmin} canInviteUser={userIsTeamOrProjectAdmin}
onMemberProfile={onMemberProfile} onMemberProfile={onMemberProfile}
onInvitedMemberProfile={onInvitedMemberProfile}
onInviteUser={onInviteUser} onInviteUser={onInviteUser}
onChangeRole={onChangeRole} onChangeRole={onChangeRole}
onChangeProjectOwner={onChangeProjectOwner} onChangeProjectOwner={onChangeProjectOwner}
@ -392,6 +426,7 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
history.push('/'); history.push('/');
}} }}
projectMembers={projectMembers} projectMembers={projectMembers}
projectInvitedMembers={projectInvitedMembers}
onProfileClick={onProfileClick} onProfileClick={onProfileClick}
onSaveName={onSaveProjectName} onSaveName={onSaveProjectName}
onOpenSettings={onOpenSettings} onOpenSettings={onOpenSettings}

View File

@ -28,13 +28,13 @@ const StyledContainer = styled(ToastContainer).attrs({
color: #fff; color: #fff;
} }
.Toastify__toast--error { .Toastify__toast--error {
background: rgba(${props => props.theme.colors.danger}); background: ${props => props.theme.colors.danger};
} }
.Toastify__toast--warning { .Toastify__toast--warning {
background: rgba(${props => props.theme.colors.warning}); background: ${props => props.theme.colors.warning};
} }
.Toastify__toast--success { .Toastify__toast--success {
background: rgba(${props => props.theme.colors.success}); background: ${props => props.theme.colors.success};
} }
.Toastify__toast-body { .Toastify__toast-body {
} }
@ -52,7 +52,6 @@ type RefreshTokenResponse = {
}; };
const App = () => { const App = () => {
const [loading, setLoading] = useState(true);
const [user, setUser] = useState<CurrentUserRaw | null>(null); const [user, setUser] = useState<CurrentUserRaw | null>(null);
const setUserRoles = (roles: CurrentUserRoles) => { const setUserRoles = (roles: CurrentUserRoles) => {
if (user) { if (user) {
@ -63,32 +62,6 @@ const App = () => {
} }
}; };
useEffect(() => {
fetch('/auth/refresh_token', {
method: 'POST',
credentials: 'include',
}).then(async x => {
const { status } = x;
if (status === 400) {
history.replace('/login');
} else {
const response: RefreshTokenResponse = await x.json();
const { accessToken, isInstalled } = response;
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);
if (!isInstalled) {
history.replace('/install');
}
}
setLoading(false);
});
}, []);
return ( return (
<> <>
<UserContext.Provider value={{ user, setUser, setUserRoles }}> <UserContext.Provider value={{ user, setUser, setUserRoles }}>
@ -97,13 +70,7 @@ const App = () => {
<BaseStyles /> <BaseStyles />
<Router history={history}> <Router history={history}>
<PopupProvider> <PopupProvider>
{loading ? ( <Routes history={history} />
<div>loading</div>
) : (
<>
<Routes history={history} />
</>
)}
</PopupProvider> </PopupProvider>
</Router> </Router>
<StyledContainer <StyledContainer

View File

@ -52,7 +52,20 @@ const Auth = () => {
}).then(async x => { }).then(async x => {
const { status } = x; const { status } = x;
if (status === 200) { if (status === 200) {
history.replace('/projects'); 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);
history.replace('/projects');
}
} }
}); });
}, []); }, []);

View File

@ -0,0 +1,61 @@
import React, { useState } from 'react';
import axios from 'axios';
import Confirm from 'shared/components/Confirm';
import { useHistory, useLocation } from 'react-router';
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';
const UsersConfirm = () => {
const history = useHistory();
const location = useLocation();
const [registered, setRegistered] = useState(false);
const params = QueryString.parse(location.search);
const { setUser } = useCurrentUser();
return (
<Container>
<LoginWrapper>
<Confirm
hasConfirmToken={params.confirmToken !== undefined}
onConfirmUser={setFailed => {
fetch('/auth/confirm', {
method: 'POST',
body: JSON.stringify({
confirmToken: params.confirmToken,
}),
})
.then(async x => {
const { status } = x;
if (status === 200) {
const response = await x.json();
const { accessToken } = response;
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.push('/');
} else {
setFailed();
}
})
.catch(() => {
setFailed();
});
}}
/>
</LoginWrapper>
</Container>
);
};
export default UsersConfirm;

View File

@ -1,88 +0,0 @@
import React, { useEffect, useContext } from 'react';
import axios from 'axios';
import Register from 'shared/components/Register';
import { useHistory } from 'react-router';
import { getAccessToken, setAccessToken } from 'shared/utils/accessToken';
import UserContext from 'App/context';
import jwtDecode from 'jwt-decode';
import { Container, LoginWrapper } from './Styles';
const Install = () => {
const history = useHistory();
const { setUser } = useContext(UserContext);
useEffect(() => {
fetch('/auth/refresh_token', {
method: 'POST',
credentials: 'include',
}).then(async x => {
const { status } = x;
const response: RefreshTokenResponse = await x.json();
const { isInstalled } = response;
if (status === 200 && isInstalled) {
history.replace('/projects');
}
});
}, []);
return (
<Container>
<LoginWrapper>
<Register
onSubmit={(data, setComplete, setError) => {
const accessToken = getAccessToken();
if (data.password !== data.password_confirm) {
setError('password', { type: 'error', message: 'Passwords must match' });
setError('password_confirm', { type: 'error', message: 'Passwords must match' });
} else {
axios
.post(
'/auth/install',
{
user: {
username: data.username,
roleCode: 'admin',
email: data.email,
password: data.password,
initials: data.initials,
fullname: data.fullname,
},
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
)
.then(async x => {
const { status } = x;
if (status === 400) {
history.replace('/login');
} else {
const response: RefreshTokenResponse = await x.data;
const { accessToken: newToken, isInstalled } = response;
const claims: JWTToken = jwtDecode(newToken);
const currentUser = {
id: claims.userId,
roles: {
org: claims.orgRole,
teams: new Map<string, string>(),
projects: new Map<string, string>(),
},
};
setUser(currentUser);
setAccessToken(newToken);
if (!isInstalled) {
history.replace('/install');
}
}
history.push('/projects');
});
}
setComplete(true);
}}
/>
</LoginWrapper>
</Container>
);
};
export default Install;

View File

@ -5,7 +5,6 @@ import { TaskMetaFilters, TaskMeta, TaskMetaMatch, DueDateFilterType } from 'sha
import Input from 'shared/components/ControlledInput'; import Input from 'shared/components/ControlledInput';
import { Popup, usePopup } from 'shared/components/PopupMenu'; import { Popup, usePopup } from 'shared/components/PopupMenu';
import produce from 'immer'; import produce from 'immer';
import moment from 'moment';
import { mixin } from 'shared/utils/styles'; import { mixin } from 'shared/utils/styles';
import Member from 'shared/components/Member'; import Member from 'shared/components/Member';
@ -13,7 +12,7 @@ const FilterMember = styled(Member)`
margin: 2px 0; margin: 2px 0;
&:hover { &:hover {
cursor: pointer; cursor: pointer;
background: rgba(${props => props.theme.colors.primary}); background: ${props => props.theme.colors.primary};
} }
`; `;
@ -72,7 +71,7 @@ export const ActionItem = styled.li`
align-items: center; align-items: center;
font-size: 14px; font-size: 14px;
&:hover { &:hover {
background: rgb(115, 103, 240); background: ${props => props.theme.colors.primary};
} }
`; `;
@ -81,7 +80,7 @@ export const ActionTitle = styled.span`
`; `;
const ActionItemSeparator = styled.li` const ActionItemSeparator = styled.li`
color: rgba(${props => 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;

View File

@ -30,7 +30,7 @@ export const ActionItem = styled.li`
align-items: center; align-items: center;
font-size: 14px; font-size: 14px;
&:hover { &:hover {
background: rgb(115, 103, 240); background: ${props => props.theme.colors.primary};
} }
&:hover ${ActionExtraMenuContainer} { &:hover ${ActionExtraMenuContainer} {
visibility: visible; visibility: visible;
@ -69,11 +69,11 @@ export const ActionExtraMenuItem = styled.li`
align-items: center; align-items: center;
font-size: 14px; font-size: 14px;
&:hover { &:hover {
background: rgb(115, 103, 240); background: rgb(${props => props.theme.colors.primary});
} }
`; `;
const ActionExtraMenuSeparator = styled.li` const ActionExtraMenuSeparator = styled.li`
color: rgba(${props => props.theme.colors.text.primary}, 0.4); color: ${props => props.theme.colors.text.primary};
font-size: 12px; font-size: 12px;
padding-left: 4px; padding-left: 4px;
padding-right: 4px; padding-right: 4px;

View File

@ -1,6 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { TaskSorting, TaskSortingType, TaskSortingDirection } from 'shared/utils/sorting'; import { TaskSorting, TaskSortingType, TaskSortingDirection } from 'shared/utils/sorting';
import { mixin } from 'shared/utils/styles';
export const ActionsList = styled.ul` export const ActionsList = styled.ul`
margin: 0; margin: 0;
@ -20,7 +21,7 @@ export const ActionItem = styled.li`
align-items: center; align-items: center;
font-size: 14px; font-size: 14px;
&:hover { &:hover {
background: rgb(115, 103, 240); background: ${props => props.theme.colors.primary};
} }
`; `;
@ -29,7 +30,7 @@ export const ActionTitle = styled.span`
`; `;
const ActionItemSeparator = styled.li` const ActionItemSeparator = styled.li`
color: rgba(${props => 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;
@ -73,6 +74,11 @@ const SortPopup: React.FC<SortPopupProps> = ({ sorting, onChangeTaskSorting }) =
> >
<ActionTitle>Task title</ActionTitle> <ActionTitle>Task title</ActionTitle>
</ActionItem> </ActionItem>
<ActionItem
onClick={() => handleSetSorting({ type: TaskSortingType.COMPLETE, direction: TaskSortingDirection.ASC })}
>
<ActionTitle>Complete</ActionTitle>
</ActionItem>
</ActionsList> </ActionsList>
); );
}; };

View File

@ -136,14 +136,14 @@ const ProjectActionWrapper = styled.div<{ disabled?: boolean }>`
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 15px; font-size: 15px;
color: rgba(${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: rgba(${props => props.theme.colors.text.secondary}); color: ${props => props.theme.colors.text.secondary};
} }
${props => ${props =>
props.disabled && props.disabled &&

View File

@ -309,7 +309,7 @@ const Details: React.FC<DetailsProps> = ({
task={data.findTask} task={data.findTask}
onChecklistDrop={checklist => { onChecklistDrop={checklist => {
updateTaskChecklistLocation({ updateTaskChecklistLocation({
variables: { checklistID: checklist.id, position: checklist.position }, variables: { taskChecklistID: checklist.id, position: checklist.position },
optimisticResponse: { optimisticResponse: {
__typename: 'Mutation', __typename: 'Mutation',
@ -324,20 +324,24 @@ const Details: React.FC<DetailsProps> = ({
}, },
}); });
}} }}
onChecklistItemDrop={(prevChecklistID, checklistID, checklistItem) => { onChecklistItemDrop={(prevChecklistID, taskChecklistID, checklistItem) => {
updateTaskChecklistItemLocation({ updateTaskChecklistItemLocation({
variables: { checklistID, checklistItemID: checklistItem.id, position: checklistItem.position }, variables: {
taskChecklistID,
taskChecklistItemID: checklistItem.id,
position: checklistItem.position,
},
optimisticResponse: { optimisticResponse: {
__typename: 'Mutation', __typename: 'Mutation',
updateTaskChecklistItemLocation: { updateTaskChecklistItemLocation: {
__typename: 'UpdateTaskChecklistItemLocationPayload', __typename: 'UpdateTaskChecklistItemLocationPayload',
prevChecklistID, prevChecklistID,
checklistID, taskChecklistID,
checklistItem: { checklistItem: {
__typename: 'TaskChecklistItem', __typename: 'TaskChecklistItem',
position: checklistItem.position, position: checklistItem.position,
id: checklistItem.id, id: checklistItem.id,
taskChecklistID: checklistID, taskChecklistID,
}, },
}, },
}, },

View File

@ -3,32 +3,11 @@ import updateApolloCache from 'shared/utils/cache';
import { usePopup, Popup } from 'shared/components/PopupMenu'; import { usePopup, Popup } from 'shared/components/PopupMenu';
import produce from 'immer'; import produce from 'immer';
import { import {
useUpdateProjectMemberRoleMutation,
useCreateProjectMemberMutation,
useDeleteProjectMemberMutation,
useSetTaskCompleteMutation,
useToggleTaskLabelMutation,
useUpdateProjectNameMutation,
useFindProjectQuery,
useUpdateTaskGroupNameMutation,
useUpdateTaskNameMutation,
useUpdateProjectLabelMutation, useUpdateProjectLabelMutation,
useCreateTaskMutation,
useDeleteProjectLabelMutation, useDeleteProjectLabelMutation,
useDeleteTaskMutation,
useUpdateTaskLocationMutation,
useUpdateTaskGroupLocationMutation,
useCreateTaskGroupMutation,
useDeleteTaskGroupMutation,
useUpdateTaskDescriptionMutation,
useAssignTaskMutation,
DeleteTaskDocument,
FindProjectDocument, FindProjectDocument,
useCreateProjectLabelMutation, useCreateProjectLabelMutation,
useUnassignTaskMutation,
useUpdateTaskDueDateMutation,
FindProjectQuery, FindProjectQuery,
useUsersQuery,
} 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';

View File

@ -3,6 +3,7 @@ import React, { useState, useRef, useEffect, useContext } from 'react';
import updateApolloCache from 'shared/utils/cache'; import updateApolloCache from 'shared/utils/cache';
import GlobalTopNavbar, { ProjectPopup } from 'App/TopNavbar'; import GlobalTopNavbar, { ProjectPopup } from 'App/TopNavbar';
import styled from 'styled-components/macro'; import styled from 'styled-components/macro';
import AsyncSelect from 'react-select/async';
import { usePopup, Popup } from 'shared/components/PopupMenu'; import { usePopup, Popup } from 'shared/components/PopupMenu';
import { import {
useParams, useParams,
@ -15,11 +16,12 @@ import {
} from 'react-router-dom'; } from 'react-router-dom';
import { import {
useUpdateProjectMemberRoleMutation, useUpdateProjectMemberRoleMutation,
useCreateProjectMemberMutation, useInviteProjectMembersMutation,
useDeleteProjectMemberMutation, useDeleteProjectMemberMutation,
useToggleTaskLabelMutation, useToggleTaskLabelMutation,
useUpdateProjectNameMutation, useUpdateProjectNameMutation,
useFindProjectQuery, useFindProjectQuery,
useDeleteInvitedProjectMemberMutation,
useUpdateTaskNameMutation, useUpdateTaskNameMutation,
useCreateTaskMutation, useCreateTaskMutation,
useDeleteTaskMutation, useDeleteTaskMutation,
@ -37,12 +39,21 @@ import Input from 'shared/components/Input';
import Member from 'shared/components/Member'; import Member from 'shared/components/Member';
import EmptyBoard from 'shared/components/EmptyBoard'; 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 Button from 'shared/components/Button';
import { useApolloClient } from '@apollo/react-hooks';
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';
const CARD_LABEL_VARIANT_STORAGE_KEY = 'card_label_variant'; 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 useStateWithLocalStorage = (localStorageKey: string): [string, React.Dispatch<React.SetStateAction<string>>] => {
const [value, setValue] = React.useState<string>(localStorage.getItem(localStorageKey) || ''); const [value, setValue] = React.useState<string>(localStorage.getItem(localStorageKey) || '');
@ -61,7 +72,7 @@ const UserMember = styled(Member)`
padding: 4px 0; padding: 4px 0;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background: rgba(${props => props.theme.colors.bg.primary}, 0.4); background: ${props => mixin.rgba(props.theme.colors.bg.primary, 0.4)};
} }
border-radius: 6px; border-radius: 6px;
`; `;
@ -70,29 +81,299 @@ const MemberList = styled.div`
margin: 8px 0; margin: 8px 0;
`; `;
type InviteUserData = {
email?: string;
suerID?: string;
};
type UserManagementPopupProps = { type UserManagementPopupProps = {
projectID: string;
users: Array<User>; users: Array<User>;
projectMembers: Array<TaskUser>; projectMembers: Array<TaskUser>;
onAddProjectMember: (userID: string) => void; onInviteProjectMembers: (data: Array<InviteUserData>) => void;
}; };
const UserManagementPopup: React.FC<UserManagementPopupProps> = ({ users, projectMembers, onAddProjectMember }) => { 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) => {
console.log(input.trim().length < 3);
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> = [];
console.log(res.data && res.data.searchMembers);
if (res.data && res.data.searchMembers) {
results = [
...res.data.searchMembers.map((m: any) => {
if (m.status === 'INVITED') {
console.log(`${m.id} is added`);
return {
label: m.id,
value: {
id: m.id,
type: 2,
profileIcon: {
bgColor: '#ccc',
initials: m.id.charAt(0),
},
},
};
} else {
console.log(`${m.user.email} is added`);
emails.push(m.user.email);
return {
label: m.user.fullName,
value: { id: m.user.id, type: 0, profileIcon: m.user.profileIcon },
};
}
}),
];
console.log(results);
}
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 }) => {
console.log(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 ( return (
<Popup tab={0} title="Invite a user"> <Popup tab={0} title="Invite a user">
<SearchInput width="100%" variant="alternate" placeholder="Email address or name" name="search" /> <InviteContainer>
<MemberList> <AsyncSelect
{users getOptionValue={option => option.value.id}
.filter(u => u.id !== projectMembers.find(p => p.id === u.id)?.id) placeholder="Email address or username"
.map(user => ( noOptionsMessage={() => null}
<UserMember onChange={(e: any) => {
key={user.id} setInvitedUsers(e);
onCardMemberClick={() => onAddProjectMember(user.id)} }}
showName isMulti
member={user} autoFocus
taskID="" cacheOptions
/> styles={colourStyles}
))} defaultOption
</MemberList> 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> </Popup>
); );
}; };
@ -135,11 +416,30 @@ const Project = () => {
const [value, setValue] = useStateWithLocalStorage(CARD_LABEL_VARIANT_STORAGE_KEY); const [value, setValue] = useStateWithLocalStorage(CARD_LABEL_VARIANT_STORAGE_KEY);
const [updateProjectMemberRole] = useUpdateProjectMemberRoleMutation(); const [updateProjectMemberRole] = useUpdateProjectMemberRoleMutation();
const [deleteTask] = useDeleteTaskMutation(); const [deleteTask] = useDeleteTaskMutation({
update: (client, resp) =>
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
const taskGroupIdx = draftCache.findProject.taskGroups.findIndex(
tg => tg.tasks.findIndex(t => t.id === resp.data.deleteTask.taskID) !== -1,
);
if (taskGroupIdx !== -1) {
draftCache.findProject.taskGroups[taskGroupIdx].tasks = cache.findProject.taskGroups[
taskGroupIdx
].tasks.filter(t => t.id !== resp.data.deleteTask.taskID);
}
}),
{ projectID },
),
});
const [updateTaskName] = useUpdateTaskNameMutation(); const [updateTaskName] = useUpdateTaskNameMutation();
const { loading, data } = useFindProjectQuery({ const { loading, data, error } = useFindProjectQuery({
variables: { projectID }, variables: { projectID },
}); });
@ -157,14 +457,36 @@ const Project = () => {
}, },
}); });
const [createProjectMember] = useCreateProjectMemberMutation({ const [inviteProjectMembers] = useInviteProjectMembersMutation({
update: (client, response) => { update: (client, response) => {
updateApolloCache<FindProjectQuery>( updateApolloCache<FindProjectQuery>(
client, client,
FindProjectDocument, FindProjectDocument,
cache => cache =>
produce(cache, draftCache => { produce(cache, draftCache => {
draftCache.findProject.members.push({ ...response.data.createProjectMember.member }); draftCache.findProject.members = [
...cache.findProject.members,
...response.data.inviteProjectMembers.members,
];
draftCache.findProject.invitedMembers = [
...cache.findProject.invitedMembers,
...response.data.inviteProjectMembers.invitedMembers,
];
}),
{ projectID },
);
},
});
const [deleteInvitedProjectMember] = useDeleteInvitedProjectMemberMutation({
update: (client, response) => {
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
draftCache.findProject.invitedMembers = cache.findProject.invitedMembers.filter(
m => m.email !== response.data.deleteInvitedProjectMember.invitedMember.email,
);
}), }),
{ projectID }, { projectID },
); );
@ -205,6 +527,9 @@ const Project = () => {
</> </>
); );
} }
if (error) {
history.push('/projects');
}
if (data) { if (data) {
labelsRef.current = data.findProject.labels; labelsRef.current = data.findProject.labels;
@ -221,6 +546,10 @@ const Project = () => {
deleteProjectMember({ variables: { userID, projectID } }); deleteProjectMember({ variables: { userID, projectID } });
hidePopup(); hidePopup();
}} }}
onRemoveInvitedFromBoard={email => {
deleteInvitedProjectMember({ variables: { projectID, email } });
hidePopup();
}}
onSaveProjectName={projectName => { onSaveProjectName={projectName => {
updateProjectName({ variables: { projectID, name: projectName } }); updateProjectName({ variables: { projectID, name: projectName } });
}} }}
@ -228,8 +557,10 @@ const Project = () => {
showPopup( showPopup(
$target, $target,
<UserManagementPopup <UserManagementPopup
onAddProjectMember={userID => { projectID={projectID}
createProjectMember({ variables: { userID, projectID } }); onInviteProjectMembers={members => {
inviteProjectMembers({ variables: { projectID, members } });
hidePopup();
}} }}
users={data.users} users={data.users}
projectMembers={data.findProject.members} projectMembers={data.findProject.members}
@ -240,8 +571,9 @@ const Project = () => {
menuType={[{ name: 'Board', link: location.pathname }]} menuType={[{ name: 'Board', link: location.pathname }]}
currentTab={0} currentTab={0}
projectMembers={data.findProject.members} projectMembers={data.findProject.members}
projectInvitedMembers={data.findProject.invitedMembers}
projectID={projectID} projectID={projectID}
teamID={data.findProject.team.id} teamID={data.findProject.team ? data.findProject.team.id : null}
name={data.findProject.name} name={data.findProject.name}
/> />
<Route path={`${match.path}`} exact render={() => <Redirect to={`${match.url}/board`} />} /> <Route path={`${match.path}`} exact render={() => <Redirect to={`${match.url}/board`} />} />
@ -284,6 +616,7 @@ const Project = () => {
}} }}
onDeleteTask={deletedTask => { onDeleteTask={deletedTask => {
deleteTask({ variables: { taskID: deletedTask.id } }); deleteTask({ variables: { taskID: deletedTask.id } });
history.push(`${match.url}/board`);
}} }}
onOpenAddLabelPopup={(task, $targetRef) => { onOpenAddLabelPopup={(task, $targetRef) => {
taskLabelsRef.current = task.labels; taskLabelsRef.current = task.labels;

View File

@ -8,8 +8,6 @@ import {
useCreateProjectMutation, useCreateProjectMutation,
GetProjectsDocument, GetProjectsDocument,
GetProjectsQuery, GetProjectsQuery,
MeQuery,
MeDocument,
} from 'shared/generated/graphql'; } from 'shared/generated/graphql';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
@ -22,33 +20,8 @@ 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 NOOP from 'shared/utils/noop'; import NOOP from 'shared/utils/noop';
import theme from 'App/ThemeStyles';
const EmptyStateContent = styled.div` import { mixin } from '../shared/utils/styles';
display: flex;
justy-content: center;
align-items: center;
flex-direction: column;
`;
const EmptyStateTitle = styled.h3`
color: #fff;
font-size: 18px;
`;
const EmptyStatePrompt = styled.span`
color: rgba(${props => props.theme.colors.text.primary});
font-size: 16px;
margin-top: 8px;
`;
const EmptyState = styled(Empty)`
display: block;
margin: 0 auto;
`;
const CreateTeamButton = styled(Button)`
width: 100%;
`;
type CreateTeamData = { teamName: string }; type CreateTeamData = { teamName: string };
@ -58,6 +31,10 @@ type CreateTeamFormProps = {
const CreateTeamFormContainer = styled.form``; const CreateTeamFormContainer = styled.form``;
const CreateTeamButton = styled(Button)`
width: 100%;
`;
const CreateTeamForm: React.FC<CreateTeamFormProps> = ({ onCreateTeam }) => { const CreateTeamForm: React.FC<CreateTeamFormProps> = ({ onCreateTeam }) => {
const { register, handleSubmit } = useForm<CreateTeamData>(); const { register, handleSubmit } = useForm<CreateTeamData>();
const createTeam = (data: CreateTeamData) => { const createTeam = (data: CreateTeamData) => {
@ -79,7 +56,7 @@ const CreateTeamForm: React.FC<CreateTeamFormProps> = ({ onCreateTeam }) => {
}; };
const ProjectAddTile = styled.div` const ProjectAddTile = styled.div`
background-color: rgba(${props => 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;
@ -201,7 +178,7 @@ const SectionActionLink = styled(Link)`
const ProjectSectionTitle = styled.h3` const ProjectSectionTitle = styled.h3`
font-size: 16px; font-size: 16px;
color: rgba(${props => props.theme.colors.text.primary}); color: ${props => props.theme.colors.text.primary};
`; `;
const ProjectsContainer = styled.div` const ProjectsContainer = styled.div`
@ -211,13 +188,6 @@ const ProjectsContainer = styled.div`
min-width: 288px; min-width: 288px;
`; `;
const ProjectGrid = styled.div`
max-width: 780px;
display: grid;
grid-template-columns: 240px 240px 240px;
gap: 20px 10px;
`;
const AddTeamButton = styled(Button)` const AddTeamButton = styled(Button)`
padding: 6px 12px; padding: 6px 12px;
position: absolute; position: absolute;
@ -225,10 +195,6 @@ const AddTeamButton = styled(Button)`
right: 12px; right: 12px;
`; `;
const CreateFirstTeam = styled(Button)`
margin-top: 8px;
`;
type ShowNewProject = { type ShowNewProject = {
open: boolean; open: boolean;
initialTeamID: null | string; initialTeamID: null | string;
@ -265,10 +231,17 @@ const Projects = () => {
return <GlobalTopNavbar onSaveProjectName={NOOP} projectID={null} name={null} />; return <GlobalTopNavbar onSaveProjectName={NOOP} projectID={null} name={null} />;
} }
const colors = ['#e362e3', '#7a6ff0', '#37c5ab', '#aa62e3', '#e8384f']; const colors = theme.colors.multiColors;
if (data && user) { if (data && user) {
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
.filter(p => p.team === null)
.sort((a, b) => {
const textA = a.name.toUpperCase();
const textB = b.name.toUpperCase();
return textA < textB ? -1 : textA > textB ? 1 : 0; // eslint-disable-line no-nested-ternary
});
const projectTeams = teams const projectTeams = teams
.sort((a, b) => { .sort((a, b) => {
const textA = a.name.toUpperCase(); const textA = a.name.toUpperCase();
@ -280,7 +253,7 @@ const Projects = () => {
id: team.id, id: team.id,
name: team.name, name: team.name,
projects: projects projects: projects
.filter(project => 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();
@ -321,39 +294,35 @@ const Projects = () => {
Add Team Add Team
</AddTeamButton> </AddTeamButton>
)} )}
{projectTeams.length === 0 && ( <div>
<EmptyStateContent> <ProjectSectionTitleWrapper>
<EmptyState width={425} height={425} /> <ProjectSectionTitle>Personal Projects</ProjectSectionTitle>
<EmptyStateTitle>No teams exist</EmptyStateTitle> </ProjectSectionTitleWrapper>
<EmptyStatePrompt>Create a new team to get started</EmptyStatePrompt> <ProjectList>
<CreateFirstTeam {personalProjects.map((project, idx) => (
variant="outline" <ProjectListItem key={project.id}>
onClick={$target => { <ProjectTile color={colors[idx % 5]} to={`/projects/${project.id}`}>
showPopup( <ProjectTileFade />
$target, <ProjectTileDetails>
<Popup <ProjectTileName>{project.name}</ProjectTileName>
title="Create team" </ProjectTileDetails>
tab={0} </ProjectTile>
onClose={() => { </ProjectListItem>
hidePopup(); ))}
}} <ProjectListItem>
> <ProjectAddTile
<CreateTeamForm onClick={() => {
onCreateTeam={teamName => { setShowNewProject({ open: true, initialTeamID: 'no-team' });
if (organizationID) { }}
createTeam({ variables: { name: teamName, organizationID } }); >
hidePopup(); <ProjectTileFade />
} <ProjectAddTileDetails>
}} <ProjectTileName centered>Create new project</ProjectTileName>
/> </ProjectAddTileDetails>
</Popup>, </ProjectAddTile>
); </ProjectListItem>
}} </ProjectList>
> </div>
Create new team
</CreateFirstTeam>
</EmptyStateContent>
)}
{projectTeams.map(team => { {projectTeams.map(team => {
return ( return (
<div key={team.id}> <div key={team.id}>
@ -407,7 +376,7 @@ const Projects = () => {
initialTeamID={showNewProject.initialTeamID} initialTeamID={showNewProject.initialTeamID}
onCreateProject={(name, teamID) => { onCreateProject={(name, teamID) => {
if (user) { if (user) {
createProject({ variables: { teamID, name, userID: user.id } }); createProject({ variables: { teamID, name } });
setShowNewProject({ open: false, initialTeamID: null }); setShowNewProject({ open: false, initialTeamID: null });
} }
}} }}

View File

@ -0,0 +1,13 @@
import styled from 'styled-components';
export const Container = styled.div`
display: flex;
align-items: center;
justify-content: center;
width: 100vw;
height: 100vh;
`;
export const LoginWrapper = styled.div`
width: 60%;
`;

View File

@ -0,0 +1,62 @@
import React, { useState } from 'react';
import axios from 'axios';
import Register from 'shared/components/Register';
import { useHistory, useLocation } from 'react-router';
import * as QueryString from 'query-string';
import { toast } from 'react-toastify';
import { Container, LoginWrapper } from './Styles';
const UsersRegister = () => {
const history = useHistory();
const location = useLocation();
const [registered, setRegistered] = useState(false);
const params = QueryString.parse(location.search);
return (
<Container>
<LoginWrapper>
<Register
registered={registered}
onSubmit={(data, setComplete, setError) => {
if (data.password !== data.password_confirm) {
setError('password', { type: 'error', message: 'Passwords must match' });
setError('password_confirm', { type: 'error', message: 'Passwords must match' });
} else {
// TODO: change to fetch?
fetch('/auth/register', {
method: 'POST',
body: JSON.stringify({
user: {
username: data.username,
roleCode: 'admin',
email: data.email,
password: data.password,
initials: data.initials,
fullname: data.fullname,
},
}),
})
.then(async x => {
const response = await x.json();
const { setup } = response;
console.log(response);
if (setup) {
history.replace(`/confirm?confirmToken=xxxx`);
} else if (params.confirmToken) {
history.replace(`/confirm?confirmToken=${params.confirmToken}`);
} else {
setRegistered(true);
}
})
.catch(() => {
toast('There was an issue trying to register');
});
}
setComplete(true);
}}
/>
</LoginWrapper>
</Container>
);
};
export default UsersRegister;

View File

@ -21,6 +21,7 @@ import TaskAssignee from 'shared/components/TaskAssignee';
import Member from 'shared/components/Member'; import Member from 'shared/components/Member';
import ControlledInput from 'shared/components/ControlledInput'; import ControlledInput from 'shared/components/ControlledInput';
import NOOP from 'shared/utils/noop'; import NOOP from 'shared/utils/noop';
import { mixin } from 'shared/utils/styles';
const MemberListWrapper = styled.div` const MemberListWrapper = styled.div`
flex: 1 1; flex: 1 1;
@ -34,7 +35,7 @@ const UserMember = styled(Member)`
padding: 4px 0; padding: 4px 0;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background: rgba(${props => props.theme.colors.bg.primary}, 0.4); background: ${props => mixin.rgba(props.theme.colors.bg.primary, 0.4)};
} }
border-radius: 6px; border-radius: 6px;
`; `;
@ -119,12 +120,12 @@ export const MiniProfileActionItem = styled.span<{ disabled?: boolean }>`
? css` ? css`
user-select: none; user-select: none;
pointer-events: none; pointer-events: none;
color: rgba(${props.theme.colors.text.primary}, 0.4); color: ${mixin.rgba(props.theme.colors.text.primary, 0.4)};
` `
: css` : css`
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background: rgb(115, 103, 240); background: ${props.theme.colors.primary};
} }
`} `}
`; `;
@ -135,7 +136,7 @@ export const Content = styled.div`
export const CurrentPermission = styled.span` export const CurrentPermission = styled.span`
margin-left: 4px; margin-left: 4px;
color: rgba(${props => 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`
@ -146,13 +147,13 @@ export const Separator = styled.div`
export const WarningText = styled.span` export const WarningText = styled.span`
display: flex; display: flex;
color: rgba(${props => 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: rgba(${props => props.theme.colors.text.primary}); color: ${props => props.theme.colors.text.primary};
`; `;
export const RemoveMemberButton = styled(Button)` export const RemoveMemberButton = styled(Button)`
@ -305,14 +306,14 @@ const MemberItemOption = styled(Button)`
`; `;
const MemberList = styled.div` const MemberList = styled.div`
border-top: 1px solid rgba(${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 rgba(${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;
@ -336,11 +337,11 @@ const MemberProfile = styled(TaskAssignee)`
`; `;
const MemberItemName = styled.p` const MemberItemName = styled.p`
color: rgba(${props => props.theme.colors.text.secondary}); color: ${props => props.theme.colors.text.secondary};
`; `;
const MemberItemUsername = styled.p` const MemberItemUsername = styled.p`
color: rgba(${props => props.theme.colors.text.primary}); color: ${props => props.theme.colors.text.primary};
`; `;
const MemberListHeader = styled.div` const MemberListHeader = styled.div`
@ -349,12 +350,12 @@ const MemberListHeader = styled.div`
`; `;
const ListTitle = styled.h3` const ListTitle = styled.h3`
font-size: 18px; font-size: 18px;
color: rgba(${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: rgba(${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;
@ -386,11 +387,11 @@ const FilterTabItem = styled.li`
font-weight: 700; font-weight: 700;
text-decoration: none; text-decoration: none;
padding: 6px 8px; padding: 6px 8px;
color: rgba(${props => props.theme.colors.text.primary}); color: ${props => props.theme.colors.text.primary};
&:hover { &:hover {
border-radius: 6px; border-radius: 6px;
background: rgba(${props => props.theme.colors.primary}); background: ${props => props.theme.colors.primary};
color: rgba(${props => props.theme.colors.text.secondary}); color: ${props => props.theme.colors.text.secondary};
} }
`; `;

View File

@ -8,6 +8,7 @@ import {
} from 'shared/generated/graphql'; } from 'shared/generated/graphql';
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';
const FilterSearch = styled(Input)` const FilterSearch = styled(Input)`
margin: 0; margin: 0;
@ -34,11 +35,11 @@ const FilterTabItem = styled.li`
font-weight: 700; font-weight: 700;
text-decoration: none; text-decoration: none;
padding: 6px 8px; padding: 6px 8px;
color: rgba(${props => props.theme.colors.text.primary}); color: ${props => props.theme.colors.text.primary};
&:hover { &:hover {
border-radius: 6px; border-radius: 6px;
background: rgba(${props => props.theme.colors.primary}); background: ${props => props.theme.colors.primary};
color: rgba(${props => props.theme.colors.text.secondary}); color: ${props => props.theme.colors.text.secondary};
} }
`; `;
@ -55,7 +56,7 @@ const FilterTabTitle = styled.h2`
`; `;
const ProjectAddTile = styled.div` const ProjectAddTile = styled.div`
background-color: rgba(${props => props.theme.colors.bg.primary}, 0.4); background-color: ${props => props.theme.colors.bg.primary};
background-size: cover; background-size: cover;
background-position: 50%; background-position: 50%;
color: #fff; color: #fff;
@ -147,7 +148,7 @@ const ProjectListWrapper = styled.div`
flex: 1 1; flex: 1 1;
`; `;
const colors = ['#e362e3', '#7a6ff0', '#37c5ab', '#aa62e3', '#e8384f']; const colors = theme.colors.multiColors;
type TeamProjectsProps = { type TeamProjectsProps = {
teamID: string; teamID: string;

View File

@ -8,15 +8,28 @@ import { HttpLink } from 'apollo-link-http';
import { onError } from 'apollo-link-error'; import { onError } from 'apollo-link-error';
import { enableMapSet } from 'immer'; import { enableMapSet } from 'immer';
import { ApolloLink, Observable, fromPromise } from 'apollo-link'; import { ApolloLink, Observable, fromPromise } from 'apollo-link';
import moment from 'moment'; import dayjs from 'dayjs';
import updateLocale from 'dayjs/plugin/updateLocale';
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import isBetween from 'dayjs/plugin/isBetween';
import weekday from 'dayjs/plugin/weekday';
import { getAccessToken, getNewToken, setAccessToken } from 'shared/utils/accessToken'; 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';
// https://able.bio/AnasT/apollo-graphql-async-access-token-refresh--470t1c8 // https://able.bio/AnasT/apollo-graphql-async-access-token-refresh--470t1c8
dayjs.extend(isSameOrAfter);
dayjs.extend(weekday);
dayjs.extend(isBetween);
dayjs.extend(customParseFormat);
enableMapSet(); enableMapSet();
moment.updateLocale('en', { dayjs.extend(updateLocale);
dayjs.updateLocale('en', {
week: { week: {
dow: 1, // First day of week is Monday dow: 1, // First day of week is Monday
doy: 7, // First week of year must contain 1 January (7 + 1 - 1) doy: 7, // First week of year must contain 1 January (7 + 1 - 1)

0
frontend/src/outline.d.ts vendored Normal file
View File

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import theme from 'App/ThemeStyles';
import AddList from '.'; import AddList from '.';
export default { export default {
@ -7,7 +8,7 @@ export default {
title: 'AddList', title: 'AddList',
parameters: { parameters: {
backgrounds: [ backgrounds: [
{ name: 'gray', value: '#262c49', default: true }, { name: 'gray', value: theme.colors.bg.secondary, default: true },
{ name: 'white', value: '#ffffff' }, { name: 'white', value: '#ffffff' },
], ],
}, },

View File

@ -67,7 +67,7 @@ export const ListNameEditorWrapper = styled.div`
display: flex; display: flex;
`; `;
export const ListNameEditor = styled(TextareaAutosize)` export const ListNameEditor = styled(TextareaAutosize)`
background-color: ${props => mixin.lighten('#262c49', 0.05)}; background-color: ${props => mixin.lighten(props.theme.colors.bg.secondary, 0.05)};
border: none; border: none;
box-shadow: inset 0 0 0 2px #0079bf; box-shadow: inset 0 0 0 2px #0079bf;
transition: margin 85ms ease-in, background 85ms ease-in; transition: margin 85ms ease-in, background 85ms ease-in;
@ -91,7 +91,7 @@ export const ListNameEditor = styled(TextareaAutosize)`
color: #c2c6dc; color: #c2c6dc;
l &:focus { l &:focus {
background-color: ${props => mixin.lighten('#262c49', 0.05)}; background-color: ${props => mixin.lighten(props.theme.colors.bg.secondary, 0.05)};
} }
`; `;

View File

@ -51,7 +51,9 @@ export const Default = () => {
}, },
}, },
]} ]}
invitedUsers={[]}
onAddUser={action('add user')} onAddUser={action('add user')}
onDeleteInvitedUser={action('delete invited user')}
/> />
</ThemeProvider> </ThemeProvider>
</> </>

View File

@ -8,6 +8,7 @@ import { RoleCode, useUpdateUserRoleMutation } from 'shared/generated/graphql';
import Input from 'shared/components/Input'; import Input from 'shared/components/Input';
import Button from 'shared/components/Button'; import Button from 'shared/components/Button';
import NOOP from 'shared/utils/noop'; import NOOP from 'shared/utils/noop';
import { mixin } from 'shared/utils/styles';
export const RoleCheckmark = styled(Checkmark)` export const RoleCheckmark = styled(Checkmark)`
padding-left: 4px; padding-left: 4px;
@ -58,12 +59,12 @@ export const MiniProfileActionItem = styled.span<{ disabled?: boolean }>`
? css` ? css`
user-select: none; user-select: none;
pointer-events: none; pointer-events: none;
color: rgba(${props.theme.colors.text.primary}, 0.4); color: ${mixin.rgba(props.theme.colors.text.primary, 0.4)};
` `
: css` : css`
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background: rgb(115, 103, 240); background: ${props.theme.colors.primary};
} }
`} `}
`; `;
@ -74,7 +75,7 @@ export const Content = styled.div`
export const CurrentPermission = styled.span` export const CurrentPermission = styled.span`
margin-left: 4px; margin-left: 4px;
color: rgba(${props => 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`
@ -85,13 +86,13 @@ export const Separator = styled.div`
export const WarningText = styled.span` export const WarningText = styled.span`
display: flex; display: flex;
color: rgba(${props => 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: rgba(${props => props.theme.colors.text.primary}); color: ${props => props.theme.colors.text.primary};
`; `;
export const RemoveMemberButton = styled(Button)` export const RemoveMemberButton = styled(Button)`
@ -104,8 +105,8 @@ type TeamRoleManagerPopupProps = {
user: User; user: User;
users: Array<User>; users: Array<User>;
warning?: string | null; warning?: string | null;
canChangeRole: boolean; canChangeRole?: boolean;
onChangeRole: (roleCode: RoleCode) => void; onChangeRole?: (roleCode: RoleCode) => void;
updateUserPassword?: (user: TaskUser, password: string) => void; updateUserPassword?: (user: TaskUser, password: string) => void;
onDeleteUser?: (userID: string, newOwnerID: string | null) => void; onDeleteUser?: (userID: string, newOwnerID: string | null) => void;
}; };
@ -333,14 +334,14 @@ const MemberItemOption = styled(Button)`
`; `;
const MemberList = styled.div` const MemberList = styled.div`
border-top: 1px solid rgba(${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 rgba(${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;
@ -364,11 +365,11 @@ const MemberProfile = styled(TaskAssignee)`
`; `;
const MemberItemName = styled.p` const MemberItemName = styled.p`
color: rgba(${props => props.theme.colors.text.secondary}); color: ${props => props.theme.colors.text.secondary};
`; `;
const MemberItemUsername = styled.p` const MemberItemUsername = styled.p`
color: rgba(${props => props.theme.colors.text.primary}); color: ${props => props.theme.colors.text.primary};
`; `;
const MemberListHeader = styled.div` const MemberListHeader = styled.div`
@ -377,12 +378,12 @@ const MemberListHeader = styled.div`
`; `;
const ListTitle = styled.h3` const ListTitle = styled.h3`
font-size: 18px; font-size: 18px;
color: rgba(${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: rgba(${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;
@ -443,17 +444,17 @@ const TabNavItemButton = styled.button<{ active: boolean }>`
width: 100%; width: 100%;
position: relative; position: relative;
color: ${props => (props.active ? 'rgba(115, 103, 240)' : '#c2c6dc')}; color: ${props => (props.active ? `${props.theme.colors.secondary}` : props.theme.colors.text.primary)};
&:hover { &:hover {
color: rgba(115, 103, 240); color: ${props => `${props.theme.colors.primary}`};
} }
&:hover svg { &:hover svg {
fill: rgba(115, 103, 240); fill: ${props => props.theme.colors.primary};
} }
`; `;
const TabItemUser = styled(User)<{ active: boolean }>` const TabItemUser = styled(User)<{ active: boolean }>`
fill: ${props => (props.active ? 'rgba(115, 103, 240)' : '#c2c6dc')} fill: ${props => (props.active ? `${props.theme.colors.primary}` : props.theme.colors.text.primary)}
stroke: ${props => (props.active ? 'rgba(115, 103, 240)' : '#c2c6dc')} stroke: ${props => (props.active ? `${props.theme.colors.primary}` : props.theme.colors.text.primary)}
`; `;
const TabNavItemSpan = styled.span` const TabNavItemSpan = styled.span`
@ -470,8 +471,8 @@ const TabNavLine = styled.span<{ top: number }>`
transform: scaleX(1); transform: scaleX(1);
top: ${props => props.top}px; top: ${props => props.top}px;
background: linear-gradient(30deg, rgba(115, 103, 240), rgba(115, 103, 240)); background: linear-gradient(30deg, ${props => props.theme.colors.primary}, ${props => props.theme.colors.primary});
box-shadow: 0 0 8px 0 rgba(115, 103, 240); 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;
@ -530,8 +531,10 @@ type AdminProps = {
onDeleteUser: (userID: string, newOwnerID: string | null) => void; onDeleteUser: (userID: string, newOwnerID: string | null) => void;
onInviteUser: ($target: React.RefObject<HTMLElement>) => void; onInviteUser: ($target: React.RefObject<HTMLElement>) => void;
users: Array<User>; users: Array<User>;
invitedUsers: Array<InvitedUserAccount>;
canInviteUser: boolean; canInviteUser: boolean;
onUpdateUserPassword: (user: TaskUser, password: string) => void; onUpdateUserPassword: (user: TaskUser, password: string) => void;
onDeleteInvitedUser: (invitedUserID: string) => void;
}; };
const Admin: React.FC<AdminProps> = ({ const Admin: React.FC<AdminProps> = ({
@ -540,7 +543,9 @@ const Admin: React.FC<AdminProps> = ({
onUpdateUserPassword, onUpdateUserPassword,
canInviteUser, canInviteUser,
onDeleteUser, onDeleteUser,
onDeleteInvitedUser,
onInviteUser, onInviteUser,
invitedUsers,
users, users,
}) => { }) => {
const warning = const warning =
@ -577,7 +582,7 @@ const Admin: React.FC<AdminProps> = ({
<TabContent> <TabContent>
<MemberListWrapper> <MemberListWrapper>
<MemberListHeader> <MemberListHeader>
<ListTitle>{`Members (${users.length})`}</ListTitle> <ListTitle>{`Members (${users.length + invitedUsers.length})`}</ListTitle>
<ListDesc> <ListDesc>
Organization admins can create / manage / delete all projects & teams. Members only have access to teams Organization admins can create / manage / delete all projects & teams. Members only have access to teams
or projects they have been added to. or projects they have been added to.
@ -635,6 +640,65 @@ const Admin: React.FC<AdminProps> = ({
</MemberListItem> </MemberListItem>
); );
})} })}
{invitedUsers.map(member => {
return (
<MemberListItem>
<MemberProfile
showRoleIcons
size={32}
onMemberProfile={NOOP}
member={{
id: member.id,
fullName: member.email,
profileIcon: {
bgColor: '#fff',
url: null,
initials: member.email.charAt(0),
},
}}
/>
<MemberListItemDetails>
<MemberItemName>{member.email}</MemberItemName>
<MemberItemUsername>Invited</MemberItemUsername>
</MemberListItemDetails>
<MemberItemOptions>
<MemberItemOption
variant="outline"
onClick={$target => {
showPopup(
$target,
<TeamRoleManagerPopup
user={{
id: member.id,
fullName: member.email,
profileIcon: {
bgColor: '#fff',
url: null,
initials: member.email.charAt(0),
},
member: {
teams: [],
projects: [],
},
owned: {
teams: [],
projects: [],
},
}}
users={users}
onDeleteUser={() => {
onDeleteInvitedUser(member.id);
}}
/>,
);
}}
>
Manage
</MemberItemOption>
</MemberItemOptions>
</MemberListItem>
);
})}
</MemberList> </MemberList>
</MemberListWrapper> </MemberListWrapper>
</TabContent> </TabContent>

View File

@ -1,5 +1,6 @@
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import styled, { css } from 'styled-components/macro'; import styled, { css } from 'styled-components/macro';
import { mixin } from '../../utils/styles';
const Text = styled.span<{ fontSize: string; justifyTextContent: string; hasIcon?: boolean }>` const Text = styled.span<{ fontSize: string; justifyTextContent: string; hasIcon?: boolean }>`
position: relative; position: relative;
@ -8,7 +9,7 @@ const Text = styled.span<{ fontSize: string; justifyTextContent: string; hasIcon
justify-content: ${props => props.justifyTextContent}; justify-content: ${props => props.justifyTextContent};
transition: all 0.2s ease; transition: all 0.2s ease;
font-size: ${props => props.fontSize}; font-size: ${props => props.fontSize};
color: rgba(${props => props.theme.colors.text.secondary}); color: ${props => props.theme.colors.text.secondary};
${props => ${props =>
props.hasIcon && props.hasIcon &&
css` css`
@ -35,32 +36,37 @@ const Base = styled.button<{ color: string; disabled: boolean }>`
`} `}
`; `;
const Filled = styled(Base)` const Filled = styled(Base)<{ hoverVariant: HoverVariant }>`
background: rgba(${props => props.theme.colors[props.color]}); background: ${props => props.theme.colors[props.color]};
&:hover { ${props =>
box-shadow: 0 8px 25px -8px rgba(${props => props.theme.colors[props.color]}); props.hoverVariant === 'boxShadow' &&
} css`
&:hover {
box-shadow: 0 8px 25px -8px ${props.theme.colors[props.color]};
}
`}
`; `;
const Outline = styled(Base)<{ invert: boolean }>` const Outline = styled(Base)<{ invert: boolean }>`
border: 1px solid rgba(${props => props.theme.colors[props.color]}); border: 1px solid ${props => props.theme.colors[props.color]};
background: transparent; background: transparent;
${props => ${props =>
props.invert props.invert
? css` ? css`
background: rgba(${props.theme.colors[props.color]}); background: ${props.theme.colors[props.color]});
& ${Text} { & ${Text} {
color: rgba(${props.theme.colors.text.secondary}); color: ${props.theme.colors.text.secondary});
} }
&:hover { &:hover {
background: rgba(${props.theme.colors[props.color]}, 0.8); background: ${mixin.rgba(props.theme.colors[props.color], 0.8)};
} }
` `
: css` : css`
& ${Text} { & ${Text} {
color: rgba(${props.theme.colors[props.color]}); color: ${props.theme.colors[props.color]});
} }
&:hover { &:hover {
background: rgba(${props.theme.colors[props.color]}, 0.08); background: ${mixin.rgba(props.theme.colors[props.color], 0.08)};
} }
`} `}
`; `;
@ -68,7 +74,7 @@ const Outline = styled(Base)<{ invert: boolean }>`
const Flat = styled(Base)` const Flat = styled(Base)`
background: transparent; background: transparent;
&:hover { &:hover {
background: rgba(${props => props.theme.colors[props.color]}, 0.2); background: ${props => mixin.rgba(props.theme.colors[props.color], 0.2)};
} }
`; `;
@ -81,7 +87,7 @@ const LineX = styled.span<{ color: string }>`
bottom: -2px; bottom: -2px;
left: 50%; left: 50%;
transform: translate(-50%); transform: translate(-50%);
background: rgba(${props => props.theme.colors[props.color]}, 1); background: ${props => mixin.rgba(props.theme.colors[props.color], 1)};
`; `;
const LineDown = styled(Base)` const LineDown = styled(Base)`
@ -90,7 +96,7 @@ const LineDown = styled(Base)`
border-width: 0; border-width: 0;
border-style: solid; border-style: solid;
border-bottom-width: 2px; border-bottom-width: 2px;
border-color: rgba(${props => props.theme.colors[props.color]}, 0.2); border-color: ${props => mixin.rgba(props.theme.colors[props.color], 0.2)};
&:hover ${LineX} { &:hover ${LineX} {
width: 100%; width: 100%;
@ -103,8 +109,8 @@ const LineDown = styled(Base)`
const Gradient = styled(Base)` const Gradient = styled(Base)`
background: linear-gradient( background: linear-gradient(
30deg, 30deg,
rgba(${props => props.theme.colors[props.color]}, 1), ${props => mixin.rgba(props.theme.colors[props.color], 1)},
rgba(${props => props.theme.colors[props.color]}, 0.5) ${props => mixin.rgba(props.theme.colors[props.color], 0.5)}
); );
text-shadow: 1px 2px 4px rgba(0, 0, 0, 0.3); text-shadow: 1px 2px 4px rgba(0, 0, 0, 0.3);
&:hover { &:hover {
@ -113,7 +119,7 @@ const Gradient = styled(Base)`
`; `;
const Relief = styled(Base)` const Relief = styled(Base)`
background: rgba(${props => props.theme.colors[props.color]}, 1); background: ${props => mixin.rgba(props.theme.colors[props.color], 1)};
-webkit-box-shadow: 0 -3px 0 0 rgba(0, 0, 0, 0.2) inset; -webkit-box-shadow: 0 -3px 0 0 rgba(0, 0, 0, 0.2) inset;
box-shadow: inset 0 -3px 0 0 rgba(0, 0, 0, 0.2); box-shadow: inset 0 -3px 0 0 rgba(0, 0, 0, 0.2);
@ -123,9 +129,11 @@ const Relief = styled(Base)`
} }
`; `;
type HoverVariant = 'boxShadow' | 'none';
type ButtonProps = { type ButtonProps = {
fontSize?: string; fontSize?: string;
variant?: 'filled' | 'outline' | 'flat' | 'lineDown' | 'gradient' | 'relief'; variant?: 'filled' | 'outline' | 'flat' | 'lineDown' | 'gradient' | 'relief';
hoverVariant?: HoverVariant;
color?: 'primary' | 'danger' | 'success' | 'warning' | 'dark'; color?: 'primary' | 'danger' | 'success' | 'warning' | 'dark';
disabled?: boolean; disabled?: boolean;
type?: 'button' | 'submit'; type?: 'button' | 'submit';
@ -142,6 +150,7 @@ const Button: React.FC<ButtonProps> = ({
invert = false, invert = false,
color = 'primary', color = 'primary',
variant = 'filled', variant = 'filled',
hoverVariant = 'boxShadow',
type = 'button', type = 'button',
justifyTextContent = 'center', justifyTextContent = 'center',
icon, icon,
@ -158,7 +167,15 @@ const Button: React.FC<ButtonProps> = ({
switch (variant) { switch (variant) {
case 'filled': case 'filled':
return ( return (
<Filled ref={$button} type={type} onClick={handleClick} className={className} disabled={disabled} color={color}> <Filled
ref={$button}
hoverVariant={hoverVariant}
type={type}
onClick={handleClick}
className={className}
disabled={disabled}
color={color}
>
{icon && icon} {icon && icon}
<Text hasIcon={typeof icon !== 'undefined'} justifyTextContent={justifyTextContent} fontSize={fontSize}> <Text hasIcon={typeof icon !== 'undefined'} justifyTextContent={justifyTextContent} fontSize={fontSize}>
{children} {children}

View File

@ -5,8 +5,8 @@ import { CheckCircle, CheckSquareOutline, Clock } from 'shared/icons';
import TaskAssignee from 'shared/components/TaskAssignee'; import TaskAssignee from 'shared/components/TaskAssignee';
export const CardMember = styled(TaskAssignee)<{ zIndex: number }>` export const CardMember = styled(TaskAssignee)<{ zIndex: number }>`
box-shadow: 0 0 0 2px rgba(${props => props.theme.colors.bg.secondary}), box-shadow: 0 0 0 2px ${props => props.theme.colors.bg.secondary},
inset 0 0 0 1px rgba(${props => props.theme.colors.bg.secondary}, 0.07); inset 0 0 0 1px ${props => mixin.rgba(props.theme.colors.bg.secondary, 0.07)};
z-index: ${props => props.zIndex}; z-index: ${props => props.zIndex};
position: relative; position: relative;
`; `;
@ -14,8 +14,8 @@ export const ChecklistIcon = styled(CheckSquareOutline)<{ color: 'success' | 'no
${props => ${props =>
props.color === 'success' && props.color === 'success' &&
css` css`
fill: rgba(${props.theme.colors.success}); fill: ${props.theme.colors.success};
stroke: rgba(${props.theme.colors.success}); stroke: ${props.theme.colors.success};
`} `}
`; `;
export const ClockIcon = styled(Clock)<{ color: string }>` export const ClockIcon = styled(Clock)<{ color: string }>`
@ -38,7 +38,7 @@ export const EditorTextarea = styled(TextareaAutosize)`
padding: 0; padding: 0;
font-size: 14px; font-size: 14px;
line-height: 18px; line-height: 18px;
color: rgba(${props => props.theme.colors.text.primary}); color: ${props => props.theme.colors.text.primary};
&:focus { &:focus {
border: none; border: none;
outline: none; outline: none;
@ -89,7 +89,7 @@ export const ListCardBadgeText = styled.span<{ color?: 'success' | 'normal' }>`
padding: 0 4px 0 6px; padding: 0 4px 0 6px;
vertical-align: top; vertical-align: top;
white-space: nowrap; white-space: nowrap;
${props => props.color === 'success' && `color: rgba(${props.theme.colors.success});`} ${props => props.color === 'success' && `color: ${props.theme.colors.success};`}
`; `;
export const ListCardContainer = styled.div<{ isActive: boolean; editable: boolean }>` export const ListCardContainer = styled.div<{ isActive: boolean; editable: boolean }>`
@ -101,7 +101,9 @@ export const ListCardContainer = styled.div<{ isActive: boolean; editable: boole
position: relative; position: relative;
background-color: ${props => background-color: ${props =>
props.isActive && !props.editable ? mixin.darken('#262c49', 0.1) : `rgba(${props.theme.colors.bg.secondary})`}; props.isActive && !props.editable
? mixin.darken(props.theme.colors.bg.secondary, 0.1)
: `${props.theme.colors.bg.secondary}`};
`; `;
export const ListCardInnerContainer = styled.div` export const ListCardInnerContainer = styled.div`
@ -221,7 +223,7 @@ export const ListCardOperation = styled.span`
top: 2px; top: 2px;
z-index: 100; z-index: 100;
&:hover { &:hover {
background-color: ${props => mixin.darken('#262c49', 0.25)}; background-color: ${props => mixin.darken(props.theme.colors.bg.secondary, 0.25)};
} }
`; `;
@ -233,7 +235,7 @@ export const CardTitle = styled.span`
word-wrap: break-word; word-wrap: break-word;
line-height: 18px; line-height: 18px;
font-size: 14px; font-size: 14px;
color: rgba(${props => props.theme.colors.text.primary}); color: ${props => props.theme.colors.text.primary};
display: flex; display: flex;
align-items: center; align-items: center;
@ -246,7 +248,7 @@ export const CardMembers = styled.div`
`; `;
export const CompleteIcon = styled(CheckCircle)` export const CompleteIcon = styled(CheckCircle)`
fill: rgba(${props => props.theme.colors.success}); fill: ${props => props.theme.colors.success};
margin-right: 4px; margin-right: 4px;
flex-shrink: 0; flex-shrink: 0;
`; `;

View File

@ -26,10 +26,9 @@ const CardComposer = ({ isOpen, onCreateCard, onClose }: Props) => {
useOnOutsideClick($cardRef, true, onClose, null); useOnOutsideClick($cardRef, true, onClose, null);
useOnEscapeKeyDown(isOpen, onClose); useOnEscapeKeyDown(isOpen, onClose);
return ( return (
<CardComposerWrapper isOpen={isOpen}> <CardComposerWrapper isOpen={isOpen} ref={$cardRef}>
<Card <Card
title={cardName} title={cardName}
ref={$cardRef}
taskID="" taskID=""
taskGroupID="" taskGroupID=""
editable editable

View File

@ -22,7 +22,7 @@ export default {
const Container = styled.div` const Container = styled.div`
width: 552px; width: 552px;
margin: 25px; margin: 25px;
border: 1px solid rgba(${props => props.theme.colors.bg.primary}); border: 1px solid ${props => props.theme.colors.bg.primary};
`; `;
const defaultItems = [ const defaultItems = [

View File

@ -12,6 +12,7 @@ import Button from 'shared/components/Button';
import TextareaAutosize from 'react-autosize-textarea'; import TextareaAutosize from 'react-autosize-textarea';
import Control from 'react-select/src/components/Control'; import Control from 'react-select/src/components/Control';
import useOnOutsideClick from 'shared/hooks/onOutsideClick'; import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import { mixin } from 'shared/utils/styles';
const Wrapper = styled.div` const Wrapper = styled.div`
margin-bottom: 24px; margin-bottom: 24px;
@ -38,7 +39,7 @@ const WindowChecklistTitle = styled.div`
const WindowTitleText = styled.h3` const WindowTitleText = styled.h3`
cursor: pointer; cursor: pointer;
color: rgba(${props => props.theme.colors.text.primary}); color: ${props => props.theme.colors.text.primary};
margin: 6px 0; margin: 6px 0;
display: inline-block; display: inline-block;
width: auto; width: auto;
@ -73,7 +74,7 @@ const ChecklistProgressPercent = styled.span`
`; `;
const ChecklistProgressBar = styled.div` const ChecklistProgressBar = styled.div`
background: rgba(${props => props.theme.colors.bg.primary}); background: ${props => props.theme.colors.bg.primary};
border-radius: 4px; border-radius: 4px;
clear: both; clear: both;
height: 8px; height: 8px;
@ -83,7 +84,7 @@ const ChecklistProgressBar = styled.div`
`; `;
const ChecklistProgressBarCurrent = styled.div<{ width: number }>` const ChecklistProgressBarCurrent = styled.div<{ width: number }>`
width: ${props => props.width}%; width: ${props => props.width}%;
background: rgba(${props => (props.width === 100 ? props.theme.colors.success : props.theme.colors.primary)}); background: ${props => (props.width === 100 ? props.theme.colors.success : props.theme.colors.primary)};
bottom: 0; bottom: 0;
left: 0; left: 0;
position: absolute; position: absolute;
@ -111,7 +112,7 @@ const ChecklistIcon = styled.div`
`; `;
const ChecklistItemCheckedIcon = styled(CheckSquare)` const ChecklistItemCheckedIcon = styled(CheckSquare)`
fill: rgba(${props => props.theme.colors.primary}); fill: ${props => props.theme.colors.primary};
`; `;
const ChecklistItemDetails = styled.div` const ChecklistItemDetails = styled.div`
@ -133,7 +134,7 @@ const ChecklistItemTextControls = styled.div`
`; `;
const ChecklistItemText = styled.span<{ complete: boolean }>` const ChecklistItemText = styled.span<{ complete: boolean }>`
color: ${props => (props.complete ? '#5e6c84' : `rgba(${props.theme.colors.text.primary})`)}; color: ${props => (props.complete ? '#5e6c84' : `${props.theme.colors.text.primary}`)};
${props => props.complete && 'text-decoration: line-through;'} ${props => props.complete && 'text-decoration: line-through;'}
line-height: 20px; line-height: 20px;
font-size: 16px; font-size: 16px;
@ -155,14 +156,14 @@ const ControlButton = styled.div`
margin-left: 4px; margin-left: 4px;
padding: 4px 6px; padding: 4px 6px;
border-radius: 6px; border-radius: 6px;
background-color: rgba(${props => props.theme.colors.bg.primary}, 0.8); background-color: ${props => mixin.rgba(props.theme.colors.bg.primary, 0.8)};
display: flex; display: flex;
width: 32px; width: 32px;
height: 32px; height: 32px;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
&:hover { &:hover {
background-color: rgba(${props => props.theme.colors.primary}, 1); background-color: ${props => mixin.rgba(props.theme.colors.primary, 1)};
} }
`; `;
@ -189,27 +190,27 @@ export const ChecklistNameEditor = styled(TextareaAutosize)`
padding: 8px 12px; padding: 8px 12px;
font-size: 16px; font-size: 16px;
line-height: 20px; line-height: 20px;
border: 1px solid rgba(${props => props.theme.colors.primary}); border: 1px solid ${props => props.theme.colors.primary};
border-radius: 3px; border-radius: 3px;
color: rgba(${props => props.theme.colors.text.primary}); color: ${props => props.theme.colors.text.primary};
border-color: rgba(${props => props.theme.colors.border}); border-color: ${props => props.theme.colors.border};
background-color: rgba(${props => props.theme.colors.bg.primary}, 0.4); background-color: ${props => mixin.rgba(props.theme.colors.bg.primary, 0.4)};
&:focus { &:focus {
border-color: rgba(${props => props.theme.colors.primary}); border-color: ${props => props.theme.colors.primary};
} }
`; `;
const AssignUserButton = styled(AccountPlus)` const AssignUserButton = styled(AccountPlus)`
fill: rgba(${props => props.theme.colors.text.primary}); fill: ${props => props.theme.colors.text.primary};
`; `;
const ClockButton = styled(Clock)` const ClockButton = styled(Clock)`
fill: rgba(${props => props.theme.colors.text.primary}); fill: ${props => props.theme.colors.text.primary};
`; `;
const TrashButton = styled(Trash)` const TrashButton = styled(Trash)`
fill: rgba(${props => props.theme.colors.text.primary}); fill: ${props => props.theme.colors.text.primary};
`; `;
const ChecklistItemWrapper = styled.div<{ ref: any }>` const ChecklistItemWrapper = styled.div<{ ref: any }>`
@ -224,7 +225,7 @@ const ChecklistItemWrapper = styled.div<{ ref: any }>`
} }
&:hover { &:hover {
background-color: rgba(${props => props.theme.colors.bg.primary}, 0.4); background-color: ${props => mixin.rgba(props.theme.colors.bg.primary, 0.4)};
} }
&:hover ${ControlButton} { &:hover ${ControlButton} {
opacity: 1; opacity: 1;
@ -246,10 +247,10 @@ const CancelButton = styled.div`
cursor: pointer; cursor: pointer;
margin: 5px; margin: 5px;
& svg { & svg {
fill: rgba(${props => props.theme.colors.text.primary}); fill: ${props => props.theme.colors.text.primary};
} }
&:hover svg { &:hover svg {
fill: rgba(${props => props.theme.colors.text.secondary}); fill: ${props => props.theme.colors.text.secondary};
} }
`; `;
@ -265,7 +266,7 @@ const EditableDeleteButton = styled.button`
border-radius: 3px; border-radius: 3px;
&:hover { &:hover {
background: rgba(${props => props.theme.colors.primary}, 0.8); background: ${props => mixin.rgba(props.theme.colors.primary, 0.8)};
} }
`; `;

View File

@ -7,7 +7,7 @@ const LabelText = styled.span`
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: rgba(${props => props.theme.colors.text.primary}); color: ${props => props.theme.colors.text.primary};
`; `;
const Container = styled.div<{ color?: string }>` const Container = styled.div<{ color?: string }>`
@ -24,11 +24,11 @@ const Container = styled.div<{ color?: string }>`
? css` ? css`
background: ${props.color}; background: ${props.color};
& ${LabelText} { & ${LabelText} {
color: rgba(${props.theme.colors.text.secondary}); color: ${props.theme.colors.text.secondary};
} }
` `
: css` : css`
background: rgba(${props.theme.colors.bg.primary}); background: ${props.theme.colors.bg.primary};
`} `}
`; `;

View File

@ -0,0 +1,103 @@
import styled from 'styled-components';
import Button from 'shared/components/Button';
export const Wrapper = styled.div`
background: #eff2f7;
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
`;
export const Column = styled.div`
width: 50%;
display: flex;
justify-content: center;
align-items: center;
`;
export const LoginFormWrapper = styled.div`
background: #10163a;
width: 100%;
`;
export const LoginFormContainer = styled.div`
min-height: 505px;
padding: 2rem;
`;
export const Title = styled.h1`
color: #ebeefd;
font-size: 18px;
margin-bottom: 14px;
`;
export const SubTitle = styled.h2`
color: #c2c6dc;
font-size: 14px;
margin-bottom: 14px;
`;
export const Form = styled.form`
display: flex;
flex-direction: column;
`;
export const FormLabel = styled.label`
color: #c2c6dc;
font-size: 12px;
position: relative;
margin-top: 14px;
`;
export const FormTextInput = styled.input`
width: 100%;
background: #262c49;
border: 1px solid rgba(0, 0, 0, 0.2);
margin-top: 4px;
padding: 0.7rem 1rem 0.7rem 3rem;
font-size: 1rem;
color: #c2c6dc;
border-radius: 5px;
`;
export const FormIcon = styled.div`
top: 30px;
left: 16px;
position: absolute;
`;
export const FormError = styled.span`
font-size: 0.875rem;
color: rgb(234, 84, 85);
`;
export const LoginButton = styled(Button)``;
export const ActionButtons = styled.div`
margin-top: 17.5px;
display: flex;
justify-content: space-between;
`;
export const RegisterButton = styled(Button)``;
export const LogoTitle = styled.div`
font-size: 24px;
font-weight: 600;
margin-left: 12px;
transition: visibility, opacity, transform 0.25s ease;
color: #7367f0;
`;
export const LogoWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
flex-direction: row;
position: relative;
width: 100%;
padding-bottom: 16px;
margin-bottom: 24px;
color: rgb(222, 235, 255);
border-bottom: 1px solid rgba(65, 69, 97, 0.65);
`;

View File

@ -0,0 +1,62 @@
import React, { useState, useEffect } from 'react';
import AccessAccount from 'shared/undraw/AccessAccount';
import { User, Lock, Taskcafe } from 'shared/icons';
import { useForm } from 'react-hook-form';
import LoadingSpinner from 'shared/components/LoadingSpinner';
import {
Form,
LogoWrapper,
LogoTitle,
ActionButtons,
RegisterButton,
FormError,
FormIcon,
FormLabel,
FormTextInput,
Wrapper,
Column,
LoginFormWrapper,
LoginFormContainer,
Title,
SubTitle,
} from './Styles';
const Confirm = ({ onConfirmUser, hasConfirmToken }: ConfirmProps) => {
const [hasFailed, setFailed] = useState(false);
const setHasFailed = () => {
setFailed(true);
};
useEffect(() => {
onConfirmUser(setHasFailed);
});
return (
<Wrapper>
<Column>
<AccessAccount width={275} height={250} />
</Column>
<Column>
<LoginFormWrapper>
<LoginFormContainer>
<LogoWrapper>
<Taskcafe width={42} height={42} />
<LogoTitle>Taskcafé</LogoTitle>
</LogoWrapper>
{hasConfirmToken ? (
<>
<Title>Confirming user...</Title>
{hasFailed ? <SubTitle>There was an error while confirming your user</SubTitle> : <LoadingSpinner />}
</>
) : (
<>
<Title>There is no confirmation token</Title>
<SubTitle>There seems to have been an error.</SubTitle>
</>
)}
</LoginFormContainer>
</LoginFormWrapper>
</Column>
</Wrapper>
);
};
export default Confirm;

View File

@ -19,7 +19,7 @@ export default {
}; };
const Wrapper = styled.div` const Wrapper = styled.div`
background: rgba(${props => props.theme.colors.bg.primary}); background: ${props => props.theme.colors.bg.primary};
padding: 45px; padding: 45px;
margin: 25px; margin: 25px;
display: flex; display: flex;

View File

@ -1,5 +1,6 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import styled, { css } from 'styled-components/macro'; import styled, { css } from 'styled-components/macro';
import theme from '../../../App/ThemeStyles';
const InputWrapper = styled.div<{ width: string }>` const InputWrapper = styled.div<{ width: string }>`
position: relative; position: relative;
@ -57,14 +58,14 @@ const InputInput = styled.input<{
background: ${props => props.focusBg}; background: ${props => props.focusBg};
} }
&:focus ~ ${InputLabel} { &:focus ~ ${InputLabel} {
color: rgba(115, 103, 240); color: ${props => props.theme.colors.primary};
transform: translate(-3px, -90%); transform: translate(-3px, -90%);
} }
${props => ${props =>
props.hasValue && props.hasValue &&
css` css`
& ~ ${InputLabel} { & ~ ${InputLabel} {
color: rgba(115, 103, 240); color: ${props.theme.colors.primary};
transform: translate(-3px, -90%); transform: translate(-3px, -90%);
} }
`} `}
@ -115,8 +116,8 @@ const ControlledInput = ({
}: ControlledInputProps) => { }: ControlledInputProps) => {
const $input = useRef<HTMLInputElement>(null); const $input = useRef<HTMLInputElement>(null);
const [hasValue, setHasValue] = useState(false); const [hasValue, setHasValue] = useState(false);
const borderColor = variant === 'normal' ? 'rgba(0, 0, 0, 0.2)' : '#414561'; const borderColor = variant === 'normal' ? 'rgba(0, 0, 0, 0.2)' : theme.colors.alternate;
const focusBg = variant === 'normal' ? 'rgba(38, 44, 73, )' : 'rgba(16, 22, 58, 1)'; const focusBg = variant === 'normal' ? theme.colors.bg.secondary : theme.colors.bg.primary;
useEffect(() => { useEffect(() => {
if (autoFocus && $input && $input.current) { if (autoFocus && $input && $input.current) {
$input.current.focus(); $input.current.focus();

View File

@ -2,6 +2,7 @@ import React, { createRef, useState } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import DropdownMenu from '.'; import DropdownMenu from '.';
import theme from '../../../App/ThemeStyles';
export default { export default {
component: DropdownMenu, component: DropdownMenu,
@ -10,7 +11,7 @@ export default {
backgrounds: [ backgrounds: [
{ name: 'white', value: '#ffffff' }, { name: 'white', value: '#ffffff' },
{ name: 'gray', value: '#f8f8f8' }, { name: 'gray', value: '#f8f8f8' },
{ name: 'darkBlue', value: '#262c49', default: true }, { name: 'darkBlue', value: theme.colors.bg.secondary, default: true },
], ],
}, },
}; };

View File

@ -59,7 +59,7 @@ export const ActionItem = styled.li`
align-items: center; align-items: center;
font-size: 14px; font-size: 14px;
&:hover { &:hover {
background: rgb(115, 103, 240); background: ${props => props.theme.colors.primary};
} }
`; `;

View File

@ -19,23 +19,23 @@ display: flex
} }
& .react-datepicker-time__header { & .react-datepicker-time__header {
color: rgba(${props => props.theme.colors.text.primary}); color: ${props => props.theme.colors.text.primary};
} }
& .react-datepicker__time-list-item { & .react-datepicker__time-list-item {
color: rgba(${props => props.theme.colors.text.primary}); color: ${props => props.theme.colors.text.primary};
} }
& .react-datepicker__time-container .react-datepicker__time & .react-datepicker__time-container .react-datepicker__time
.react-datepicker__time-box ul.react-datepicker__time-list .react-datepicker__time-box ul.react-datepicker__time-list
li.react-datepicker__time-list-item:hover { li.react-datepicker__time-list-item:hover {
color: rgba(${props => props.theme.colors.text.secondary}); color: ${props => props.theme.colors.text.secondary};
background: rgba(${props => props.theme.colors.bg.secondary}); background: ${props => props.theme.colors.bg.secondary};
} }
& .react-datepicker__time-container .react-datepicker__time { & .react-datepicker__time-container .react-datepicker__time {
background: rgba(${props => props.theme.colors.bg.primary}); background: ${props => props.theme.colors.bg.primary};
} }
& .react-datepicker--time-only { & .react-datepicker--time-only {
background: rgba(${props => props.theme.colors.bg.primary}); background: ${props => props.theme.colors.bg.primary};
border: 1px solid rgba(${props => props.theme.colors.border}); border: 1px solid ${props => props.theme.colors.border};
} }
& .react-datepicker * { & .react-datepicker * {
@ -75,12 +75,12 @@ display: flex
} }
& .react-datepicker__day--selected { & .react-datepicker__day--selected {
border-radius: 50%; border-radius: 50%;
background: rgba(115, 103, 240); background: ${props => props.theme.colors.primary};
color: #fff; color: #fff;
} }
& .react-datepicker__day--selected:hover { & .react-datepicker__day--selected:hover {
border-radius: 50%; border-radius: 50%;
background: rgba(115, 103, 240); background: ${props => props.theme.colors.primary};
color: #fff; color: #fff;
} }
& .react-datepicker__header { & .react-datepicker__header {
@ -88,7 +88,7 @@ display: flex
border: none; border: none;
} }
& .react-datepicker__header--time { & .react-datepicker__header--time {
border-bottom: 1px solid rgba(${props => props.theme.colors.border}); border-bottom: 1px solid ${props => props.theme.colors.border};
} }
`; `;

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect, forwardRef } from 'react'; import React, { useState, useEffect, forwardRef } from 'react';
import moment from 'moment'; import dayjs from 'dayjs';
import styled from 'styled-components'; import styled from 'styled-components';
import DatePicker from 'react-datepicker'; import DatePicker from 'react-datepicker';
import _ from 'lodash'; import _ from 'lodash';
@ -43,7 +43,7 @@ const HeaderSelectLabel = styled.div`
color: #c2c6dc; color: #c2c6dc;
&:hover { &:hover {
background: rgba(115, 103, 240); background: ${props => props.theme.colors.primary};
color: #c2c6dc; color: #c2c6dc;
} }
`; `;
@ -60,8 +60,8 @@ const HeaderSelect = styled.select`
appearance: none; appearance: none;
&:hover { &:hover {
background: #262c49; background: ${props => props.theme.colors.bg.secondary};
border: 1px solid rgba(115, 103, 240); border: 1px solid ${props => props.theme.colors.primary};
outline: none !important; outline: none !important;
box-shadow: none; box-shadow: none;
color: #c2c6dc; color: #c2c6dc;
@ -93,7 +93,7 @@ const HeaderButton = styled.button`
border: none; border: none;
border-radius: 3px; border-radius: 3px;
&:hover { &:hover {
background: rgba(115, 103, 240); background: ${props => props.theme.colors.primary};
color: #fff; color: #fff;
} }
`; `;
@ -110,12 +110,12 @@ const HeaderActions = styled.div`
`; `;
const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange, onRemoveDueDate, onCancel }) => { const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange, onRemoveDueDate, onCancel }) => {
const now = moment(); const now = dayjs();
const { register, handleSubmit, errors, setValue, setError, formState, control } = useForm<DueDateFormData>(); const { register, handleSubmit, errors, setValue, setError, formState, control } = useForm<DueDateFormData>();
const [startDate, setStartDate] = useState(new Date()); const [startDate, setStartDate] = useState(new Date());
useEffect(() => { useEffect(() => {
const newDate = moment(startDate).format('YYYY-MM-DD'); const newDate = dayjs(startDate).format('YYYY-MM-DD');
setValue('endDate', newDate); setValue('endDate', newDate);
}, [startDate]); }, [startDate]);
@ -135,7 +135,7 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
'December', 'December',
]; ];
const saveDueDate = (data: any) => { const saveDueDate = (data: any) => {
const newDate = moment(`${data.endDate} ${moment(data.endTime).format('h:mm A')}`, 'YYYY-MM-DD h:mm A'); const newDate = dayjs(`${data.endDate} ${dayjs(data.endTime).format('h:mm A')}`, 'YYYY-MM-DD h:mm A');
if (newDate.isValid()) { if (newDate.isValid()) {
onDueDateChange(task, newDate.toDate()); onDueDateChange(task, newDate.toDate());
} }

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import styled, { keyframes } from 'styled-components/macro'; import styled, { keyframes } from 'styled-components/macro';
import { mixin } from 'shared/utils/styles'; import { mixin } from 'shared/utils/styles';
import theme from '../../../App/ThemeStyles';
export const BoardContainer = styled.div` export const BoardContainer = styled.div`
position: relative; position: relative;
@ -34,9 +35,9 @@ export const Container = styled.div`
white-space: nowrap; white-space: nowrap;
`; `;
export const defaultBaseColor = '#10163a'; export const defaultBaseColor = theme.colors.bg.primary;
export const defaultHighlightColor = mixin.lighten('#10163a', 0.25); export const defaultHighlightColor = mixin.lighten(theme.colors.bg.primary, 0.25);
export const skeletonKeyframes = keyframes` export const skeletonKeyframes = keyframes`
0% { 0% {

View File

@ -19,7 +19,7 @@ export default {
}; };
const Wrapper = styled.div` const Wrapper = styled.div`
background: rgba(${props => props.theme.colors.bg.primary}); background: ${props => props.theme.colors.bg.primary};
padding: 45px; padding: 45px;
margin: 25px; margin: 25px;
display: flex; display: flex;

View File

@ -1,5 +1,6 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import styled, { css } from 'styled-components/macro'; import styled, { css } from 'styled-components/macro';
import theme from '../../../App/ThemeStyles';
const InputWrapper = styled.div<{ width: string }>` const InputWrapper = styled.div<{ width: string }>`
position: relative; position: relative;
@ -53,18 +54,18 @@ const InputInput = styled.input<{
transition: all 0.3s ease; transition: all 0.3s ease;
&: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 ${props => props.theme.colors.primary};
background: ${props => props.focusBg}; background: ${props => props.focusBg};
} }
&:focus ~ ${InputLabel} { &:focus ~ ${InputLabel} {
color: rgba(115, 103, 240); color: ${props => props.theme.colors.primary};
transform: translate(-3px, -90%); transform: translate(-3px, -90%);
} }
${props => ${props =>
props.hasValue && props.hasValue &&
css` css`
& ~ ${InputLabel} { & ~ ${InputLabel} {
color: rgba(115, 103, 240); color: ${props.theme.colors.primary};
transform: translate(-3px, -90%); transform: translate(-3px, -90%);
} }
`} `}
@ -138,8 +139,8 @@ const Input = React.forwardRef(
$ref: any, $ref: any,
) => { ) => {
const [hasValue, setHasValue] = useState(defaultValue !== ''); const [hasValue, setHasValue] = useState(defaultValue !== '');
const borderColor = variant === 'normal' ? 'rgba(0, 0, 0, 0.2)' : '#414561'; const borderColor = variant === 'normal' ? 'rgba(0,0,0,0.2)' : theme.colors.alternate;
const focusBg = variant === 'normal' ? 'rgba(38, 44, 73, )' : 'rgba(16, 22, 58, 1)'; const focusBg = variant === 'normal' ? theme.colors.bg.secondary : theme.colors.bg.primary;
// Merge forwarded ref and internal ref in order to be able to access the ref in the useEffect // Merge forwarded ref and internal ref in order to be able to access the ref in the useEffect
// The forwarded ref is not accessible by itself, which is what the innerRef & combined ref is for // The forwarded ref is not accessible by itself, which is what the innerRef & combined ref is for

View File

@ -1,6 +1,5 @@
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
import TextareaAutosize from 'react-autosize-textarea'; import TextareaAutosize from 'react-autosize-textarea';
import { mixin } from 'shared/utils/styles';
export const Container = styled.div` export const Container = styled.div`
width: 272px; width: 272px;
@ -34,7 +33,7 @@ export const AddCardButton = styled.a`
&:hover { &:hover {
color: #c2c6dc; color: #c2c6dc;
text-decoration: none; text-decoration: none;
background: rgb(115, 103, 240); background: ${props => props.theme.colors.primary};
} }
`; `;
export const Wrapper = styled.div` export const Wrapper = styled.div`
@ -73,7 +72,6 @@ export const HeaderName = styled(TextareaAutosize)`
box-shadow: none; box-shadow: none;
font-weight: 600; font-weight: 600;
margin: -4px 0; margin: -4px 0;
padding: 4px 8px;
letter-spacing: normal; letter-spacing: normal;
word-spacing: normal; word-spacing: normal;
@ -97,7 +95,7 @@ export const Header = styled.div<{ isEditing: boolean }>`
props.isEditing && props.isEditing &&
css` css`
& ${HeaderName} { & ${HeaderName} {
box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px; box-shadow: ${props.theme.colors.primary} 0px 0px 0px 1px;
} }
`} `}
`; `;

View File

@ -21,7 +21,7 @@ export const ListActionItem = styled.span`
margin: 0 -12px; margin: 0 -12px;
text-decoration: none; text-decoration: none;
&:hover { &:hover {
background: rgb(115, 103, 240); background: ${props => props.theme.colors.primary};
} }
`; `;

View File

@ -1,5 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import theme from 'App/ThemeStyles';
import Lists from '.'; import Lists from '.';
export default { export default {
@ -7,7 +8,7 @@ export default {
title: 'Lists', title: 'Lists',
parameters: { parameters: {
backgrounds: [ backgrounds: [
{ name: 'gray', value: '#262c49', default: true }, { name: 'gray', value: theme.colors.bg.secondary, default: true },
{ name: 'white', value: '#ffffff' }, { name: 'white', value: '#ffffff' },
], ],
}, },

View File

@ -10,7 +10,7 @@ import {
getNewDraggablePosition, getNewDraggablePosition,
getAfterDropDraggableList, getAfterDropDraggableList,
} from 'shared/utils/draggables'; } from 'shared/utils/draggables';
import moment from 'moment'; import dayjs from 'dayjs';
import { TaskSorting, TaskSortingType, TaskSortingDirection, sortTasks } from 'shared/utils/sorting'; import { TaskSorting, TaskSortingType, TaskSortingDirection, sortTasks } from 'shared/utils/sorting';
import { Container, BoardContainer, BoardWrapper } from './Styles'; import { Container, BoardContainer, BoardWrapper } from './Styles';
@ -51,7 +51,7 @@ export type TaskStatusFilter = {
export interface TaskMetaFilterName { export interface TaskMetaFilterName {
meta: TaskMeta; meta: TaskMeta;
value?: string | moment.Moment | null; value?: string | dayjs.Dayjs | null;
id?: string | null; id?: string | null;
} }
@ -104,30 +104,30 @@ function shouldStatusFilter(task: Task, filter: TaskStatusFilter) {
return true; return true;
} }
if (filter.status === TaskStatus.COMPLETE && task.completedAt && task.complete === true) { if (filter.status === TaskStatus.COMPLETE && task.completedAt && task.complete === true) {
const completedAt = moment(task.completedAt); const completedAt = dayjs(task.completedAt);
const REFERENCE = moment(); // fixed just for testing, use moment(); const REFERENCE = dayjs();
switch (filter.since) { switch (filter.since) {
case TaskSince.TODAY: case TaskSince.TODAY:
const TODAY = REFERENCE.clone().startOf('day'); const TODAY = REFERENCE.clone().startOf('day');
return completedAt.isSame(TODAY, 'd'); return completedAt.isSame(TODAY, 'd');
case TaskSince.YESTERDAY: case TaskSince.YESTERDAY:
const YESTERDAY = REFERENCE.clone() const YESTERDAY = REFERENCE.clone()
.subtract(1, 'days') .subtract(1, 'day')
.startOf('day'); .startOf('day');
return completedAt.isSameOrAfter(YESTERDAY, 'd'); return completedAt.isSameOrAfter(YESTERDAY, 'd');
case TaskSince.ONE_WEEK: case TaskSince.ONE_WEEK:
const ONE_WEEK = REFERENCE.clone() const ONE_WEEK = REFERENCE.clone()
.subtract(7, 'days') .subtract(7, 'day')
.startOf('day'); .startOf('day');
return completedAt.isSameOrAfter(ONE_WEEK, 'd'); return completedAt.isSameOrAfter(ONE_WEEK, 'd');
case TaskSince.TWO_WEEKS: case TaskSince.TWO_WEEKS:
const TWO_WEEKS = REFERENCE.clone() const TWO_WEEKS = REFERENCE.clone()
.subtract(14, 'days') .subtract(14, 'day')
.startOf('day'); .startOf('day');
return completedAt.isSameOrAfter(TWO_WEEKS, 'd'); return completedAt.isSameOrAfter(TWO_WEEKS, 'd');
case TaskSince.THREE_WEEKS: case TaskSince.THREE_WEEKS:
const THREE_WEEKS = REFERENCE.clone() const THREE_WEEKS = REFERENCE.clone()
.subtract(21, 'days') .subtract(21, 'day')
.startOf('day'); .startOf('day');
return completedAt.isSameOrAfter(THREE_WEEKS, 'd'); return completedAt.isSameOrAfter(THREE_WEEKS, 'd');
default: default:
@ -353,7 +353,7 @@ const SimpleLists: React.FC<SimpleProps> = ({
task.dueDate task.dueDate
? { ? {
isPastDue: false, isPastDue: false,
formattedDate: moment(task.dueDate).format('MMM D, YYYY'), formattedDate: dayjs(task.dueDate).format('MMM D, YYYY'),
} }
: undefined : undefined
} }

View File

@ -1,5 +1,5 @@
import { TaskMetaFilters, DueDateFilterType } from 'shared/components/Lists'; import { TaskMetaFilters, DueDateFilterType } from 'shared/components/Lists';
import moment from 'moment'; import dayjs from 'dayjs';
enum ShouldFilter { enum ShouldFilter {
NO_FILTER, NO_FILTER,
@ -24,8 +24,8 @@ export default function shouldMetaFilter(task: Task, filters: TaskMetaFilters) {
isFiltered = shouldFilter(!(task.dueDate && task.dueDate !== null)); isFiltered = shouldFilter(!(task.dueDate && task.dueDate !== null));
} }
if (task.dueDate) { if (task.dueDate) {
const taskDueDate = moment(task.dueDate); const taskDueDate = dayjs(task.dueDate);
const today = moment(); const today = dayjs();
let start; let start;
let end; let end;
switch (filters.dueDate.type) { switch (filters.dueDate.type) {
@ -40,7 +40,7 @@ export default function shouldMetaFilter(task: Task, filters: TaskMetaFilters) {
taskDueDate.isBefore( taskDueDate.isBefore(
today today
.clone() .clone()
.add(1, 'days') .add(1, 'day')
.endOf('day'), .endOf('day'),
), ),
); );
@ -60,12 +60,12 @@ export default function shouldMetaFilter(task: Task, filters: TaskMetaFilters) {
start = today start = today
.clone() .clone()
.weekday(0) .weekday(0)
.add(7, 'days') .add(7, 'day')
.startOf('day'); .startOf('day');
end = today end = today
.clone() .clone()
.weekday(6) .weekday(6)
.add(7, 'days') .add(7, 'day')
.endOf('day'); .endOf('day');
isFiltered = shouldFilter(taskDueDate.isBetween(start, end)); isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
break; break;
@ -73,7 +73,7 @@ export default function shouldMetaFilter(task: Task, filters: TaskMetaFilters) {
start = today.clone().startOf('day'); start = today.clone().startOf('day');
end = today end = today
.clone() .clone()
.add(7, 'days') .add(7, 'day')
.endOf('day'); .endOf('day');
isFiltered = shouldFilter(taskDueDate.isBetween(start, end)); isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
break; break;
@ -81,7 +81,7 @@ export default function shouldMetaFilter(task: Task, filters: TaskMetaFilters) {
start = today.clone().startOf('day'); start = today.clone().startOf('day');
end = today end = today
.clone() .clone()
.add(14, 'days') .add(14, 'day')
.endOf('day'); .endOf('day');
isFiltered = shouldFilter(taskDueDate.isBetween(start, end)); isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
break; break;
@ -89,7 +89,7 @@ export default function shouldMetaFilter(task: Task, filters: TaskMetaFilters) {
start = today.clone().startOf('day'); start = today.clone().startOf('day');
end = today end = today
.clone() .clone()
.add(21, 'days') .add(21, 'day')
.endOf('day'); .endOf('day');
isFiltered = shouldFilter(taskDueDate.isBetween(start, end)); isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
break; break;

View File

@ -0,0 +1,24 @@
import React from 'react';
import NormalizeStyles from 'App/NormalizeStyles';
import BaseStyles from 'App/BaseStyles';
import LoadingSpinner from '.';
export default {
component: LoadingSpinner,
title: 'Login',
parameters: {
backgrounds: [
{ name: 'white', value: '#ffffff' },
{ name: 'gray', value: '#cdd3e1', default: true },
],
},
};
export const Default = () => {
return (
<>
<NormalizeStyles />
<BaseStyles />
<LoadingSpinner />
</>
);
};

View File

@ -0,0 +1,42 @@
import styled, { keyframes } from 'styled-components';
const LoadingSpinnerKeyframes = keyframes`
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
`;
export const LoadingSpinnerWrapper = styled.div<{ color: string; size: string; borderSize: string; thickness: string }>`
display: inline-block;
position: relative;
width: ${props => props.borderSize};
height: ${props => props.borderSize};
& > div {
box-sizing: border-box;
display: block;
position: absolute;
width: ${props => props.size};
height: ${props => props.size};
margin: ${props => props.thickness};
border: ${props => props.thickness} solid ${props => props.theme.colors[props.color]};
border-radius: 50%;
animation: 1.2s ${LoadingSpinnerKeyframes} cubic-bezier(0.5, 0, 0.5, 1) infinite;
border-color: ${props => props.theme.colors[props.color]} transparent transparent transparent;
}
& > div:nth-child(1) {
animation-delay: -0.45s;
}
& > div:nth-child(2) {
animation-delay: -0.3s;
}
& > div:nth-child(3) {
animation-delay: -0.15s;
}
`;
export default LoadingSpinnerWrapper;

View File

@ -0,0 +1,41 @@
import React from 'react';
import { LoadingSpinnerWrapper } from './Styles';
type LoadingSpinnerProps = {
color?: 'primary' | 'danger' | 'success' | 'warning' | 'dark';
size?: string;
borderSize?: string;
thickness?: string;
};
/**
* The default parameters may not be applicable to every scenario
*
* While borderSize and size should be a single prop,
* it is currently not as such because it would require math to be done to strings
* e.g "80px - 16"
*
*
* @param color
* @param size The size of the spinner. It is recommended to be at least 16 px less than the borderSize
* @param thickness
* @param borderSize Border size affects the size of the border which if is too small may break the spinner.
* @constructor
*/
const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({
color = 'primary',
size = '64px',
thickness = '8px',
borderSize = '80px',
}) => {
return (
<LoadingSpinnerWrapper color={color} size={size} thickness={thickness} borderSize={borderSize}>
<div />
<div />
<div />
</LoadingSpinnerWrapper>
);
};
export default LoadingSpinner;

View File

@ -1,5 +1,6 @@
import styled from 'styled-components'; import styled from 'styled-components';
import Button from 'shared/components/Button'; import Button from 'shared/components/Button';
import { mixin } from 'shared/utils/styles';
export const Wrapper = styled.div` export const Wrapper = styled.div`
background: #eff2f7; background: #eff2f7;
@ -68,7 +69,7 @@ export const FormIcon = styled.div`
export const FormError = styled.span` export const FormError = styled.span`
font-size: 0.875rem; font-size: 0.875rem;
color: rgb(234, 84, 85); color: ${props => props.theme.colors.danger};
`; `;
export const LoginButton = styled(Button)``; export const LoginButton = styled(Button)``;
@ -99,5 +100,5 @@ export const LogoWrapper = styled.div`
padding-bottom: 16px; padding-bottom: 16px;
margin-bottom: 24px; margin-bottom: 24px;
color: rgb(222, 235, 255); color: rgb(222, 235, 255);
border-bottom: 1px solid rgba(65, 69, 97, 0.65); border-bottom: 1px solid ${props => mixin.rgba(props.theme.colors.alternate, 0.65)};
`; `;

View File

@ -3,6 +3,7 @@ import AccessAccount from 'shared/undraw/AccessAccount';
import { User, Lock, Taskcafe } from 'shared/icons'; import { User, Lock, Taskcafe } from 'shared/icons';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import LoadingSpinner from 'shared/components/LoadingSpinner';
import { import {
Form, Form,
LogoWrapper, LogoWrapper,
@ -73,6 +74,7 @@ const Login = ({ onSubmit }: LoginProps) => {
<ActionButtons> <ActionButtons>
<RegisterButton variant="outline">Register</RegisterButton> <RegisterButton variant="outline">Register</RegisterButton>
{!isComplete && <LoadingSpinner size="32px" thickness="2px" borderSize="48px" />}
<LoginButton type="submit" disabled={!isComplete}> <LoginButton type="submit" disabled={!isComplete}>
Login Login
</LoginButton> </LoginButton>

View File

@ -20,14 +20,14 @@ export const MemberManagerSearch = styled(TextareaAutosize)`
font-size: 14px; font-size: 14px;
font-weight: 400; font-weight: 400;
background: #262c49; background: ${props => props.theme.colors.bgColor.secondary};
outline: none; outline: none;
color: #c2c6dc; color: ${props => props.theme.colors.text.primary};
border-color: #414561; border-color: ${props => props.theme.colors.border};
&:focus { &:focus {
box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px; box-shadow: ${props => props.theme.colors.primary} 0px 0px 0px 1px;
background: ${mixin.darken('#262c49', 0.15)}; background: ${props => mixin.darken(props.theme.colors.bgColor.secondary, 0.15)};
} }
`; `;
@ -66,8 +66,8 @@ export const BoardMemberListItemContent = styled(Member)`
color: #c2c6dc; color: #c2c6dc;
&:hover { &:hover {
background-color: rgba(${props => props.theme.colors.primary}); background-color: ${props => props.theme.colors.primary};
color: rgba(${props => props.theme.colors.text.secondary}); color: ${props => props.theme.colors.text.secondary};
} }
`; `;
@ -80,7 +80,7 @@ export const ProfileIcon = styled.div`
justify-content: center; justify-content: center;
color: #c2c6dc; color: #c2c6dc;
font-weight: 700; font-weight: 700;
background: rgb(115, 103, 240); background: ${props => props.theme.colors.primary};
cursor: pointer; cursor: pointer;
margin-right: 6px; margin-right: 6px;
`; `;

View File

@ -1,6 +1,7 @@
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
import Button from 'shared/components/Button'; import Button from 'shared/components/Button';
import { Checkmark } from 'shared/icons'; import { Checkmark } from 'shared/icons';
import { mixin } from 'shared/utils/styles';
export const RoleCheckmark = styled(Checkmark)` export const RoleCheckmark = styled(Checkmark)`
padding-left: 4px; padding-left: 4px;
@ -80,36 +81,36 @@ export const MiniProfileActionItem = styled.span<{ disabled?: boolean }>`
? css` ? css`
user-select: none; user-select: none;
pointer-events: none; pointer-events: none;
color: rgba(${props.theme.colors.text.primary}, 0.4); color: ${mixin.rgba(props.theme.colors.text.primary, 0.4)};
` `
: css` : css`
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background: rgb(115, 103, 240); background: ${props.theme.colors.primary};
} }
`} `}
`; `;
export const CurrentPermission = styled.span` export const CurrentPermission = styled.span`
margin-left: 4px; margin-left: 4px;
color: rgba(${props => 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`
height: 1px; height: 1px;
border-top: 1px solid #414561; border-top: 1px solid ${props => props.theme.colors.alternate};
margin: 0.25rem !important; margin: 0.25rem !important;
`; `;
export const WarningText = styled.span` export const WarningText = styled.span`
display: flex; display: flex;
color: rgba(${props => 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: rgba(${props => props.theme.colors.text.primary}); color: ${props => props.theme.colors.text.primary};
`; `;
export const RemoveMemberButton = styled(Button)` export const RemoveMemberButton = styled(Button)`

View File

@ -47,6 +47,7 @@ const permissions = [
type MiniProfileProps = { type MiniProfileProps = {
bio: string; bio: string;
user: TaskUser; user: TaskUser;
invited?: boolean;
onRemoveFromTask?: () => void; onRemoveFromTask?: () => void;
onChangeRole?: (roleCode: RoleCode) => void; onChangeRole?: (roleCode: RoleCode) => void;
onRemoveFromBoard?: () => void; onRemoveFromBoard?: () => void;
@ -56,6 +57,7 @@ type MiniProfileProps = {
const MiniProfile: React.FC<MiniProfileProps> = ({ const MiniProfile: React.FC<MiniProfileProps> = ({
user, user,
bio, bio,
invited,
canChangeRole, canChangeRole,
onRemoveFromTask, onRemoveFromTask,
onChangeRole, onChangeRole,
@ -74,7 +76,7 @@ const MiniProfile: React.FC<MiniProfileProps> = ({
)} )}
<ProfileInfo> <ProfileInfo>
<InfoTitle>{user.fullName}</InfoTitle> <InfoTitle>{user.fullName}</InfoTitle>
<InfoUsername>{`@${user.username}`}</InfoUsername> {invited ? <InfoUsername>Invited</InfoUsername> : <InfoUsername>{`@${user.username}`}</InfoUsername>}
<InfoBio>{bio}</InfoBio> <InfoBio>{bio}</InfoBio>
</ProfileInfo> </ProfileInfo>
</Profile> </Profile>

View File

@ -30,9 +30,9 @@ const CloseIcon = styled(Cross)`
top: 16px; top: 16px;
right: -32px; right: -32px;
cursor: pointer; cursor: pointer;
fill: rgba(${props => props.theme.colors.text.primary}); fill: ${props => props.theme.colors.text.primary};
&:hover { &:hover {
fill: rgba(${props => props.theme.colors.text.secondary}); fill: ${props => props.theme.colors.text.secondary};
} }
`; `;

View File

@ -1,4 +1,5 @@
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
import { mixin } from 'shared/utils/styles';
export const Logo = styled.div``; export const Logo = styled.div``;
@ -9,7 +10,7 @@ export const LogoTitle = styled.div`
font-size: 24px; font-size: 24px;
font-weight: 600; font-weight: 600;
transition: visibility, opacity, transform 0.25s ease; transition: visibility, opacity, transform 0.25s ease;
color: #7367f0; color: #22ff00;
`; `;
export const ActionContainer = styled.div` export const ActionContainer = styled.div`
position: relative; position: relative;
@ -46,8 +47,8 @@ export const ActionButtonWrapper = styled.div<{ active?: boolean }>`
${props => ${props =>
props.active && props.active &&
css` css`
background: rgb(115, 103, 240); background: ${props.theme.colors.primary};
box-shadow: 0 0 10px 1px rgba(115, 103, 240, 0.7); box-shadow: 0 0 10px 1px ${mixin.rgba(props.theme.colors.primary, 0.7)};
`} `}
border-radius: 6px; border-radius: 6px;
cursor: pointer; cursor: pointer;
@ -73,7 +74,7 @@ export const LogoWrapper = styled.div`
color: rgb(222, 235, 255); color: rgb(222, 235, 255);
cursor: pointer; cursor: pointer;
transition: color 0.1s ease 0s, border 0.1s ease 0s; transition: color 0.1s ease 0s, border 0.1s ease 0s;
border-bottom: 1px solid rgba(65, 69, 97, 0.65); border-bottom: 1px solid ${props => mixin.rgba(props.theme.colors.alternate, 0.65)};
`; `;
export const Container = styled.aside` export const Container = styled.aside`
@ -87,12 +88,12 @@ export const Container = styled.aside`
transform: translateZ(0px); transform: translateZ(0px);
background: #10163a; background: #10163a;
transition: all 0.1s ease 0s; transition: all 0.1s ease 0s;
border-right: 1px solid rgba(65, 69, 97, 0.65); border-right: 1px solid ${props => mixin.rgba(props.theme.colors.alternate, 0.65)};
&:hover { &:hover {
width: 260px; width: 260px;
box-shadow: rgba(0, 0, 0, 0.6) 0px 0px 50px 0px; box-shadow: rgba(0, 0, 0, 0.6) 0px 0px 50px 0px;
border-right: 1px solid rgba(65, 69, 97, 0); border-right: 1px solid ${props => mixin.rgba(props.theme.colors.alternate, 0)};
} }
&:hover ${LogoTitle} { &:hover ${LogoTitle} {
bottom: -12px; bottom: -12px;
@ -106,6 +107,6 @@ export const Container = styled.aside`
} }
&:hover ${LogoWrapper} { &:hover ${LogoWrapper} {
border-bottom: 1px solid rgba(65, 69, 97, 0); border-bottom: 1px solid ${props => mixin.rgba(props.theme.colors.alternate, 0)};
} }
`; `;

View File

@ -3,16 +3,17 @@ import styled from 'styled-components';
import { mixin } from 'shared/utils/styles'; import { mixin } from 'shared/utils/styles';
import Select from 'react-select'; import Select from 'react-select';
import { ArrowLeft, Cross } from 'shared/icons'; import { ArrowLeft, Cross } from 'shared/icons';
import theme from '../../../App/ThemeStyles';
function getBackgroundColor(isDisabled: boolean, isSelected: boolean, isFocused: boolean) { function getBackgroundColor(isDisabled: boolean, isSelected: boolean, isFocused: boolean) {
if (isDisabled) { if (isDisabled) {
return null; return null;
} }
if (isSelected) { if (isSelected) {
return mixin.darken('#262c49', 0.25); return mixin.darken(theme.colors.bg.secondary, 0.25);
} }
if (isFocused) { if (isFocused) {
return mixin.darken('#262c49', 0.15); return mixin.darken(theme.colors.bg.secondary, 0.15);
} }
return null; return null;
} }
@ -97,8 +98,8 @@ const ProjectName = styled.input`
font-weight: 400; font-weight: 400;
&:focus { &:focus {
background: ${mixin.darken('#262c49', 0.15)}; background: ${props => mixin.darken(props.theme.colors.bg.secondary, 0.15)};
box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px; box-shadow: ${props => props.theme.colors.primary} 0px 0px 0px 1px;
} }
`; `;
const ProjectNameLabel = styled.label` const ProjectNameLabel = styled.label`
@ -126,35 +127,35 @@ const colourStyles = {
control: (styles: any, data: any) => { control: (styles: any, data: any) => {
return { return {
...styles, ...styles,
backgroundColor: data.isMenuOpen ? mixin.darken('#262c49', 0.15) : '#262c49', backgroundColor: data.isMenuOpen ? mixin.darken(theme.colors.bg.secondary, 0.15) : theme.colors.bg.secondary,
boxShadow: data.menuIsOpen ? 'rgb(115, 103, 240) 0px 0px 0px 1px' : 'none', boxShadow: data.menuIsOpen ? `${theme.colors.primary} 0px 0px 0px 1px` : 'none',
borderRadius: '3px', borderRadius: '3px',
borderWidth: '1px', borderWidth: '1px',
borderStyle: 'solid', borderStyle: 'solid',
borderImage: 'initial', borderImage: 'initial',
borderColor: '#414561', borderColor: theme.colors.alternate,
':hover': { ':hover': {
boxShadow: 'rgb(115, 103, 240) 0px 0px 0px 1px', boxShadow: `${theme.colors.primary} 0px 0px 0px 1px`,
borderRadius: '3px', borderRadius: '3px',
borderWidth: '1px', borderWidth: '1px',
borderStyle: 'solid', borderStyle: 'solid',
borderImage: 'initial', borderImage: 'initial',
borderColor: '#414561', borderColor: theme.colors.alternate,
}, },
':active': { ':active': {
boxShadow: 'rgb(115, 103, 240) 0px 0px 0px 1px', boxShadow: `${theme.colors.primary} 0px 0px 0px 1px`,
borderRadius: '3px', borderRadius: '3px',
borderWidth: '1px', borderWidth: '1px',
borderStyle: 'solid', borderStyle: 'solid',
borderImage: 'initial', borderImage: 'initial',
borderColor: 'rgb(115, 103, 240)', borderColor: `${theme.colors.primary}`,
}, },
}; };
}, },
menu: (styles: any) => { menu: (styles: any) => {
return { return {
...styles, ...styles,
backgroundColor: mixin.darken('#262c49', 0.15), backgroundColor: mixin.darken(theme.colors.bg.secondary, 0.15),
}; };
}, },
dropdownIndicator: (styles: any) => ({ ...styles, color: '#c2c6dc', ':hover': { color: '#c2c6dc' } }), dropdownIndicator: (styles: any) => ({ ...styles, color: '#c2c6dc', ':hover': { color: '#c2c6dc' } }),
@ -167,11 +168,11 @@ const colourStyles = {
cursor: isDisabled ? 'not-allowed' : 'default', cursor: isDisabled ? 'not-allowed' : 'default',
':active': { ':active': {
...styles[':active'], ...styles[':active'],
backgroundColor: !isDisabled && (isSelected ? mixin.darken('#262c49', 0.25) : '#fff'), backgroundColor: !isDisabled && (isSelected ? mixin.darken(theme.colors.bg.secondary, 0.25) : '#fff'),
}, },
':hover': { ':hover': {
...styles[':hover'], ...styles[':hover'],
backgroundColor: !isDisabled && (isSelected ? 'rgb(115, 103, 240)' : 'rgb(115, 103, 240)'), backgroundColor: !isDisabled && (isSelected ? theme.colors.primary : theme.colors.primary),
}, },
}; };
}, },
@ -209,21 +210,21 @@ const CreateButton = styled.button`
&:hover { &:hover {
color: #fff; color: #fff;
background: rgb(115, 103, 240); background: ${props => props.theme.colors.primary};
border-color: rgb(115, 103, 240); border-color: ${props => props.theme.colors.primary};
} }
`; `;
type NewProjectProps = { type NewProjectProps = {
initialTeamID: string | null; initialTeamID: string | null;
teams: Array<Team>; teams: Array<Team>;
onClose: () => void; onClose: () => void;
onCreateProject: (projectName: string, teamID: string) => void; onCreateProject: (projectName: string, teamID: string | null) => void;
}; };
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 = 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>
@ -271,8 +272,8 @@ const NewProject: React.FC<NewProjectProps> = ({ initialTeamID, teams, onClose,
</ProjectInfo> </ProjectInfo>
<CreateButton <CreateButton
onClick={() => { onClick={() => {
if (team && projectName !== '') { if (projectName !== '') {
onCreateProject(projectName, team); onCreateProject(projectName, team === 'no-team' ? null : team);
} }
}} }}
> >

View File

@ -37,7 +37,7 @@ const ItemTextContainer = styled.div`
const ItemTextTitle = styled.span` const ItemTextTitle = styled.span`
font-weight: 500; font-weight: 500;
display: block; display: block;
color: rgba(${props => props.theme.colors.primary}); color: ${props => props.theme.colors.primary};
font-size: 14px; font-size: 14px;
`; `;
const ItemTextDesc = styled.span` const ItemTextDesc = styled.span`
@ -76,21 +76,21 @@ const NotificationHeader = styled.div`
text-align: center; text-align: center;
border-top-left-radius: 6px; border-top-left-radius: 6px;
border-top-right-radius: 6px; border-top-right-radius: 6px;
background: rgba(${props => props.theme.colors.primary}); background: ${props => props.theme.colors.primary};
`; `;
const NotificationHeaderTitle = styled.span` const NotificationHeaderTitle = styled.span`
font-size: 14px; font-size: 14px;
color: rgba(${props => props.theme.colors.text.secondary}); color: ${props => props.theme.colors.text.secondary};
`; `;
const NotificationFooter = styled.div` const NotificationFooter = styled.div`
cursor: pointer; cursor: pointer;
padding: 0.5rem; padding: 0.5rem;
text-align: center; text-align: center;
color: rgba(${props => props.theme.colors.primary}); color: ${props => props.theme.colors.primary};
&:hover { &:hover {
background: #10163a; background: ${props => props.theme.colors.bg.primary};
} }
border-bottom-left-radius: 6px; border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px; border-bottom-right-radius: 6px;

View File

@ -4,7 +4,7 @@ import styled from 'styled-components';
import { SaveButton, DeleteButton, LabelBox, EditLabelForm, FieldLabel, FieldName } from './Styles'; import { SaveButton, DeleteButton, LabelBox, EditLabelForm, FieldLabel, FieldName } from './Styles';
const WhiteCheckmark = styled(Checkmark)` const WhiteCheckmark = styled(Checkmark)`
fill: rgba(${props => props.theme.colors.text.secondary}); fill: ${props => props.theme.colors.text.secondary};
`; `;
type Props = { type Props = {
labelColors: Array<LabelColor>; labelColors: Array<LabelColor>;

View File

@ -1,6 +1,7 @@
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
import { mixin } from 'shared/utils/styles'; import { mixin } from 'shared/utils/styles';
import ControlledInput from 'shared/components/ControlledInput'; import ControlledInput from 'shared/components/ControlledInput';
import theme from 'App/ThemeStyles';
export const Container = styled.div<{ export const Container = styled.div<{
invertY: boolean; invertY: boolean;
@ -176,7 +177,7 @@ export const LabelIcon = styled.div`
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background: rgb(115, 103, 240); background: ${props => props.theme.colors.primary};
} }
`; `;
@ -233,8 +234,8 @@ export const FieldName = styled.input`
font-weight: 400; font-weight: 400;
&:focus { &:focus {
box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px; box-shadow: ${props => props.theme.colors.primary} 0px 0px 0px 1px;
background: ${mixin.darken('#262c49', 0.15)}; background: ${mixin.darken(theme.colors.bg.secondary, 0.15)};
} }
`; `;
@ -258,7 +259,7 @@ export const LabelBox = styled.span<{ color: string }>`
`; `;
export const SaveButton = styled.input` export const SaveButton = styled.input`
background: rgb(115, 103, 240); background: ${props => props.theme.colors.primary};
box-shadow: none; box-shadow: none;
border: none; border: none;
color: #fff; color: #fff;
@ -296,7 +297,7 @@ export const DeleteButton = styled.input`
&:hover { &:hover {
color: #fff; color: #fff;
background: rgb(115, 103, 240); background: ${props => props.theme.colors.primary};
border-color: transparent; border-color: transparent;
} }
`; `;
@ -317,7 +318,7 @@ export const CreateLabelButton = styled.button`
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background: rgb(115, 103, 240); background: ${props => props.theme.colors.primary};
} }
`; `;

View File

@ -4,6 +4,7 @@ import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import NOOP from 'shared/utils/noop'; import NOOP from 'shared/utils/noop';
import produce from 'immer'; import produce from 'immer';
import theme from 'App/ThemeStyles';
import { import {
Container, Container,
ContainerDiamond, ContainerDiamond,
@ -18,7 +19,7 @@ import {
function getPopupOptions(options?: PopupOptions) { function getPopupOptions(options?: PopupOptions) {
const popupOptions = { const popupOptions = {
borders: true, borders: true,
diamondColor: '#262c49', diamondColor: theme.colors.bg.secondary,
targetPadding: '10px', targetPadding: '10px',
showDiamond: true, showDiamond: true,
width: 316, width: 316,

View File

@ -24,7 +24,7 @@ export const ListActionItem = styled.span`
margin: 0 -12px; margin: 0 -12px;
text-decoration: none; text-decoration: none;
&:hover { &:hover {
background: rgb(115, 103, 240); background: ${props => props.theme.colors.primary};
} }
`; `;

View File

@ -1,6 +1,4 @@
import styled, { keyframes, css } from 'styled-components'; import styled, { keyframes, css } from 'styled-components';
import TextareaAutosize from 'react-autosize-textarea';
import { mixin } from 'shared/utils/styles';
export const Wrapper = styled.div<{ open: boolean }>` export const Wrapper = styled.div<{ open: boolean }>`
background: rgba(0, 0, 0, 0.55); background: rgba(0, 0, 0, 0.55);
@ -30,7 +28,7 @@ export const Container = styled.div<{ fixed: boolean; width: number; top: number
export const SaveButton = styled.button` export const SaveButton = styled.button`
cursor: pointer; cursor: pointer;
background: rgb(115, 103, 240); background: ${props => props.theme.colors.primary};
box-shadow: none; box-shadow: none;
border: none; border: none;
color: #fff; color: #fff;

View File

@ -1,5 +1,6 @@
import styled from 'styled-components'; import styled from 'styled-components';
import Button from 'shared/components/Button'; import Button from 'shared/components/Button';
import { mixin } from 'shared/utils/styles';
export const Wrapper = styled.div` export const Wrapper = styled.div`
background: #eff2f7; background: #eff2f7;
@ -68,7 +69,7 @@ export const FormIcon = styled.div`
export const FormError = styled.span` export const FormError = styled.span`
font-size: 0.875rem; font-size: 0.875rem;
color: rgb(234, 84, 85); color: ${props => props.theme.colors.danger};
`; `;
export const LoginButton = styled(Button)``; export const LoginButton = styled(Button)``;
@ -99,5 +100,5 @@ export const LogoWrapper = styled.div`
padding-bottom: 16px; padding-bottom: 16px;
margin-bottom: 24px; margin-bottom: 24px;
color: rgb(222, 235, 255); color: rgb(222, 235, 255);
border-bottom: 1px solid rgba(65, 69, 97, 0.65); border-bottom: 1px solid ${props => mixin.rgba(props.theme.colors.alternate, 0.65)};
`; `;

View File

@ -24,7 +24,7 @@ import {
const EMAIL_PATTERN = /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/i; const EMAIL_PATTERN = /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/i;
const INITIALS_PATTERN = /[a-zA-Z]{2,3}/i; const INITIALS_PATTERN = /[a-zA-Z]{2,3}/i;
const Register = ({ onSubmit }: 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, errors, setError } = useForm<RegisterFormData>();
const loginSubmit = (data: RegisterFormData) => { const loginSubmit = (data: RegisterFormData) => {
@ -43,103 +43,112 @@ const Register = ({ onSubmit }: RegisterProps) => {
<Taskcafe width={42} height={42} /> <Taskcafe width={42} height={42} />
<LogoTitle>Taskcafé</LogoTitle> <LogoTitle>Taskcafé</LogoTitle>
</LogoWrapper> </LogoWrapper>
<Title>Register</Title> {registered ? (
<SubTitle>Please create the system admin user</SubTitle> <>
<Form onSubmit={handleSubmit(loginSubmit)}> <Title>Thanks for registering</Title>
<FormLabel htmlFor="fullname"> <SubTitle>Please check your inbox for a confirmation email.</SubTitle>
Full name </>
<FormTextInput ) : (
type="text" <>
id="fullname" <Title>Register</Title>
name="fullname" <SubTitle>Please create your user</SubTitle>
ref={register({ required: 'Full name is required' })} <Form onSubmit={handleSubmit(loginSubmit)}>
/> <FormLabel htmlFor="fullname">
<FormIcon> Full name
<User width={20} height={20} /> <FormTextInput
</FormIcon> type="text"
</FormLabel> id="fullname"
{errors.username && <FormError>{errors.username.message}</FormError>} name="fullname"
<FormLabel htmlFor="username"> ref={register({ required: 'Full name is required' })}
Username />
<FormTextInput <FormIcon>
type="text" <User width={20} height={20} />
id="username" </FormIcon>
name="username" </FormLabel>
ref={register({ required: 'Username is required' })} {errors.username && <FormError>{errors.username.message}</FormError>}
/> <FormLabel htmlFor="username">
<FormIcon> Username
<User width={20} height={20} /> <FormTextInput
</FormIcon> type="text"
</FormLabel> id="username"
{errors.username && <FormError>{errors.username.message}</FormError>} name="username"
<FormLabel htmlFor="email"> ref={register({ required: 'Username is required' })}
Email />
<FormTextInput <FormIcon>
type="text" <User width={20} height={20} />
id="email" </FormIcon>
name="email" </FormLabel>
ref={register({ {errors.username && <FormError>{errors.username.message}</FormError>}
required: 'Email is required', <FormLabel htmlFor="email">
pattern: { value: EMAIL_PATTERN, message: 'Must be a valid email' }, Email
})} <FormTextInput
/> type="text"
<FormIcon> id="email"
<User width={20} height={20} /> name="email"
</FormIcon> ref={register({
</FormLabel> required: 'Email is required',
{errors.email && <FormError>{errors.email.message}</FormError>} pattern: { value: EMAIL_PATTERN, message: 'Must be a valid email' },
<FormLabel htmlFor="initials"> })}
Initials />
<FormTextInput <FormIcon>
type="text" <User width={20} height={20} />
id="initials" </FormIcon>
name="initials" </FormLabel>
ref={register({ {errors.email && <FormError>{errors.email.message}</FormError>}
required: 'Initials is required', <FormLabel htmlFor="initials">
pattern: { Initials
value: INITIALS_PATTERN, <FormTextInput
message: 'Initials must be between 2 to 3 characters.', type="text"
}, id="initials"
})} name="initials"
/> ref={register({
<FormIcon> required: 'Initials is required',
<User width={20} height={20} /> pattern: {
</FormIcon> value: INITIALS_PATTERN,
</FormLabel> message: 'Initials must be between 2 to 3 characters.',
{errors.initials && <FormError>{errors.initials.message}</FormError>} },
<FormLabel htmlFor="password"> })}
Password />
<FormTextInput <FormIcon>
type="password" <User width={20} height={20} />
id="password" </FormIcon>
name="password" </FormLabel>
ref={register({ required: 'Password is required' })} {errors.initials && <FormError>{errors.initials.message}</FormError>}
/> <FormLabel htmlFor="password">
<FormIcon> Password
<Lock width={20} height={20} /> <FormTextInput
</FormIcon> type="password"
</FormLabel> id="password"
{errors.password && <FormError>{errors.password.message}</FormError>} name="password"
<FormLabel htmlFor="password_confirm"> ref={register({ required: 'Password is required' })}
Password (Confirm) />
<FormTextInput <FormIcon>
type="password" <Lock width={20} height={20} />
id="password_confirm" </FormIcon>
name="password_confirm" </FormLabel>
ref={register({ required: 'Password (confirm) is required' })} {errors.password && <FormError>{errors.password.message}</FormError>}
/> <FormLabel htmlFor="password_confirm">
<FormIcon> Password (Confirm)
<Lock width={20} height={20} /> <FormTextInput
</FormIcon> type="password"
</FormLabel> id="password_confirm"
{errors.password_confirm && <FormError>{errors.password_confirm.message}</FormError>} name="password_confirm"
ref={register({ required: 'Password (confirm) is required' })}
/>
<FormIcon>
<Lock width={20} height={20} />
</FormIcon>
</FormLabel>
{errors.password_confirm && <FormError>{errors.password_confirm.message}</FormError>}
<ActionButtons> <ActionButtons>
<RegisterButton type="submit" disabled={!isComplete}> <RegisterButton type="submit" disabled={!isComplete}>
Register Register
</RegisterButton> </RegisterButton>
</ActionButtons> </ActionButtons>
</Form> </Form>
</>
)}
</LoginFormContainer> </LoginFormContainer>
</LoginFormWrapper> </LoginFormWrapper>
</Column> </Column>

View File

@ -2,53 +2,54 @@ import React from 'react';
import Select from 'react-select'; import Select from 'react-select';
import styled from 'styled-components'; import styled from 'styled-components';
import { mixin } from 'shared/utils/styles'; import { mixin } from 'shared/utils/styles';
import theme from 'App/ThemeStyles';
function getBackgroundColor(isDisabled: boolean, isSelected: boolean, isFocused: boolean) { function getBackgroundColor(isDisabled: boolean, isSelected: boolean, isFocused: boolean) {
if (isDisabled) { if (isDisabled) {
return null; return null;
} }
if (isSelected) { if (isSelected) {
return mixin.darken('#262c49', 0.25); return mixin.darken(theme.colors.bg.secondary, 0.25);
} }
if (isFocused) { if (isFocused) {
return mixin.darken('#262c49', 0.15); return mixin.darken(theme.colors.bg.secondary, 0.15);
} }
return null; return null;
} }
const colourStyles = { export const colourStyles = {
control: (styles: any, data: any) => { control: (styles: any, data: any) => {
return { return {
...styles, ...styles,
backgroundColor: data.isMenuOpen ? mixin.darken('#262c49', 0.15) : '#262c49', backgroundColor: data.isMenuOpen ? mixin.darken(theme.colors.bg.secondary, 0.15) : theme.colors.bg.secondary,
boxShadow: data.menuIsOpen ? 'rgb(115, 103, 240) 0px 0px 0px 1px' : 'none', boxShadow: data.menuIsOpen ? `${theme.colors.primary} 0px 0px 0px 1px` : 'none',
borderRadius: '3px', borderRadius: '3px',
borderWidth: '1px', borderWidth: '1px',
borderStyle: 'solid', borderStyle: 'solid',
borderImage: 'initial', borderImage: 'initial',
borderColor: '#414561', borderColor: theme.colors.alternate,
':hover': { ':hover': {
boxShadow: 'rgb(115, 103, 240) 0px 0px 0px 1px', boxShadow: `${theme.colors.primary} 0px 0px 0px 1px`,
borderRadius: '3px', borderRadius: '3px',
borderWidth: '1px', borderWidth: '1px',
borderStyle: 'solid', borderStyle: 'solid',
borderImage: 'initial', borderImage: 'initial',
borderColor: '#414561', borderColor: theme.colors.alternate,
}, },
':active': { ':active': {
boxShadow: 'rgb(115, 103, 240) 0px 0px 0px 1px', boxShadow: `${theme.colors.primary} 0px 0px 0px 1px`,
borderRadius: '3px', borderRadius: '3px',
borderWidth: '1px', borderWidth: '1px',
borderStyle: 'solid', borderStyle: 'solid',
borderImage: 'initial', borderImage: 'initial',
borderColor: 'rgb(115, 103, 240)', borderColor: `${theme.colors.primary}`,
}, },
}; };
}, },
menu: (styles: any) => { menu: (styles: any) => {
return { return {
...styles, ...styles,
backgroundColor: mixin.darken('#262c49', 0.15), backgroundColor: mixin.darken(theme.colors.bg.secondary, 0.15),
}; };
}, },
dropdownIndicator: (styles: any) => ({ ...styles, color: '#c2c6dc', ':hover': { color: '#c2c6dc' } }), dropdownIndicator: (styles: any) => ({ ...styles, color: '#c2c6dc', ':hover': { color: '#c2c6dc' } }),
@ -61,11 +62,11 @@ const colourStyles = {
cursor: isDisabled ? 'not-allowed' : 'default', cursor: isDisabled ? 'not-allowed' : 'default',
':active': { ':active': {
...styles[':active'], ...styles[':active'],
backgroundColor: !isDisabled && (isSelected ? mixin.darken('#262c49', 0.25) : '#fff'), backgroundColor: !isDisabled && (isSelected ? mixin.darken(theme.colors.bg.secondary, 0.25) : '#fff'),
}, },
':hover': { ':hover': {
...styles[':hover'], ...styles[':hover'],
backgroundColor: !isDisabled && (isSelected ? 'rgb(115, 103, 240)' : 'rgb(115, 103, 240)'), backgroundColor: !isDisabled && (isSelected ? theme.colors.primary : theme.colors.primary),
}, },
}; };
}, },
@ -86,7 +87,7 @@ const colourStyles = {
const InputLabel = styled.span<{ width: string }>` const InputLabel = styled.span<{ width: string }>`
width: ${props => props.width}; width: ${props => props.width};
padding-left: 0.7rem; padding-left: 0.7rem;
color: rgba(115, 103, 240); color: ${props => props.theme.colors.primary};
left: 0; left: 0;
top: 0; top: 0;
transition: all 0.2s ease; transition: all 0.2s ease;

View File

@ -17,7 +17,7 @@ const UserInfoInput = styled(Input)`
const FormError = styled.span` const FormError = styled.span`
font-size: 12px; font-size: 12px;
color: rgba(${props => props.theme.colors.warning}); color: ${props => props.theme.colors.warning};
`; `;
const ProfileContainer = styled.div` const ProfileContainer = styled.div`
@ -152,12 +152,12 @@ const TabNavItemButton = styled.button<{ active: boolean }>`
width: 100%; width: 100%;
position: relative; position: relative;
color: ${props => (props.active ? 'rgba(115, 103, 240)' : '#c2c6dc')}; color: ${props => (props.active ? `${props.theme.colors.primary}` : '#c2c6dc')};
&:hover { &:hover {
color: rgba(115, 103, 240); color: ${props => props.theme.colors.primary};
} }
&:hover svg { &:hover svg {
fill: rgba(115, 103, 240); fill: ${props => props.theme.colors.primary};
} }
`; `;
@ -175,8 +175,8 @@ const TabNavLine = styled.span<{ top: number }>`
transform: scaleX(1); transform: scaleX(1);
top: ${props => props.top}px; top: ${props => props.top}px;
background: linear-gradient(30deg, rgba(115, 103, 240), rgba(115, 103, 240)); background: linear-gradient(30deg, ${props => props.theme.colors.primary}, ${props => props.theme.colors.primary});
box-shadow: 0 0 8px 0 rgba(115, 103, 240); 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;

View File

@ -1,5 +1,5 @@
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import styled from 'styled-components'; import styled, { css } from 'styled-components';
import { DoubleChevronUp, Crown } from 'shared/icons'; import { DoubleChevronUp, Crown } from 'shared/icons';
export const AdminIcon = styled(DoubleChevronUp)` export const AdminIcon = styled(DoubleChevronUp)`
@ -24,46 +24,78 @@ const TaskDetailAssignee = styled.div`
position: relative; position: relative;
`; `;
export const Wrapper = styled.div<{ size: number | string; bgColor: string | null; backgroundURL: string | null }>` export const Wrapper = styled.div<{
size: number | string;
bgColor: string | null;
backgroundURL: string | null;
hasClick: boolean;
}>`
width: ${props => props.size}px; 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: rgba(${props => (props.backgroundURL ? props.theme.colors.text.primary : '0,0,0')}); color: ${props => (props.backgroundURL ? props.theme.colors.text.primary : 'rgb(0,0,0)')};
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;
font-size: 14px; font-size: 14px;
font-weight: 400; font-weight: 400;
&:hover { ${props =>
opacity: 0.8; props.hasClick &&
} css`
&:hover {
opacity: 0.8;
}
`}
`; `;
type TaskAssigneeProps = { type TaskAssigneeProps = {
size: number | string; size: number | string;
showRoleIcons?: boolean; showRoleIcons?: boolean;
member: TaskUser; member: TaskUser;
onMemberProfile: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void; invited?: boolean;
onMemberProfile?: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
className?: string; className?: string;
}; };
const TaskAssignee: React.FC<TaskAssigneeProps> = ({ showRoleIcons, member, onMemberProfile, size, className }) => { const TaskAssignee: React.FC<TaskAssigneeProps> = ({
showRoleIcons,
member,
invited,
onMemberProfile,
size,
className,
}) => {
const $memberRef = useRef<HTMLDivElement>(null); const $memberRef = useRef<HTMLDivElement>(null);
let profileIcon: ProfileIcon = {
url: null,
bgColor: null,
initials: null,
};
if (member.profileIcon) {
profileIcon = member.profileIcon;
}
return ( return (
<TaskDetailAssignee <TaskDetailAssignee
className={className} className={className}
ref={$memberRef} ref={$memberRef}
onClick={e => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
onMemberProfile($memberRef, member.id); if (onMemberProfile) {
onMemberProfile($memberRef, member.id);
}
}} }}
key={member.id} key={member.id}
> >
<Wrapper backgroundURL={member.profileIcon.url ?? null} bgColor={member.profileIcon.bgColor ?? null} size={size}> <Wrapper
{(!member.profileIcon.url && member.profileIcon.initials) ?? ''} hasClick={typeof onMemberProfile !== undefined}
backgroundURL={profileIcon.url ?? null}
bgColor={profileIcon.bgColor ?? null}
size={size}
>
{(!profileIcon.url && profileIcon.initials) ?? ''}
</Wrapper> </Wrapper>
{showRoleIcons && member.role && member.role.code === 'admin' && <AdminIcon width={10} height={10} />} {showRoleIcons && member.role && member.role.code === 'admin' && <AdminIcon width={10} height={10} />}
{showRoleIcons && member.role && member.role.code === 'owner' && <OwnerIcon width={10} height={10} />} {showRoleIcons && member.role && member.role.code === 'owner' && <OwnerIcon width={10} height={10} />}

View File

@ -3,8 +3,6 @@ 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';
import TaskAssignee from 'shared/components/TaskAssignee'; import TaskAssignee from 'shared/components/TaskAssignee';
import { User, Trash, Paperclip } from 'shared/icons';
import Member from 'shared/components/Member';
export const Container = styled.div` export const Container = styled.div`
display: flex; display: flex;
@ -33,35 +31,35 @@ export const MarkCompleteButton = styled.button<{ invert: boolean }>`
${props => ${props =>
props.invert props.invert
? css` ? css`
background: rgba(${props.theme.colors.success}); background: ${props.theme.colors.success};
& svg { & svg {
fill: rgba(${props.theme.colors.text.secondary}); fill: ${props.theme.colors.text.secondary};
} }
& span { & span {
color: rgba(${props.theme.colors.text.secondary}); color: ${props.theme.colors.text.secondary};
} }
&:hover { &:hover {
background: rgba(${props.theme.colors.success}, 0.8); background: ${mixin.rgba(props.theme.colors.success, 0.8)};
} }
` `
: css` : css`
background: none; background: none;
border: 1px solid rgba(${props.theme.colors.text.secondary}); border: 1px solid ${props.theme.colors.text.secondary};
& svg { & svg {
fill: rgba(${props.theme.colors.text.secondary}); fill: ${props.theme.colors.text.secondary};
} }
& span { & span {
color: rgba(${props.theme.colors.text.secondary}); color: ${props.theme.colors.text.secondary};
} }
&:hover { &:hover {
background: rgba(${props.theme.colors.success}, 0.08); background: ${mixin.rgba(props.theme.colors.success, 0.08)};
border: 1px solid rgba(${props.theme.colors.success}); border: 1px solid ${props.theme.colors.success};
} }
&:hover svg { &:hover svg {
fill: rgba(${props.theme.colors.success}); fill: ${props.theme.colors.success};
} }
&:hover span { &:hover span {
color: rgba(${props.theme.colors.success}); color: ${props.theme.colors.success};
} }
`} `}
`; `;
@ -85,7 +83,7 @@ export const SidebarTitle = styled.div`
font-size: 12px; font-size: 12px;
min-height: 24px; min-height: 24px;
margin-left: 8px; margin-left: 8px;
color: rgba(${props => props.theme.colors.text.primary}, 0.75); color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.75)};
padding-top: 4px; padding-top: 4px;
letter-spacing: 0.5px; letter-spacing: 0.5px;
text-transform: uppercase; text-transform: uppercase;
@ -93,7 +91,7 @@ export const SidebarTitle = styled.div`
export const SidebarButton = styled.div` export const SidebarButton = styled.div`
font-size: 14px; font-size: 14px;
color: rgba(${props => props.theme.colors.text.primary}); color: ${props => props.theme.colors.text.primary};
min-height: 32px; min-height: 32px;
width: 100%; width: 100%;
@ -168,7 +166,7 @@ export const TaskDetailsTitle = styled(TextareaAutosize)`
} }
&:focus { &:focus {
border-color: rgba(${props => props.theme.colors.primary}); border-color: ${props => props.theme.colors.primary};
} }
`; `;
@ -176,7 +174,7 @@ export const DueDateTitle = styled.div`
font-size: 12px; font-size: 12px;
min-height: 24px; min-height: 24px;
margin-left: 8px; margin-left: 8px;
color: rgba(${props => props.theme.colors.text.primary}, 0.75); color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.75)};
padding-top: 8px; padding-top: 8px;
letter-spacing: 0.5px; letter-spacing: 0.5px;
text-transform: uppercase; text-transform: uppercase;
@ -187,7 +185,7 @@ export const AssignedUsersSection = styled.div`
padding-right: 32px; padding-right: 32px;
padding-top: 24px; padding-top: 24px;
padding-bottom: 24px; padding-bottom: 24px;
border-bottom: 1px solid #414561; border-bottom: 1px solid ${props => props.theme.colors.alternate};
display: flex; display: flex;
flex-direction: column; flex-direction: column;
`; `;
@ -205,10 +203,10 @@ export const AssignUserIcon = styled.div`
justify-content: center; justify-content: center;
align-items: center; align-items: center;
&:hover { &:hover {
border: 1px solid rgba(${props => props.theme.colors.text.secondary}, 0.75); border: 1px solid ${props => mixin.rgba(props.theme.colors.text.secondary, 0.75)};
} }
&:hover svg { &:hover svg {
fill: rgba(${props => props.theme.colors.text.secondary}, 0.75); fill: ${props => mixin.rgba(props.theme.colors.text.secondary, 0.75)};
} }
`; `;
@ -223,17 +221,17 @@ export const AssignUsersButton = styled.div`
align-items: center; align-items: center;
border: 1px solid transparent; border: 1px solid transparent;
&:hover { &:hover {
border: 1px solid ${mixin.darken('#414561', 0.15)}; border: 1px solid ${props => mixin.darken(props.theme.colors.alternate, 0.15)};
} }
&:hover ${AssignUserIcon} { &:hover ${AssignUserIcon} {
border: 1px solid #414561; border: 1px solid ${props => props.theme.colors.alternate};
} }
`; `;
export const AssignUserLabel = styled.span` export const AssignUserLabel = styled.span`
flex: 1 1 auto; flex: 1 1 auto;
line-height: 15px; line-height: 15px;
color: rgba(${props => props.theme.colors.text.primary}, 0.75); color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.75)};
`; `;
export const ExtraActionsSection = styled.div` export const ExtraActionsSection = styled.div`
@ -245,7 +243,7 @@ export const ExtraActionsSection = styled.div`
`; `;
export const ActionButtonsTitle = styled.h3` export const ActionButtonsTitle = styled.h3`
color: rgba(${props => props.theme.colors.text.primary}); color: ${props => props.theme.colors.text.primary};
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
letter-spacing: 0.04em; letter-spacing: 0.04em;
@ -255,7 +253,7 @@ export const ActionButton = styled(Button)`
margin-top: 8px; margin-top: 8px;
margin-left: -10px; margin-left: -10px;
padding: 8px 16px; padding: 8px 16px;
background: rgba(${props => props.theme.colors.bg.primary}, 0.5); background: ${props => mixin.rgba(props.theme.colors.bg.primary, 0.5)};
text-align: left; text-align: left;
transition: transform 0.2s ease; transition: transform 0.2s ease;
& span { & span {
@ -264,7 +262,7 @@ export const ActionButton = styled(Button)`
&:hover { &:hover {
box-shadow: none; box-shadow: none;
transform: translateX(4px); transform: translateX(4px);
background: rgba(${props => props.theme.colors.bg.primary}, 0.75); background: ${props => mixin.rgba(props.theme.colors.bg.primary, 0.75)};
} }
`; `;
@ -283,10 +281,10 @@ export const HeaderActionIcon = styled.div`
cursor: pointer; cursor: pointer;
svg { svg {
fill: rgba(${props => props.theme.colors.text.primary}, 0.75); fill: ${props => mixin.rgba(props.theme.colors.text.primary, 0.75)};
} }
&:hover svg { &:hover svg {
fill: rgba(${props => props.theme.colors.primary}); fill: ${props => mixin.rgba(props.theme.colors.primary, 0.75)});
} }
`; `;
@ -343,7 +341,7 @@ export const MetaDetail = styled.div`
`; `;
export const MetaDetailTitle = styled.h3` export const MetaDetailTitle = styled.h3`
color: rgba(${props => props.theme.colors.text.primary}); color: ${props => props.theme.colors.text.primary};
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
letter-spacing: 0.04em; letter-spacing: 0.04em;
@ -362,7 +360,7 @@ export const MetaDetailContent = styled.div`
`; `;
export const TaskDetailsAddLabel = styled.div` export const TaskDetailsAddLabel = styled.div`
border-radius: 3px; border-radius: 3px;
background: ${mixin.darken('#262c49', 0.15)}; background: ${props => mixin.darken(props.theme.colors.bg.secondary, 0.15)};
cursor: pointer; cursor: pointer;
&:hover { &:hover {
opacity: 0.8; opacity: 0.8;
@ -377,7 +375,7 @@ export const TaskDetailsAddLabelIcon = styled.div`
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-radius: 3px; border-radius: 3px;
background: ${mixin.darken('#262c49', 0.15)}; background: ${props => mixin.darken(props.theme.colors.bg.secondary, 0.15)};
cursor: pointer; cursor: pointer;
&:hover { &:hover {
opacity: 0.8; opacity: 0.8;
@ -452,11 +450,11 @@ export const TabBarSection = styled.div`
`; `;
export const TabBarItem = styled.div` export const TabBarItem = styled.div`
box-shadow: inset 0 -2px rgba(216, 93, 216); box-shadow: inset 0 -2px ${props => props.theme.colors.primary};
padding: 12px 7px 14px 7px; padding: 12px 7px 14px 7px;
margin-bottom: -1px; margin-bottom: -1px;
margin-right: 36px; margin-right: 36px;
color: rgba(${props => props.theme.colors.text.primary}); color: ${props => props.theme.colors.text.primary};
`; `;
export const CommentContainer = styled.div` export const CommentContainer = styled.div`
@ -491,7 +489,7 @@ export const CommentTextArea = styled(TextareaAutosize)`
line-height: 28px; line-height: 28px;
padding: 4px 6px; padding: 4px 6px;
border-radius: 6px; border-radius: 6px;
color: rgba(${props => props.theme.colors.text.primary}); color: ${props => props.theme.colors.text.primary};
background: #1f243e; background: #1f243e;
border: none; border: none;
transition: max-height 200ms, height 200ms, min-height 200ms; transition: max-height 200ms, height 200ms, min-height 200ms;
@ -556,13 +554,13 @@ export const ActivityItemHeaderTitle = styled.div`
`; `;
export const ActivityItemHeaderTitleName = styled.span` export const ActivityItemHeaderTitleName = styled.span`
color: rgba(${props => props.theme.colors.text.primary}); color: ${props => props.theme.colors.text.primary};
font-weight: 500; font-weight: 500;
`; `;
export const ActivityItemTimestamp = styled.span<{ margin: number }>` export const ActivityItemTimestamp = styled.span<{ margin: number }>`
font-size: 12px; font-size: 12px;
color: rgba(${props => props.theme.colors.text.primary}, 0.65); color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.65)};
margin-left: ${props => props.margin}px; margin-left: ${props => props.margin}px;
`; `;
@ -575,15 +573,15 @@ export const ActivityItemComment = styled.div`
border-radius: 3px; border-radius: 3px;
${mixin.boxShadowCard} ${mixin.boxShadowCard}
position: relative; position: relative;
color: rgba(${props => props.theme.colors.text.primary}); color: ${props => props.theme.colors.text.primary};
padding: 8px 12px; padding: 8px 12px;
margin: 4px 0; margin: 4px 0;
background-color: ${mixin.darken('#262c49', 0.1)}; background-color: ${props => mixin.darken(props.theme.colors.alternate, 0.1)};
`; `;
export const ActivityItemLog = styled.span` export const ActivityItemLog = styled.span`
margin-left: 2px; margin-left: 2px;
color: rgba(${props => props.theme.colors.text.primary}); color: ${props => props.theme.colors.text.primary};
`; `;
export const ViewRawButton = styled.button` export const ViewRawButton = styled.button`
@ -594,9 +592,9 @@ export const ViewRawButton = styled.button`
right: 4px; right: 4px;
bottom: -24px; bottom: -24px;
cursor: pointer; cursor: pointer;
color: rgba(${props => props.theme.colors.text.primary}, 0.25); color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.25)};
&:hover { &:hover {
color: rgba(${props => props.theme.colors.text.primary}); color: ${props => props.theme.colors.text.primary};
} }
`; `;

View File

@ -17,7 +17,7 @@ import dark from 'shared/utils/editorTheme';
import styled from 'styled-components'; import styled from 'styled-components';
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'; import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
import moment from 'moment'; import dayjs from 'dayjs';
import Task from 'shared/icons/Task'; import Task from 'shared/icons/Task';
import { import {
@ -182,7 +182,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
}} }}
> >
{task.dueDate ? ( {task.dueDate ? (
<SidebarButtonText>{moment(task.dueDate).format('MMM D [at] h:mm A')}</SidebarButtonText> <SidebarButtonText>{dayjs(task.dueDate).format('MMM D [at] h:mm A')}</SidebarButtonText>
) : ( ) : (
<SidebarButtonText>No due date</SidebarButtonText> <SidebarButtonText>No due date</SidebarButtonText>
)} )}

View File

@ -24,7 +24,7 @@ const Textarea = styled(TextareaAutosize)`
font-size: 20px; font-size: 20px;
padding: 3px 10px 3px 8px; padding: 3px 10px 3px 8px;
&:focus { &:focus {
box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px; box-shadow: ${props => props.theme.colors.primary} 0px 0px 0px 1px;
} }
`; `;

View File

@ -11,7 +11,8 @@ export const ProjectMember = styled(TaskAssignee)<{ zIndex: number }>`
z-index: ${props => props.zIndex}; z-index: ${props => props.zIndex};
position: relative; position: relative;
box-shadow: 0 0 0 2px rgba(16, 22, 58), inset 0 0 0 1px rgba(16, 22, 58, 0.07); box-shadow: 0 0 0 2px ${props => props.theme.colors.bg.primary},
inset 0 0 0 1px ${props => mixin.rgba(props.theme.colors.bg.primary, 0.07)};
`; `;
export const NavbarWrapper = styled.div` export const NavbarWrapper = styled.div`
@ -28,9 +29,9 @@ export const NavbarHeader = styled.header`
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
background: rgb(16, 22, 58); background: ${props => props.theme.colors.bg.primary};
box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.05); box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.05);
border-bottom: 1px solid rgba(65, 69, 97, 0.65); border-bottom: 1px solid ${props => mixin.rgba(props.theme.colors.alternate, 0.65)};
`; `;
export const Breadcrumbs = styled.div` export const Breadcrumbs = styled.div`
color: rgb(94, 108, 132); color: rgb(94, 108, 132);
@ -124,7 +125,7 @@ export const ProjectTabs = styled.div`
export const ProjectTab = styled(NavLink)` export const ProjectTab = styled(NavLink)`
font-size: 80%; font-size: 80%;
color: rgba(${props => props.theme.colors.text.primary}); color: ${props => props.theme.colors.text.primary};
font-size: 15px; font-size: 15px;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
@ -141,22 +142,22 @@ export const ProjectTab = styled(NavLink)`
} }
&:hover { &:hover {
box-shadow: inset 0 -2px rgba(${props => props.theme.colors.text.secondary}); box-shadow: inset 0 -2px ${props => props.theme.colors.text.secondary};
color: rgba(${props => props.theme.colors.text.secondary}); color: ${props => props.theme.colors.text.secondary};
} }
&.active { &.active {
box-shadow: inset 0 -2px rgba(${props => props.theme.colors.secondary}); box-shadow: inset 0 -2px ${props => props.theme.colors.secondary};
color: rgba(${props => props.theme.colors.secondary}); color: ${props => props.theme.colors.secondary};
} }
&.active:hover { &.active:hover {
box-shadow: inset 0 -2px rgba(${props => props.theme.colors.secondary}); box-shadow: inset 0 -2px ${props => props.theme.colors.secondary};
color: rgba(${props => props.theme.colors.secondary}); color: ${props => props.theme.colors.secondary};
} }
`; `;
export const ProjectName = styled.h1` export const ProjectName = styled.h1`
color: rgba(${props => props.theme.colors.text.primary}); color: ${props => props.theme.colors.text.primary};
font-weight: 600; font-weight: 600;
font-size: 20px; font-size: 20px;
padding: 3px 10px 3px 8px; padding: 3px 10px 3px 8px;
@ -185,7 +186,7 @@ export const ProjectNameTextarea = styled(TextareaAutosize)`
font-size: 20px; font-size: 20px;
padding: 3px 10px 3px 8px; padding: 3px 10px 3px 8px;
&:focus { &:focus {
box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px; box-shadow: ${props => props.theme.colors.primary} 0px 0px 0px 1px;
} }
`; `;
@ -203,7 +204,7 @@ export const ProjectSwitcher = styled.button`
color: #c2c6dc; color: #c2c6dc;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background: rgb(115, 103, 240); background: ${props => props.theme.colors.primary};
} }
`; `;
@ -227,7 +228,7 @@ export const ProjectSettingsButton = styled.button`
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background: rgb(115, 103, 240); background: ${props => props.theme.colors.primary};
} }
`; `;
@ -243,7 +244,7 @@ export const ProjectFinder = styled(Button)`
export const NavSeparator = styled.div` export const NavSeparator = styled.div`
width: 1px; width: 1px;
background: rgba(${props => props.theme.colors.border}); background: ${props => props.theme.colors.border};
height: 34px; height: 34px;
margin: 0 20px; margin: 0 20px;
`; `;
@ -260,11 +261,11 @@ export const LogoContainer = styled(Link)`
export const TaskcafeTitle = styled.h2` export const TaskcafeTitle = styled.h2`
margin-left: 5px; margin-left: 5px;
color: rgba(${props => props.theme.colors.text.primary}); color: ${props => props.theme.colors.text.primary};
font-size: 20px; font-size: 20px;
`; `;
export const TaskcafeLogo = styled(Taskcafe)` export const TaskcafeLogo = styled(Taskcafe)`
fill: rgba(${props => props.theme.colors.text.primary}); fill: ${props => props.theme.colors.text.primary};
stroke: rgba(${props => props.theme.colors.text.primary}); stroke: ${props => props.theme.colors.text.primary};
`; `;

View File

@ -2,8 +2,8 @@ import React, { useState } from 'react';
import NormalizeStyles from 'App/NormalizeStyles'; import NormalizeStyles from 'App/NormalizeStyles';
import BaseStyles from 'App/BaseStyles'; import BaseStyles from 'App/BaseStyles';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import DropdownMenu from 'shared/components/DropdownMenu';
import TopNavbar from '.'; import TopNavbar from '.';
import theme from '../../../App/ThemeStyles';
export default { export default {
component: TopNavbar, component: TopNavbar,
@ -15,7 +15,7 @@ export default {
backgrounds: [ backgrounds: [
{ name: 'white', value: '#ffffff' }, { name: 'white', value: '#ffffff' },
{ name: 'gray', value: '#f8f8f8' }, { name: 'gray', value: '#f8f8f8' },
{ name: 'darkBlue', value: '#262c49', default: true }, { name: 'darkBlue', value: theme.colors.bg.secondary, default: true },
], ],
}, },
}; };

View File

@ -1,10 +1,11 @@
import React, { useRef, useState, useEffect } from 'react'; import React, { useRef, useState, useEffect } from 'react';
import { Home, Star, Bell, AngleDown, BarChart, CheckCircle } from 'shared/icons'; import { Home, Star, Bell, AngleDown, BarChart, CheckCircle, ListUnordered } from 'shared/icons';
import styled from 'styled-components'; import styled from 'styled-components';
import ProfileIcon from 'shared/components/ProfileIcon'; import ProfileIcon from 'shared/components/ProfileIcon';
import { usePopup } from 'shared/components/PopupMenu'; import { usePopup } from 'shared/components/PopupMenu';
import { RoleCode } from 'shared/generated/graphql'; import { RoleCode } from 'shared/generated/graphql';
import NOOP from 'shared/utils/noop'; import NOOP from 'shared/utils/noop';
import { useHistory } from 'react-router';
import { import {
TaskcafeLogo, TaskcafeLogo,
TaskcafeTitle, TaskcafeTitle,
@ -173,8 +174,11 @@ type NavBarProps = {
user: TaskUser | null; user: TaskUser | null;
onOpenSettings: ($target: React.RefObject<HTMLElement>) => void; onOpenSettings: ($target: React.RefObject<HTMLElement>) => void;
projectMembers?: Array<TaskUser> | null; projectMembers?: Array<TaskUser> | null;
projectInvitedMembers?: Array<InvitedUser> | null;
onRemoveFromBoard?: (userID: string) => void; onRemoveFromBoard?: (userID: string) => void;
onMemberProfile?: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void; onMemberProfile?: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
onInvitedMemberProfile?: ($targetRef: React.RefObject<HTMLElement>, email: string) => void;
}; };
const NavBar: React.FC<NavBarProps> = ({ const NavBar: React.FC<NavBarProps> = ({
@ -184,10 +188,12 @@ const NavBar: React.FC<NavBarProps> = ({
onChangeProjectOwner, onChangeProjectOwner,
currentTab, currentTab,
onMemberProfile, onMemberProfile,
onInvitedMemberProfile,
canEditProjectName = false, canEditProjectName = false,
onOpenProjectFinder, onOpenProjectFinder,
onFavorite, onFavorite,
onSetTab, onSetTab,
projectInvitedMembers,
onChangeRole, onChangeRole,
name, name,
onRemoveFromBoard, onRemoveFromBoard,
@ -204,6 +210,7 @@ const NavBar: React.FC<NavBarProps> = ({
onProfileClick($target); onProfileClick($target);
} }
}; };
const history = useHistory();
const { showPopup } = usePopup(); const { showPopup } = usePopup();
return ( return (
<NavbarWrapper> <NavbarWrapper>
@ -245,19 +252,38 @@ const NavBar: React.FC<NavBarProps> = ({
<TaskcafeTitle>Taskcafé</TaskcafeTitle> <TaskcafeTitle>Taskcafé</TaskcafeTitle>
</LogoContainer> </LogoContainer>
<GlobalActions> <GlobalActions>
{projectMembers && onMemberProfile && ( {projectMembers && projectInvitedMembers && onMemberProfile && onInvitedMemberProfile && (
<> <>
<ProjectMembers> <ProjectMembers>
{projectMembers.map((member, idx) => ( {projectMembers.map((member, idx) => (
<ProjectMember <ProjectMember
showRoleIcons showRoleIcons
zIndex={projectMembers.length - idx} zIndex={projectMembers.length - idx + projectInvitedMembers.length}
key={member.id} key={member.id}
size={28} size={28}
member={member} member={member}
onMemberProfile={onMemberProfile} onMemberProfile={onMemberProfile}
/> />
))} ))}
{projectInvitedMembers.map((member, idx) => (
<ProjectMember
showRoleIcons
zIndex={projectInvitedMembers.length - idx}
key={member.email}
size={28}
invited
member={{
id: member.email,
fullName: member.email,
profileIcon: {
url: null,
initials: member.email.charAt(0),
bgColor: '#fff',
},
}}
onMemberProfile={onInvitedMemberProfile}
/>
))}
{canInviteUser && ( {canInviteUser && (
<InviteButton <InviteButton
onClick={$target => { onClick={$target => {
@ -283,6 +309,9 @@ const NavBar: React.FC<NavBarProps> = ({
<IconContainer disabled onClick={NOOP}> <IconContainer disabled onClick={NOOP}>
<CheckCircle width={20} height={20} /> <CheckCircle width={20} height={20} />
</IconContainer> </IconContainer>
<IconContainer disabled onClick={NOOP}>
<ListUnordered width={20} height={20} />
</IconContainer>
<IconContainer disabled onClick={onNotificationClick}> <IconContainer disabled onClick={onNotificationClick}>
<Bell color="#c2c6dc" size={20} /> <Bell color="#c2c6dc" size={20} />
</IconContainer> </IconContainer>

View File

@ -113,6 +113,14 @@ export type UserAccount = {
member: MemberList; member: MemberList;
}; };
export type InvitedUserAccount = {
__typename?: 'InvitedUserAccount';
id: Scalars['ID'];
email: Scalars['String'];
invitedOn: Scalars['Time'];
member: MemberList;
};
export type Team = { export type Team = {
__typename?: 'Team'; __typename?: 'Team';
id: Scalars['ID']; id: Scalars['ID'];
@ -121,14 +129,21 @@ export type Team = {
members: Array<Member>; members: Array<Member>;
}; };
export type InvitedMember = {
__typename?: 'InvitedMember';
email: Scalars['String'];
invitedOn: Scalars['Time'];
};
export type Project = { export type Project = {
__typename?: 'Project'; __typename?: 'Project';
id: Scalars['ID']; id: Scalars['ID'];
createdAt: Scalars['Time']; createdAt: Scalars['Time'];
name: Scalars['String']; name: Scalars['String'];
team: Team; team?: Maybe<Team>;
taskGroups: Array<TaskGroup>; taskGroups: Array<TaskGroup>;
members: Array<Member>; members: Array<Member>;
invitedMembers: Array<InvitedMember>;
labels: Array<ProjectLabel>; labels: Array<ProjectLabel>;
}; };
@ -209,7 +224,10 @@ export enum ObjectType {
Org = 'ORG', Org = 'ORG',
Team = 'TEAM', Team = 'TEAM',
Project = 'PROJECT', Project = 'PROJECT',
Task = 'TASK' Task = 'TASK',
TaskGroup = 'TASK_GROUP',
TaskChecklist = 'TASK_CHECKLIST',
TaskChecklistItem = 'TASK_CHECKLIST_ITEM'
} }
export type Query = { export type Query = {
@ -218,11 +236,13 @@ export type Query = {
findTask: Task; findTask: Task;
findTeam: Team; findTeam: Team;
findUser: UserAccount; findUser: UserAccount;
invitedUsers: Array<InvitedUserAccount>;
labelColors: Array<LabelColor>; labelColors: Array<LabelColor>;
me: MePayload; me: MePayload;
notifications: Array<Notification>; notifications: Array<Notification>;
organizations: Array<Organization>; organizations: Array<Organization>;
projects: Array<Project>; projects: Array<Project>;
searchMembers: Array<MemberSearchResult>;
taskGroups: Array<TaskGroup>; taskGroups: Array<TaskGroup>;
teams: Array<Team>; teams: Array<Team>;
users: Array<UserAccount>; users: Array<UserAccount>;
@ -253,6 +273,11 @@ export type QueryProjectsArgs = {
input?: Maybe<ProjectsFilter>; input?: Maybe<ProjectsFilter>;
}; };
export type QuerySearchMembersArgs = {
input: MemberSearchFilter;
};
export type Mutation = { export type Mutation = {
__typename?: 'Mutation'; __typename?: 'Mutation';
addTaskLabel: Task; addTaskLabel: Task;
@ -260,7 +285,6 @@ export type Mutation = {
clearProfileAvatar: UserAccount; clearProfileAvatar: UserAccount;
createProject: Project; createProject: Project;
createProjectLabel: ProjectLabel; createProjectLabel: ProjectLabel;
createProjectMember: CreateProjectMemberPayload;
createRefreshToken: RefreshToken; createRefreshToken: RefreshToken;
createTask: Task; createTask: Task;
createTaskChecklist: TaskChecklist; createTaskChecklist: TaskChecklist;
@ -269,6 +293,8 @@ export type Mutation = {
createTeam: Team; createTeam: Team;
createTeamMember: CreateTeamMemberPayload; createTeamMember: CreateTeamMemberPayload;
createUserAccount: UserAccount; createUserAccount: UserAccount;
deleteInvitedProjectMember: DeleteInvitedProjectMemberPayload;
deleteInvitedUserAccount: DeleteInvitedUserAccountPayload;
deleteProject: DeleteProjectPayload; deleteProject: DeleteProjectPayload;
deleteProjectLabel: ProjectLabel; deleteProjectLabel: ProjectLabel;
deleteProjectMember: DeleteProjectMemberPayload; deleteProjectMember: DeleteProjectMemberPayload;
@ -281,6 +307,7 @@ export type Mutation = {
deleteTeamMember: DeleteTeamMemberPayload; deleteTeamMember: DeleteTeamMemberPayload;
deleteUserAccount: DeleteUserAccountPayload; deleteUserAccount: DeleteUserAccountPayload;
duplicateTaskGroup: DuplicateTaskGroupPayload; duplicateTaskGroup: DuplicateTaskGroupPayload;
inviteProjectMembers: InviteProjectMembersPayload;
logoutUser: Scalars['Boolean']; logoutUser: Scalars['Boolean'];
removeTaskLabel: Task; removeTaskLabel: Task;
setTaskChecklistItemComplete: TaskChecklistItem; setTaskChecklistItemComplete: TaskChecklistItem;
@ -330,11 +357,6 @@ export type MutationCreateProjectLabelArgs = {
}; };
export type MutationCreateProjectMemberArgs = {
input: CreateProjectMember;
};
export type MutationCreateRefreshTokenArgs = { export type MutationCreateRefreshTokenArgs = {
input: NewRefreshToken; input: NewRefreshToken;
}; };
@ -375,6 +397,16 @@ export type MutationCreateUserAccountArgs = {
}; };
export type MutationDeleteInvitedProjectMemberArgs = {
input: DeleteInvitedProjectMember;
};
export type MutationDeleteInvitedUserAccountArgs = {
input: DeleteInvitedUserAccount;
};
export type MutationDeleteProjectArgs = { export type MutationDeleteProjectArgs = {
input: DeleteProject; input: DeleteProject;
}; };
@ -435,6 +467,11 @@ export type MutationDuplicateTaskGroupArgs = {
}; };
export type MutationInviteProjectMembersArgs = {
input: InviteProjectMembers;
};
export type MutationLogoutUserArgs = { export type MutationLogoutUserArgs = {
input: LogoutUser; input: LogoutUser;
}; };
@ -588,7 +625,7 @@ export type ProjectsFilter = {
}; };
export type FindUser = { export type FindUser = {
userId: Scalars['String']; userID: Scalars['UUID'];
}; };
export type FindProject = { export type FindProject = {
@ -640,8 +677,7 @@ export type Notification = {
}; };
export type NewProject = { export type NewProject = {
userID: Scalars['UUID']; teamID?: Maybe<Scalars['UUID']>;
teamID: Scalars['UUID'];
name: Scalars['String']; name: Scalars['String'];
}; };
@ -686,15 +722,32 @@ export type UpdateProjectLabelColor = {
labelColorID: Scalars['UUID']; labelColorID: Scalars['UUID'];
}; };
export type CreateProjectMember = { export type DeleteInvitedProjectMember = {
projectID: Scalars['UUID']; projectID: Scalars['UUID'];
userID: Scalars['UUID']; email: Scalars['String'];
}; };
export type CreateProjectMemberPayload = { export type DeleteInvitedProjectMemberPayload = {
__typename?: 'CreateProjectMemberPayload'; __typename?: 'DeleteInvitedProjectMemberPayload';
invitedMember: InvitedMember;
};
export type MemberInvite = {
userID?: Maybe<Scalars['UUID']>;
email?: Maybe<Scalars['String']>;
};
export type InviteProjectMembers = {
projectID: Scalars['UUID'];
members: Array<MemberInvite>;
};
export type InviteProjectMembersPayload = {
__typename?: 'InviteProjectMembersPayload';
ok: Scalars['Boolean']; ok: Scalars['Boolean'];
member: Member; projectID: Scalars['UUID'];
members: Array<Member>;
invitedMembers: Array<InvitedMember>;
}; };
export type DeleteProjectMember = { export type DeleteProjectMember = {
@ -722,7 +775,7 @@ export type UpdateProjectMemberRolePayload = {
}; };
export type NewTask = { export type NewTask = {
taskGroupID: Scalars['String']; taskGroupID: Scalars['UUID'];
name: Scalars['String']; name: Scalars['String'];
position: Scalars['Float']; position: Scalars['Float'];
}; };
@ -765,34 +818,34 @@ export type NewTaskLocation = {
}; };
export type DeleteTaskInput = { export type DeleteTaskInput = {
taskID: Scalars['String']; taskID: Scalars['UUID'];
}; };
export type DeleteTaskPayload = { export type DeleteTaskPayload = {
__typename?: 'DeleteTaskPayload'; __typename?: 'DeleteTaskPayload';
taskID: Scalars['String']; taskID: Scalars['UUID'];
}; };
export type UpdateTaskName = { export type UpdateTaskName = {
taskID: Scalars['String']; taskID: Scalars['UUID'];
name: Scalars['String']; name: Scalars['String'];
}; };
export type UpdateTaskChecklistItemLocation = { export type UpdateTaskChecklistItemLocation = {
checklistID: Scalars['UUID']; taskChecklistID: Scalars['UUID'];
checklistItemID: Scalars['UUID']; taskChecklistItemID: Scalars['UUID'];
position: Scalars['Float']; position: Scalars['Float'];
}; };
export type UpdateTaskChecklistItemLocationPayload = { export type UpdateTaskChecklistItemLocationPayload = {
__typename?: 'UpdateTaskChecklistItemLocationPayload'; __typename?: 'UpdateTaskChecklistItemLocationPayload';
checklistID: Scalars['UUID']; taskChecklistID: Scalars['UUID'];
prevChecklistID: Scalars['UUID']; prevChecklistID: Scalars['UUID'];
checklistItem: TaskChecklistItem; checklistItem: TaskChecklistItem;
}; };
export type UpdateTaskChecklistLocation = { export type UpdateTaskChecklistLocation = {
checklistID: Scalars['UUID']; taskChecklistID: Scalars['UUID'];
position: Scalars['Float']; position: Scalars['Float'];
}; };
@ -908,7 +961,7 @@ export type DeleteTaskGroupPayload = {
}; };
export type NewTaskGroup = { export type NewTaskGroup = {
projectID: Scalars['String']; projectID: Scalars['UUID'];
name: Scalars['String']; name: Scalars['String'];
position: Scalars['Float']; position: Scalars['Float'];
}; };
@ -919,6 +972,7 @@ export type AddTaskLabelInput = {
}; };
export type RemoveTaskLabelInput = { export type RemoveTaskLabelInput = {
taskID: Scalars['UUID'];
taskLabelID: Scalars['UUID']; taskLabelID: Scalars['UUID'];
}; };
@ -986,6 +1040,29 @@ export type UpdateTeamMemberRolePayload = {
member: Member; member: Member;
}; };
export type DeleteInvitedUserAccount = {
invitedUserID: Scalars['UUID'];
};
export type DeleteInvitedUserAccountPayload = {
__typename?: 'DeleteInvitedUserAccountPayload';
invitedUser: InvitedUserAccount;
};
export type MemberSearchFilter = {
SearchFilter: Scalars['String'];
projectID?: Maybe<Scalars['UUID']>;
};
export type MemberSearchResult = {
__typename?: 'MemberSearchResult';
similarity: Scalars['Int'];
user: UserAccount;
confirmed: Scalars['Boolean'];
invited: Scalars['Boolean'];
joined: Scalars['Boolean'];
};
export type UpdateUserInfoPayload = { export type UpdateUserInfoPayload = {
__typename?: 'UpdateUserInfoPayload'; __typename?: 'UpdateUserInfoPayload';
user: UserAccount; user: UserAccount;
@ -1020,7 +1097,7 @@ export type UpdateUserRolePayload = {
}; };
export type NewRefreshToken = { export type NewRefreshToken = {
userId: Scalars['String']; userID: Scalars['UUID'];
}; };
export type NewUserAccount = { export type NewUserAccount = {
@ -1033,7 +1110,7 @@ export type NewUserAccount = {
}; };
export type LogoutUser = { export type LogoutUser = {
userID: Scalars['String']; userID: Scalars['UUID'];
}; };
export type DeleteUserAccount = { export type DeleteUserAccount = {
@ -1081,8 +1158,7 @@ export type ClearProfileAvatarMutation = (
); );
export type CreateProjectMutationVariables = { export type CreateProjectMutationVariables = {
teamID: Scalars['UUID']; teamID?: Maybe<Scalars['UUID']>;
userID: Scalars['UUID'];
name: Scalars['String']; name: Scalars['String'];
}; };
@ -1092,10 +1168,10 @@ export type CreateProjectMutation = (
& { createProject: ( & { createProject: (
{ __typename?: 'Project' } { __typename?: 'Project' }
& Pick<Project, 'id' | 'name'> & Pick<Project, 'id' | 'name'>
& { team: ( & { team?: Maybe<(
{ __typename?: 'Team' } { __typename?: 'Team' }
& Pick<Team, 'id' | 'name'> & Pick<Team, 'id' | 'name'>
) } )> }
) } ) }
); );
@ -1119,7 +1195,7 @@ export type CreateProjectLabelMutation = (
); );
export type CreateTaskGroupMutationVariables = { export type CreateTaskGroupMutationVariables = {
projectID: Scalars['String']; projectID: Scalars['UUID'];
name: Scalars['String']; name: Scalars['String'];
position: Scalars['Float']; position: Scalars['Float'];
}; };
@ -1147,7 +1223,7 @@ export type DeleteProjectLabelMutation = (
); );
export type DeleteTaskMutationVariables = { export type DeleteTaskMutationVariables = {
taskID: Scalars['String']; taskID: Scalars['UUID'];
}; };
@ -1190,10 +1266,10 @@ export type FindProjectQuery = (
& { findProject: ( & { findProject: (
{ __typename?: 'Project' } { __typename?: 'Project' }
& Pick<Project, 'name'> & Pick<Project, 'name'>
& { team: ( & { team?: Maybe<(
{ __typename?: 'Team' } { __typename?: 'Team' }
& Pick<Team, 'id'> & Pick<Team, 'id'>
), members: Array<( )>, members: Array<(
{ __typename?: 'Member' } { __typename?: 'Member' }
& Pick<Member, 'id' | 'fullName' | 'username'> & Pick<Member, 'id' | 'fullName' | 'username'>
& { role: ( & { role: (
@ -1203,6 +1279,9 @@ export type FindProjectQuery = (
{ __typename?: 'ProfileIcon' } { __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'> & Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
) } ) }
)>, invitedMembers: Array<(
{ __typename?: 'InvitedMember' }
& Pick<InvitedMember, 'email' | 'invitedOn'>
)>, labels: Array<( )>, labels: Array<(
{ __typename?: 'ProjectLabel' } { __typename?: 'ProjectLabel' }
& Pick<ProjectLabel, 'id' | 'createdDate' | 'name'> & Pick<ProjectLabel, 'id' | 'createdDate' | 'name'>
@ -1357,10 +1436,10 @@ export type GetProjectsQuery = (
)>, projects: Array<( )>, projects: Array<(
{ __typename?: 'Project' } { __typename?: 'Project' }
& Pick<Project, 'id' | 'name'> & Pick<Project, 'id' | 'name'>
& { team: ( & { team?: Maybe<(
{ __typename?: 'Team' } { __typename?: 'Team' }
& Pick<Team, 'id' | 'name'> & Pick<Team, 'id' | 'name'>
) } )> }
)> } )> }
); );
@ -1388,31 +1467,6 @@ export type MeQuery = (
) } ) }
); );
export type CreateProjectMemberMutationVariables = {
projectID: Scalars['UUID'];
userID: Scalars['UUID'];
};
export type CreateProjectMemberMutation = (
{ __typename?: 'Mutation' }
& { createProjectMember: (
{ __typename?: 'CreateProjectMemberPayload' }
& Pick<CreateProjectMemberPayload, 'ok'>
& { member: (
{ __typename?: 'Member' }
& Pick<Member, 'id' | 'fullName' | 'username'>
& { profileIcon: (
{ __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
), role: (
{ __typename?: 'Role' }
& Pick<Role, 'code' | 'name'>
) }
) }
) }
);
export type DeleteProjectMutationVariables = { export type DeleteProjectMutationVariables = {
projectID: Scalars['UUID']; projectID: Scalars['UUID'];
}; };
@ -1430,6 +1484,23 @@ export type DeleteProjectMutation = (
) } ) }
); );
export type DeleteInvitedProjectMemberMutationVariables = {
projectID: Scalars['UUID'];
email: Scalars['String'];
};
export type DeleteInvitedProjectMemberMutation = (
{ __typename?: 'Mutation' }
& { deleteInvitedProjectMember: (
{ __typename?: 'DeleteInvitedProjectMemberPayload' }
& { invitedMember: (
{ __typename?: 'InvitedMember' }
& Pick<InvitedMember, 'email'>
) }
) }
);
export type DeleteProjectMemberMutationVariables = { export type DeleteProjectMemberMutationVariables = {
projectID: Scalars['UUID']; projectID: Scalars['UUID'];
userID: Scalars['UUID']; userID: Scalars['UUID'];
@ -1448,6 +1519,34 @@ export type DeleteProjectMemberMutation = (
) } ) }
); );
export type InviteProjectMembersMutationVariables = {
projectID: Scalars['UUID'];
members: Array<MemberInvite>;
};
export type InviteProjectMembersMutation = (
{ __typename?: 'Mutation' }
& { inviteProjectMembers: (
{ __typename?: 'InviteProjectMembersPayload' }
& Pick<InviteProjectMembersPayload, 'ok'>
& { invitedMembers: Array<(
{ __typename?: 'InvitedMember' }
& Pick<InvitedMember, 'email' | 'invitedOn'>
)>, members: Array<(
{ __typename?: 'Member' }
& Pick<Member, 'id' | 'fullName' | 'username'>
& { profileIcon: (
{ __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
), role: (
{ __typename?: 'Role' }
& Pick<Role, 'code' | 'name'>
) }
)> }
) }
);
export type UpdateProjectMemberRoleMutationVariables = { export type UpdateProjectMemberRoleMutationVariables = {
projectID: Scalars['UUID']; projectID: Scalars['UUID'];
userID: Scalars['UUID']; userID: Scalars['UUID'];
@ -1472,7 +1571,7 @@ export type UpdateProjectMemberRoleMutation = (
); );
export type CreateTaskMutationVariables = { export type CreateTaskMutationVariables = {
taskGroupID: Scalars['String']; taskGroupID: Scalars['UUID'];
name: Scalars['String']; name: Scalars['String'];
position: Scalars['Float']; position: Scalars['Float'];
}; };
@ -1583,8 +1682,8 @@ export type SetTaskCompleteMutation = (
); );
export type UpdateTaskChecklistItemLocationMutationVariables = { export type UpdateTaskChecklistItemLocationMutationVariables = {
checklistID: Scalars['UUID']; taskChecklistID: Scalars['UUID'];
checklistItemID: Scalars['UUID']; taskChecklistItemID: Scalars['UUID'];
position: Scalars['Float']; position: Scalars['Float'];
}; };
@ -1593,7 +1692,7 @@ export type UpdateTaskChecklistItemLocationMutation = (
{ __typename?: 'Mutation' } { __typename?: 'Mutation' }
& { updateTaskChecklistItemLocation: ( & { updateTaskChecklistItemLocation: (
{ __typename?: 'UpdateTaskChecklistItemLocationPayload' } { __typename?: 'UpdateTaskChecklistItemLocationPayload' }
& Pick<UpdateTaskChecklistItemLocationPayload, 'checklistID' | 'prevChecklistID'> & Pick<UpdateTaskChecklistItemLocationPayload, 'taskChecklistID' | 'prevChecklistID'>
& { checklistItem: ( & { checklistItem: (
{ __typename?: 'TaskChecklistItem' } { __typename?: 'TaskChecklistItem' }
& Pick<TaskChecklistItem, 'id' | 'taskChecklistID' | 'position'> & Pick<TaskChecklistItem, 'id' | 'taskChecklistID' | 'position'>
@ -1616,7 +1715,7 @@ export type UpdateTaskChecklistItemNameMutation = (
); );
export type UpdateTaskChecklistLocationMutationVariables = { export type UpdateTaskChecklistLocationMutationVariables = {
checklistID: Scalars['UUID']; taskChecklistID: Scalars['UUID'];
position: Scalars['Float']; position: Scalars['Float'];
}; };
@ -1833,10 +1932,10 @@ export type GetTeamQuery = (
), projects: Array<( ), projects: Array<(
{ __typename?: 'Project' } { __typename?: 'Project' }
& Pick<Project, 'id' | 'name'> & Pick<Project, 'id' | 'name'>
& { team: ( & { team?: Maybe<(
{ __typename?: 'Team' } { __typename?: 'Team' }
& Pick<Team, 'id' | 'name'> & Pick<Team, 'id' | 'name'>
) } )> }
)>, users: Array<( )>, users: Array<(
{ __typename?: 'UserAccount' } { __typename?: 'UserAccount' }
& Pick<UserAccount, 'id' | 'email' | 'fullName' | 'username'> & Pick<UserAccount, 'id' | 'email' | 'fullName' | 'username'>
@ -2072,7 +2171,7 @@ export type UpdateTaskLocationMutation = (
); );
export type UpdateTaskNameMutationVariables = { export type UpdateTaskNameMutationVariables = {
taskID: Scalars['String']; taskID: Scalars['UUID'];
name: Scalars['String']; name: Scalars['String'];
}; };
@ -2128,6 +2227,22 @@ export type CreateUserAccountMutation = (
) } ) }
); );
export type DeleteInvitedUserAccountMutationVariables = {
invitedUserID: Scalars['UUID'];
};
export type DeleteInvitedUserAccountMutation = (
{ __typename?: 'Mutation' }
& { deleteInvitedUserAccount: (
{ __typename?: 'DeleteInvitedUserAccountPayload' }
& { invitedUser: (
{ __typename?: 'InvitedUserAccount' }
& Pick<InvitedUserAccount, 'id'>
) }
) }
);
export type DeleteUserAccountMutationVariables = { export type DeleteUserAccountMutationVariables = {
userID: Scalars['UUID']; userID: Scalars['UUID'];
newOwnerID?: Maybe<Scalars['UUID']>; newOwnerID?: Maybe<Scalars['UUID']>;
@ -2209,7 +2324,10 @@ export type UsersQueryVariables = {};
export type UsersQuery = ( export type UsersQuery = (
{ __typename?: 'Query' } { __typename?: 'Query' }
& { users: Array<( & { invitedUsers: Array<(
{ __typename?: 'InvitedUserAccount' }
& Pick<InvitedUserAccount, 'id' | 'email' | 'invitedOn'>
)>, users: Array<(
{ __typename?: 'UserAccount' } { __typename?: 'UserAccount' }
& Pick<UserAccount, 'id' | 'email' | 'fullName' | 'username'> & Pick<UserAccount, 'id' | 'email' | 'fullName' | 'username'>
& { role: ( & { role: (
@ -2361,8 +2479,8 @@ export type ClearProfileAvatarMutationHookResult = ReturnType<typeof useClearPro
export type ClearProfileAvatarMutationResult = ApolloReactCommon.MutationResult<ClearProfileAvatarMutation>; export type ClearProfileAvatarMutationResult = ApolloReactCommon.MutationResult<ClearProfileAvatarMutation>;
export type ClearProfileAvatarMutationOptions = ApolloReactCommon.BaseMutationOptions<ClearProfileAvatarMutation, ClearProfileAvatarMutationVariables>; export type ClearProfileAvatarMutationOptions = ApolloReactCommon.BaseMutationOptions<ClearProfileAvatarMutation, ClearProfileAvatarMutationVariables>;
export const CreateProjectDocument = gql` export const CreateProjectDocument = gql`
mutation createProject($teamID: UUID!, $userID: UUID!, $name: String!) { mutation createProject($teamID: UUID, $name: String!) {
createProject(input: {teamID: $teamID, userID: $userID, name: $name}) { createProject(input: {teamID: $teamID, name: $name}) {
id id
name name
team { team {
@ -2388,7 +2506,6 @@ export type CreateProjectMutationFn = ApolloReactCommon.MutationFunction<CreateP
* const [createProjectMutation, { data, loading, error }] = useCreateProjectMutation({ * const [createProjectMutation, { data, loading, error }] = useCreateProjectMutation({
* variables: { * variables: {
* teamID: // value for 'teamID' * teamID: // value for 'teamID'
* userID: // value for 'userID'
* name: // value for 'name' * name: // value for 'name'
* }, * },
* }); * });
@ -2442,7 +2559,7 @@ export type CreateProjectLabelMutationHookResult = ReturnType<typeof useCreatePr
export type CreateProjectLabelMutationResult = ApolloReactCommon.MutationResult<CreateProjectLabelMutation>; export type CreateProjectLabelMutationResult = ApolloReactCommon.MutationResult<CreateProjectLabelMutation>;
export type CreateProjectLabelMutationOptions = ApolloReactCommon.BaseMutationOptions<CreateProjectLabelMutation, CreateProjectLabelMutationVariables>; export type CreateProjectLabelMutationOptions = ApolloReactCommon.BaseMutationOptions<CreateProjectLabelMutation, CreateProjectLabelMutationVariables>;
export const CreateTaskGroupDocument = gql` export const CreateTaskGroupDocument = gql`
mutation createTaskGroup($projectID: String!, $name: String!, $position: Float!) { mutation createTaskGroup($projectID: UUID!, $name: String!, $position: Float!) {
createTaskGroup(input: {projectID: $projectID, name: $name, position: $position}) { createTaskGroup(input: {projectID: $projectID, name: $name, position: $position}) {
id id
name name
@ -2510,7 +2627,7 @@ export type DeleteProjectLabelMutationHookResult = ReturnType<typeof useDeletePr
export type DeleteProjectLabelMutationResult = ApolloReactCommon.MutationResult<DeleteProjectLabelMutation>; export type DeleteProjectLabelMutationResult = ApolloReactCommon.MutationResult<DeleteProjectLabelMutation>;
export type DeleteProjectLabelMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteProjectLabelMutation, DeleteProjectLabelMutationVariables>; export type DeleteProjectLabelMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteProjectLabelMutation, DeleteProjectLabelMutationVariables>;
export const DeleteTaskDocument = gql` export const DeleteTaskDocument = gql`
mutation deleteTask($taskID: String!) { mutation deleteTask($taskID: UUID!) {
deleteTask(input: {taskID: $taskID}) { deleteTask(input: {taskID: $taskID}) {
taskID taskID
} }
@ -2602,6 +2719,10 @@ export const FindProjectDocument = gql`
bgColor bgColor
} }
} }
invitedMembers {
email
invitedOn
}
labels { labels {
id id
createdDate createdDate
@ -2883,53 +3004,6 @@ export function useMeLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptio
export type MeQueryHookResult = ReturnType<typeof useMeQuery>; export type MeQueryHookResult = ReturnType<typeof useMeQuery>;
export type MeLazyQueryHookResult = ReturnType<typeof useMeLazyQuery>; export type MeLazyQueryHookResult = ReturnType<typeof useMeLazyQuery>;
export type MeQueryResult = ApolloReactCommon.QueryResult<MeQuery, MeQueryVariables>; export type MeQueryResult = ApolloReactCommon.QueryResult<MeQuery, MeQueryVariables>;
export const CreateProjectMemberDocument = gql`
mutation createProjectMember($projectID: UUID!, $userID: UUID!) {
createProjectMember(input: {projectID: $projectID, userID: $userID}) {
ok
member {
id
fullName
profileIcon {
url
initials
bgColor
}
username
role {
code
name
}
}
}
}
`;
export type CreateProjectMemberMutationFn = ApolloReactCommon.MutationFunction<CreateProjectMemberMutation, CreateProjectMemberMutationVariables>;
/**
* __useCreateProjectMemberMutation__
*
* To run a mutation, you first call `useCreateProjectMemberMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useCreateProjectMemberMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [createProjectMemberMutation, { data, loading, error }] = useCreateProjectMemberMutation({
* variables: {
* projectID: // value for 'projectID'
* userID: // value for 'userID'
* },
* });
*/
export function useCreateProjectMemberMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<CreateProjectMemberMutation, CreateProjectMemberMutationVariables>) {
return ApolloReactHooks.useMutation<CreateProjectMemberMutation, CreateProjectMemberMutationVariables>(CreateProjectMemberDocument, baseOptions);
}
export type CreateProjectMemberMutationHookResult = ReturnType<typeof useCreateProjectMemberMutation>;
export type CreateProjectMemberMutationResult = ApolloReactCommon.MutationResult<CreateProjectMemberMutation>;
export type CreateProjectMemberMutationOptions = ApolloReactCommon.BaseMutationOptions<CreateProjectMemberMutation, CreateProjectMemberMutationVariables>;
export const DeleteProjectDocument = gql` export const DeleteProjectDocument = gql`
mutation deleteProject($projectID: UUID!) { mutation deleteProject($projectID: UUID!) {
deleteProject(input: {projectID: $projectID}) { deleteProject(input: {projectID: $projectID}) {
@ -2965,6 +3039,41 @@ export function useDeleteProjectMutation(baseOptions?: ApolloReactHooks.Mutation
export type DeleteProjectMutationHookResult = ReturnType<typeof useDeleteProjectMutation>; export type DeleteProjectMutationHookResult = ReturnType<typeof useDeleteProjectMutation>;
export type DeleteProjectMutationResult = ApolloReactCommon.MutationResult<DeleteProjectMutation>; export type DeleteProjectMutationResult = ApolloReactCommon.MutationResult<DeleteProjectMutation>;
export type DeleteProjectMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteProjectMutation, DeleteProjectMutationVariables>; export type DeleteProjectMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteProjectMutation, DeleteProjectMutationVariables>;
export const DeleteInvitedProjectMemberDocument = gql`
mutation deleteInvitedProjectMember($projectID: UUID!, $email: String!) {
deleteInvitedProjectMember(input: {projectID: $projectID, email: $email}) {
invitedMember {
email
}
}
}
`;
export type DeleteInvitedProjectMemberMutationFn = ApolloReactCommon.MutationFunction<DeleteInvitedProjectMemberMutation, DeleteInvitedProjectMemberMutationVariables>;
/**
* __useDeleteInvitedProjectMemberMutation__
*
* To run a mutation, you first call `useDeleteInvitedProjectMemberMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useDeleteInvitedProjectMemberMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [deleteInvitedProjectMemberMutation, { data, loading, error }] = useDeleteInvitedProjectMemberMutation({
* variables: {
* projectID: // value for 'projectID'
* email: // value for 'email'
* },
* });
*/
export function useDeleteInvitedProjectMemberMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<DeleteInvitedProjectMemberMutation, DeleteInvitedProjectMemberMutationVariables>) {
return ApolloReactHooks.useMutation<DeleteInvitedProjectMemberMutation, DeleteInvitedProjectMemberMutationVariables>(DeleteInvitedProjectMemberDocument, baseOptions);
}
export type DeleteInvitedProjectMemberMutationHookResult = ReturnType<typeof useDeleteInvitedProjectMemberMutation>;
export type DeleteInvitedProjectMemberMutationResult = ApolloReactCommon.MutationResult<DeleteInvitedProjectMemberMutation>;
export type DeleteInvitedProjectMemberMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteInvitedProjectMemberMutation, DeleteInvitedProjectMemberMutationVariables>;
export const DeleteProjectMemberDocument = gql` export const DeleteProjectMemberDocument = gql`
mutation deleteProjectMember($projectID: UUID!, $userID: UUID!) { mutation deleteProjectMember($projectID: UUID!, $userID: UUID!) {
deleteProjectMember(input: {projectID: $projectID, userID: $userID}) { deleteProjectMember(input: {projectID: $projectID, userID: $userID}) {
@ -3002,6 +3111,57 @@ export function useDeleteProjectMemberMutation(baseOptions?: ApolloReactHooks.Mu
export type DeleteProjectMemberMutationHookResult = ReturnType<typeof useDeleteProjectMemberMutation>; export type DeleteProjectMemberMutationHookResult = ReturnType<typeof useDeleteProjectMemberMutation>;
export type DeleteProjectMemberMutationResult = ApolloReactCommon.MutationResult<DeleteProjectMemberMutation>; export type DeleteProjectMemberMutationResult = ApolloReactCommon.MutationResult<DeleteProjectMemberMutation>;
export type DeleteProjectMemberMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteProjectMemberMutation, DeleteProjectMemberMutationVariables>; export type DeleteProjectMemberMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteProjectMemberMutation, DeleteProjectMemberMutationVariables>;
export const InviteProjectMembersDocument = gql`
mutation inviteProjectMembers($projectID: UUID!, $members: [MemberInvite!]!) {
inviteProjectMembers(input: {projectID: $projectID, members: $members}) {
ok
invitedMembers {
email
invitedOn
}
members {
id
fullName
profileIcon {
url
initials
bgColor
}
username
role {
code
name
}
}
}
}
`;
export type InviteProjectMembersMutationFn = ApolloReactCommon.MutationFunction<InviteProjectMembersMutation, InviteProjectMembersMutationVariables>;
/**
* __useInviteProjectMembersMutation__
*
* To run a mutation, you first call `useInviteProjectMembersMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useInviteProjectMembersMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [inviteProjectMembersMutation, { data, loading, error }] = useInviteProjectMembersMutation({
* variables: {
* projectID: // value for 'projectID'
* members: // value for 'members'
* },
* });
*/
export function useInviteProjectMembersMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<InviteProjectMembersMutation, InviteProjectMembersMutationVariables>) {
return ApolloReactHooks.useMutation<InviteProjectMembersMutation, InviteProjectMembersMutationVariables>(InviteProjectMembersDocument, baseOptions);
}
export type InviteProjectMembersMutationHookResult = ReturnType<typeof useInviteProjectMembersMutation>;
export type InviteProjectMembersMutationResult = ApolloReactCommon.MutationResult<InviteProjectMembersMutation>;
export type InviteProjectMembersMutationOptions = ApolloReactCommon.BaseMutationOptions<InviteProjectMembersMutation, InviteProjectMembersMutationVariables>;
export const UpdateProjectMemberRoleDocument = gql` export const UpdateProjectMemberRoleDocument = gql`
mutation updateProjectMemberRole($projectID: UUID!, $userID: UUID!, $roleCode: RoleCode!) { mutation updateProjectMemberRole($projectID: UUID!, $userID: UUID!, $roleCode: RoleCode!) {
updateProjectMemberRole(input: {projectID: $projectID, userID: $userID, roleCode: $roleCode}) { updateProjectMemberRole(input: {projectID: $projectID, userID: $userID, roleCode: $roleCode}) {
@ -3044,7 +3204,7 @@ export type UpdateProjectMemberRoleMutationHookResult = ReturnType<typeof useUpd
export type UpdateProjectMemberRoleMutationResult = ApolloReactCommon.MutationResult<UpdateProjectMemberRoleMutation>; export type UpdateProjectMemberRoleMutationResult = ApolloReactCommon.MutationResult<UpdateProjectMemberRoleMutation>;
export type UpdateProjectMemberRoleMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateProjectMemberRoleMutation, UpdateProjectMemberRoleMutationVariables>; export type UpdateProjectMemberRoleMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateProjectMemberRoleMutation, UpdateProjectMemberRoleMutationVariables>;
export const CreateTaskDocument = gql` export const CreateTaskDocument = gql`
mutation createTask($taskGroupID: String!, $name: String!, $position: Float!) { mutation createTask($taskGroupID: UUID!, $name: String!, $position: Float!) {
createTask(input: {taskGroupID: $taskGroupID, name: $name, position: $position}) { createTask(input: {taskGroupID: $taskGroupID, name: $name, position: $position}) {
...TaskFields ...TaskFields
} }
@ -3297,9 +3457,9 @@ export type SetTaskCompleteMutationHookResult = ReturnType<typeof useSetTaskComp
export type SetTaskCompleteMutationResult = ApolloReactCommon.MutationResult<SetTaskCompleteMutation>; export type SetTaskCompleteMutationResult = ApolloReactCommon.MutationResult<SetTaskCompleteMutation>;
export type SetTaskCompleteMutationOptions = ApolloReactCommon.BaseMutationOptions<SetTaskCompleteMutation, SetTaskCompleteMutationVariables>; export type SetTaskCompleteMutationOptions = ApolloReactCommon.BaseMutationOptions<SetTaskCompleteMutation, SetTaskCompleteMutationVariables>;
export const UpdateTaskChecklistItemLocationDocument = gql` export const UpdateTaskChecklistItemLocationDocument = gql`
mutation updateTaskChecklistItemLocation($checklistID: UUID!, $checklistItemID: UUID!, $position: Float!) { mutation updateTaskChecklistItemLocation($taskChecklistID: UUID!, $taskChecklistItemID: UUID!, $position: Float!) {
updateTaskChecklistItemLocation(input: {checklistID: $checklistID, checklistItemID: $checklistItemID, position: $position}) { updateTaskChecklistItemLocation(input: {taskChecklistID: $taskChecklistID, taskChecklistItemID: $taskChecklistItemID, position: $position}) {
checklistID taskChecklistID
prevChecklistID prevChecklistID
checklistItem { checklistItem {
id id
@ -3324,8 +3484,8 @@ export type UpdateTaskChecklistItemLocationMutationFn = ApolloReactCommon.Mutati
* @example * @example
* const [updateTaskChecklistItemLocationMutation, { data, loading, error }] = useUpdateTaskChecklistItemLocationMutation({ * const [updateTaskChecklistItemLocationMutation, { data, loading, error }] = useUpdateTaskChecklistItemLocationMutation({
* variables: { * variables: {
* checklistID: // value for 'checklistID' * taskChecklistID: // value for 'taskChecklistID'
* checklistItemID: // value for 'checklistItemID' * taskChecklistItemID: // value for 'taskChecklistItemID'
* position: // value for 'position' * position: // value for 'position'
* }, * },
* }); * });
@ -3371,8 +3531,8 @@ export type UpdateTaskChecklistItemNameMutationHookResult = ReturnType<typeof us
export type UpdateTaskChecklistItemNameMutationResult = ApolloReactCommon.MutationResult<UpdateTaskChecklistItemNameMutation>; export type UpdateTaskChecklistItemNameMutationResult = ApolloReactCommon.MutationResult<UpdateTaskChecklistItemNameMutation>;
export type UpdateTaskChecklistItemNameMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateTaskChecklistItemNameMutation, UpdateTaskChecklistItemNameMutationVariables>; export type UpdateTaskChecklistItemNameMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateTaskChecklistItemNameMutation, UpdateTaskChecklistItemNameMutationVariables>;
export const UpdateTaskChecklistLocationDocument = gql` export const UpdateTaskChecklistLocationDocument = gql`
mutation updateTaskChecklistLocation($checklistID: UUID!, $position: Float!) { mutation updateTaskChecklistLocation($taskChecklistID: UUID!, $position: Float!) {
updateTaskChecklistLocation(input: {checklistID: $checklistID, position: $position}) { updateTaskChecklistLocation(input: {taskChecklistID: $taskChecklistID, position: $position}) {
checklist { checklist {
id id
position position
@ -3395,7 +3555,7 @@ export type UpdateTaskChecklistLocationMutationFn = ApolloReactCommon.MutationFu
* @example * @example
* const [updateTaskChecklistLocationMutation, { data, loading, error }] = useUpdateTaskChecklistLocationMutation({ * const [updateTaskChecklistLocationMutation, { data, loading, error }] = useUpdateTaskChecklistLocationMutation({
* variables: { * variables: {
* checklistID: // value for 'checklistID' * taskChecklistID: // value for 'taskChecklistID'
* position: // value for 'position' * position: // value for 'position'
* }, * },
* }); * });
@ -4275,7 +4435,7 @@ export type UpdateTaskLocationMutationHookResult = ReturnType<typeof useUpdateTa
export type UpdateTaskLocationMutationResult = ApolloReactCommon.MutationResult<UpdateTaskLocationMutation>; export type UpdateTaskLocationMutationResult = ApolloReactCommon.MutationResult<UpdateTaskLocationMutation>;
export type UpdateTaskLocationMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateTaskLocationMutation, UpdateTaskLocationMutationVariables>; export type UpdateTaskLocationMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateTaskLocationMutation, UpdateTaskLocationMutationVariables>;
export const UpdateTaskNameDocument = gql` export const UpdateTaskNameDocument = gql`
mutation updateTaskName($taskID: String!, $name: String!) { mutation updateTaskName($taskID: UUID!, $name: String!) {
updateTaskName(input: {taskID: $taskID, name: $name}) { updateTaskName(input: {taskID: $taskID, name: $name}) {
id id
name name
@ -4380,6 +4540,40 @@ export function useCreateUserAccountMutation(baseOptions?: ApolloReactHooks.Muta
export type CreateUserAccountMutationHookResult = ReturnType<typeof useCreateUserAccountMutation>; export type CreateUserAccountMutationHookResult = ReturnType<typeof useCreateUserAccountMutation>;
export type CreateUserAccountMutationResult = ApolloReactCommon.MutationResult<CreateUserAccountMutation>; export type CreateUserAccountMutationResult = ApolloReactCommon.MutationResult<CreateUserAccountMutation>;
export type CreateUserAccountMutationOptions = ApolloReactCommon.BaseMutationOptions<CreateUserAccountMutation, CreateUserAccountMutationVariables>; export type CreateUserAccountMutationOptions = ApolloReactCommon.BaseMutationOptions<CreateUserAccountMutation, CreateUserAccountMutationVariables>;
export const DeleteInvitedUserAccountDocument = gql`
mutation deleteInvitedUserAccount($invitedUserID: UUID!) {
deleteInvitedUserAccount(input: {invitedUserID: $invitedUserID}) {
invitedUser {
id
}
}
}
`;
export type DeleteInvitedUserAccountMutationFn = ApolloReactCommon.MutationFunction<DeleteInvitedUserAccountMutation, DeleteInvitedUserAccountMutationVariables>;
/**
* __useDeleteInvitedUserAccountMutation__
*
* To run a mutation, you first call `useDeleteInvitedUserAccountMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useDeleteInvitedUserAccountMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [deleteInvitedUserAccountMutation, { data, loading, error }] = useDeleteInvitedUserAccountMutation({
* variables: {
* invitedUserID: // value for 'invitedUserID'
* },
* });
*/
export function useDeleteInvitedUserAccountMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<DeleteInvitedUserAccountMutation, DeleteInvitedUserAccountMutationVariables>) {
return ApolloReactHooks.useMutation<DeleteInvitedUserAccountMutation, DeleteInvitedUserAccountMutationVariables>(DeleteInvitedUserAccountDocument, baseOptions);
}
export type DeleteInvitedUserAccountMutationHookResult = ReturnType<typeof useDeleteInvitedUserAccountMutation>;
export type DeleteInvitedUserAccountMutationResult = ApolloReactCommon.MutationResult<DeleteInvitedUserAccountMutation>;
export type DeleteInvitedUserAccountMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteInvitedUserAccountMutation, DeleteInvitedUserAccountMutationVariables>;
export const DeleteUserAccountDocument = gql` export const DeleteUserAccountDocument = gql`
mutation deleteUserAccount($userID: UUID!, $newOwnerID: UUID) { mutation deleteUserAccount($userID: UUID!, $newOwnerID: UUID) {
deleteUserAccount(input: {userID: $userID, newOwnerID: $newOwnerID}) { deleteUserAccount(input: {userID: $userID, newOwnerID: $newOwnerID}) {
@ -4533,6 +4727,11 @@ export type UpdateUserRoleMutationResult = ApolloReactCommon.MutationResult<Upda
export type UpdateUserRoleMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateUserRoleMutation, UpdateUserRoleMutationVariables>; export type UpdateUserRoleMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateUserRoleMutation, UpdateUserRoleMutationVariables>;
export const UsersDocument = gql` export const UsersDocument = gql`
query users { query users {
invitedUsers {
id
email
invitedOn
}
users { users {
id id
email email

View File

@ -1,5 +1,5 @@
mutation createProject($teamID: UUID!, $userID: UUID!, $name: String!) { mutation createProject($teamID: UUID, $name: String!) {
createProject(input: {teamID: $teamID, userID: $userID, name: $name}) { createProject(input: {teamID: $teamID, name: $name}) {
id id
name name
team { team {

View File

@ -1,4 +1,4 @@
mutation createTaskGroup( $projectID: String!, $name: String!, $position: Float! ) { mutation createTaskGroup( $projectID: UUID!, $name: String!, $position: Float! ) {
createTaskGroup( createTaskGroup(
input: { projectID: $projectID, name: $name, position: $position } input: { projectID: $projectID, name: $name, position: $position }
) { ) {

View File

@ -1,4 +1,4 @@
mutation deleteTask($taskID: String!) { mutation deleteTask($taskID: UUID!) {
deleteTask(input: { taskID: $taskID }) { deleteTask(input: { taskID: $taskID }) {
taskID taskID
} }

View File

@ -22,6 +22,10 @@ query findProject($projectID: UUID!) {
bgColor bgColor
} }
} }
invitedMembers {
email
invitedOn
}
labels { labels {
id id
createdDate createdDate

View File

@ -1,25 +0,0 @@
import gql from 'graphql-tag';
export const CREATE_PROJECT_MEMBER_MUTATION = gql`
mutation createProjectMember($projectID: UUID!, $userID: UUID!) {
createProjectMember(input: { projectID: $projectID, userID: $userID }) {
ok
member {
id
fullName
profileIcon {
url
initials
bgColor
}
username
role {
code
name
}
}
}
}
`;
export default CREATE_PROJECT_MEMBER_MUTATION;

View File

@ -0,0 +1,13 @@
import gql from 'graphql-tag';
export const DELETE_PROJECT_INVITED_MEMBER_MUTATION = gql`
mutation deleteInvitedProjectMember($projectID: UUID!, $email: String!) {
deleteInvitedProjectMember(input: { projectID: $projectID, email: $email }) {
invitedMember {
email
}
}
}
`;
export default DELETE_PROJECT_INVITED_MEMBER_MUTATION;

View File

@ -0,0 +1,29 @@
import gql from 'graphql-tag';
export const INVITE_PROJECT_MEMBERS_MUTATION = gql`
mutation inviteProjectMembers($projectID: UUID!, $members: [MemberInvite!]!) {
inviteProjectMembers(input: { projectID: $projectID, members: $members }) {
ok
invitedMembers {
email
invitedOn
}
members {
id
fullName
profileIcon {
url
initials
bgColor
}
username
role {
code
name
}
}
}
}
`;
export default INVITE_PROJECT_MEMBERS_MUTATION;

View File

@ -2,7 +2,7 @@ import gql from 'graphql-tag';
import TASK_FRAGMENT from '../fragments/task'; import TASK_FRAGMENT from '../fragments/task';
const CREATE_TASK_MUTATION = gql` const CREATE_TASK_MUTATION = gql`
mutation createTask($taskGroupID: String!, $name: String!, $position: Float!) { mutation createTask($taskGroupID: UUID!, $name: String!, $position: Float!) {
createTask(input: { taskGroupID: $taskGroupID, name: $name, position: $position }) { createTask(input: { taskGroupID: $taskGroupID, name: $name, position: $position }) {
...TaskFields ...TaskFields
} }

View File

@ -1,11 +1,11 @@
import gql from 'graphql-tag'; import gql from 'graphql-tag';
const UPDATE_TASK_CHECKLIST_ITEM_LOCATION_MUTATION = gql` const UPDATE_TASK_CHECKLIST_ITEM_LOCATION_MUTATION = gql`
mutation updateTaskChecklistItemLocation($checklistID: UUID!, $checklistItemID: UUID!, $position: Float!) { mutation updateTaskChecklistItemLocation($taskChecklistID: UUID!, $taskChecklistItemID: UUID!, $position: Float!) {
updateTaskChecklistItemLocation( updateTaskChecklistItemLocation(
input: { checklistID: $checklistID, checklistItemID: $checklistItemID, position: $position } input: { taskChecklistID: $taskChecklistID, taskChecklistItemID: $taskChecklistItemID, position: $position }
) { ) {
checklistID taskChecklistID
prevChecklistID prevChecklistID
checklistItem { checklistItem {
id id

View File

@ -1,8 +1,8 @@
import gql from 'graphql-tag'; import gql from 'graphql-tag';
const UPDATE_TASK_CHECKLIST_LOCATION_MUTATION = gql` const UPDATE_TASK_CHECKLIST_LOCATION_MUTATION = gql`
mutation updateTaskChecklistLocation($checklistID: UUID!, $position: Float!) { mutation updateTaskChecklistLocation($taskChecklistID: UUID!, $position: Float!) {
updateTaskChecklistLocation(input: { checklistID: $checklistID, position: $position }) { updateTaskChecklistLocation(input: { taskChecklistID: $taskChecklistID, position: $position }) {
checklist { checklist {
id id
position position

View File

@ -1,4 +1,4 @@
mutation updateTaskName($taskID: String!, $name: String!) { mutation updateTaskName($taskID: UUID!, $name: String!) {
updateTaskName(input: { taskID: $taskID, name: $name }) { updateTaskName(input: { taskID: $taskID, name: $name }) {
id id
name name

View File

@ -0,0 +1,13 @@
import gql from 'graphql-tag';
export const DELETE_INVITED_USER_MUTATION = gql`
mutation deleteInvitedUserAccount($invitedUserID: UUID!) {
deleteInvitedUserAccount(input: { invitedUserID: $invitedUserID }) {
invitedUser {
id
}
}
}
`;
export default DELETE_INVITED_USER_MUTATION;

View File

@ -1,4 +1,9 @@
query users { query users {
invitedUsers {
id
email
invitedOn
}
users { users {
id id
email email

View File

@ -0,0 +1,12 @@
import React from 'react';
import Icon, { IconProps } from './Icon';
const ArrowDown: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
return (
<Icon width={width} height={height} className={className} viewBox="0 0 448 512">
<path d="M413.1 222.5l22.2 22.2c9.4 9.4 9.4 24.6 0 33.9L241 473c-9.4 9.4-24.6 9.4-33.9 0L12.7 278.6c-9.4-9.4-9.4-24.6 0-33.9l22.2-22.2c9.5-9.5 25-9.3 34.3.4L184 343.4V56c0-13.3 10.7-24 24-24h32c13.3 0 24 10.7 24 24v287.4l114.8-120.5c9.3-9.8 24.8-10 34.3-.4z" />
</Icon>
);
};
export default ArrowDown;

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