19 Commits

Author SHA1 Message Date
960f07cd11 feat: ability to create new nodes on enter and delete old nodes on delete 2021-02-14 06:00:55 -06:00
19d302355f feat: add outliner 2020-12-22 14:35:46 -06:00
f4ef7fec83 feat: add task activity 2020-12-22 13:03:08 -06:00
f732b211c9 fix: update bg color variable name in MemberManager 2020-12-18 20:36:08 -06:00
b5fd3b1bf1 refactor: make theme more consistent 2020-12-17 22:56:49 -06:00
ea767f3d19 fix: replace deprecated method with a correct one 2020-12-17 22:47:43 -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
174 changed files with 15022 additions and 6536 deletions

View File

@ -21,4 +21,4 @@ windows:
- database:
root: ./
panes:
- pgcli postgres://taskcafe:taskcafe_test@localhost:5432/taskcafe
- pgcli postgres://taskcafe:taskcafe_test@localhost:8855/taskcafe

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
- 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
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" />
</a>
<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 href="https://goreportcard.com/report/github.com/JordanKnott/taskcafe">
<img alt="Go Report Card" src="https://goreportcard.com/badge/github.com/JordanKnott/taskcafe" />
</a>
<a href="">
<img alt="Docker pulls" src="https://img.shields.io/docker/pulls/taskcafe/taskcafe" />
</a>
</p>
<p align="center">
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

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

View File

@ -31,7 +31,9 @@
"@typescript-eslint/no-unused-vars": "off",
"react/jsx-filename-extension": [2, { "extensions": [".js", ".jsx", ".ts", ".tsx"] }],
"no-case-declarations": "off",
"no-plusplus": "off",
"react/prop-types": 0,
"no-continue": "off",
"react/jsx-props-no-spreading": "off",
"no-param-reassign": "off",
"import/extensions": [

View File

@ -12,7 +12,9 @@
"@types/jest": "^24.0.0",
"@types/jwt-decode": "^2.2.1",
"@types/lodash": "^4.14.149",
"@types/marked": "^1.2.2",
"@types/node": "^12.0.0",
"@types/query-string": "^6.3.0",
"@types/react": "^16.9.21",
"@types/react-beautiful-dnd": "^12.1.1",
"@types/react-datepicker": "^2.11.0",
@ -21,6 +23,7 @@
"@types/react-router-dom": "^5.1.3",
"@types/react-select": "^3.0.13",
"@types/react-timeago": "^4.1.1",
"@types/react-window": "^1.8.2",
"@types/styled-components": "^5.0.0",
"apollo-cache-inmemory": "^1.6.5",
"apollo-client": "^2.6.8",
@ -33,14 +36,16 @@
"axios-auth-refresh": "^2.2.7",
"color": "^3.1.2",
"date-fns": "^2.14.0",
"dayjs": "^1.9.1",
"graphql": "^15.0.0",
"graphql-tag": "^2.10.3",
"history": "^4.10.1",
"immer": "^6.0.3",
"jwt-decode": "^2.2.0",
"lodash": "^4.17.15",
"moment": "^2.24.0",
"lodash": "^4.17.20",
"marked": "^2.0.0",
"prop-types": "^15.7.2",
"query-string": "^6.13.7",
"react": "^16.12.0",
"react-autosize-textarea": "^7.0.0",
"react-beautiful-dnd": "^13.0.0",
@ -52,9 +57,11 @@
"react-router-dom": "^5.1.2",
"react-scripts": "3.4.0",
"react-select": "^3.1.0",
"rich-markdown-editor": "^10.6.5",
"react-timeago": "^4.4.0",
"react-toastify": "^6.0.8",
"react-visibility-sensor": "^5.1.1",
"react-window": "^1.8.6",
"rich-markdown-editor": "^10.6.5",
"styled-components": "^5.0.1",
"typescript": "~3.7.2"
},

View File

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

View File

@ -1,16 +1,21 @@
import React from 'react';
import { Switch, Route } from 'react-router-dom';
import React, { useEffect, useState } from 'react';
import { Switch, Route, useHistory } from 'react-router-dom';
import * as H from 'history';
import Dashboard from 'Dashboard';
import Admin from 'Admin';
import Confirm from 'Confirm';
import Projects from 'Projects';
import Project from 'Projects/Project';
import Teams from 'Teams';
import Login from 'Auth';
import Install from 'Install';
import Register from 'Register';
import Profile from 'Profile';
import styled from 'styled-components';
import JwtDecode from 'jwt-decode';
import { setAccessToken } from 'shared/utils/accessToken';
import { useCurrentUser } from 'App/context';
import Outline from 'Outline';
const MainContent = styled.div`
padding: 0 0 0 0;
@ -21,6 +26,62 @@ const MainContent = styled.div`
flex-grow: 1;
`;
type RefreshTokenResponse = {
accessToken: string;
setup?: null | { confirmToken: string };
};
const AuthorizedRoutes = () => {
const history = useHistory();
const [loading, setLoading] = useState(true);
const { setUser } = useCurrentUser();
useEffect(() => {
const abortController = new AbortController();
fetch('/auth/refresh_token', {
signal: abortController.signal,
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 () => {
abortController.abort();
};
}, []);
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="/outline" component={Outline} />
<Route path="/profile" component={Profile} />
<Route path="/admin" component={Admin} />
</MainContent>
</Switch>
);
};
type RoutesProps = {
history: H.History;
};
@ -28,15 +89,9 @@ type RoutesProps = {
const Routes: React.FC<RoutesProps> = () => (
<Switch>
<Route exact path="/login" component={Login} />
<Route exact path="/install" component={Install} />
<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>
<Route exact path="/register" component={Register} />
<Route exact path="/confirm" component={Confirm} />
<AuthorizedRoutes />
</Switch>
);

View File

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

View File

@ -20,6 +20,7 @@ import MiniProfile from 'shared/components/MiniProfile';
import cache from 'App/cache';
import NOOP from 'shared/utils/noop';
import NotificationPopup, { NotificationItem } from 'shared/components/NotifcationPopup';
import theme from './ThemeStyles';
const TeamContainer = styled.div`
display: flex;
@ -62,7 +63,7 @@ const TeamProjectBackground = styled.div<{ color: string }>`
opacity: 1;
border-radius: 3px;
&:before {
background: rgba(${props => props.theme.colors.bg.secondary});
background: ${props => props.theme.colors.bg.secondary};
bottom: 0;
content: '';
left: 0;
@ -114,7 +115,7 @@ const TeamProjectContainer = styled.div`
margin: 0 4px 4px 0;
min-width: 0;
&:hover ${TeamProjectTitle} {
color: rgba(${props => props.theme.colors.text.secondary});
color: ${props => props.theme.colors.text.secondary};
}
&:hover ${TeamProjectAvatar} {
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 { loading, data } = useGetProjectsQuery();
@ -137,7 +138,7 @@ const ProjectFinder = () => {
return {
id: team.id,
name: team.name,
projects: projects.filter(project => project.team.id === team.id),
projects: projects.filter(project => project.team && project.team.id === team.id),
};
});
return (
@ -166,7 +167,7 @@ const ProjectFinder = () => {
return <span>error</span>;
};
type ProjectPopupProps = {
history: History<History.PoorMansUnknown>;
history: any;
name: string;
projectID: string;
};
@ -181,7 +182,7 @@ export const ProjectPopup: React.FC<ProjectPopupProps> = ({ history, name, proje
const newData = produce(cacheData, (draftState: any) => {
draftState.projects = draftState.projects.filter(
(project: any) => project.id !== deleteData.data.deleteProject.project.id,
(project: any) => project.id !== deleteData.data?.deleteProject.project.id,
);
});
@ -230,10 +231,12 @@ type GlobalTopNavbarProps = {
menuType?: Array<MenuItem>;
onChangeRole?: (userID: string, roleCode: RoleCode) => void;
projectMembers?: null | Array<TaskUser>;
projectInvitedMembers?: null | Array<InvitedUser>;
onSaveProjectName?: (projectName: string) => void;
onInviteUser?: ($target: React.RefObject<HTMLElement>) => void;
onSetTab?: (tab: number) => void;
onRemoveFromBoard?: (userID: string) => void;
onRemoveInvitedFromBoard?: (email: string) => void;
};
const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
@ -246,8 +249,10 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
name,
popupContent,
projectMembers,
projectInvitedMembers,
onInviteUser,
onSaveProjectName,
onRemoveInvitedFromBoard,
onRemoveFromBoard,
}) => {
const { user, setUserRoles, setUser } = useCurrentUser();
@ -324,7 +329,7 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
/>
))}
</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;
}
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 member = projectMembers ? projectMembers.find(u => u.id === memberID) : null;
const warning =
@ -382,6 +415,7 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
canEditProjectName={userIsTeamOrProjectAdmin}
canInviteUser={userIsTeamOrProjectAdmin}
onMemberProfile={onMemberProfile}
onInvitedMemberProfile={onInvitedMemberProfile}
onInviteUser={onInviteUser}
onChangeRole={onChangeRole}
onChangeProjectOwner={onChangeProjectOwner}
@ -392,6 +426,7 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
history.push('/');
}}
projectMembers={projectMembers}
projectInvitedMembers={projectInvitedMembers}
onProfileClick={onProfileClick}
onSaveName={onSaveProjectName}
onOpenSettings={onOpenSettings}

View File

@ -28,13 +28,13 @@ const StyledContainer = styled(ToastContainer).attrs({
color: #fff;
}
.Toastify__toast--error {
background: rgba(${props => props.theme.colors.danger});
background: ${props => props.theme.colors.danger};
}
.Toastify__toast--warning {
background: rgba(${props => props.theme.colors.warning});
background: ${props => props.theme.colors.warning};
}
.Toastify__toast--success {
background: rgba(${props => props.theme.colors.success});
background: ${props => props.theme.colors.success};
}
.Toastify__toast-body {
}
@ -46,13 +46,8 @@ const StyledContainer = styled(ToastContainer).attrs({
`;
const history = createBrowserHistory();
type RefreshTokenResponse = {
accessToken: string;
isInstalled: boolean;
};
const App = () => {
const [loading, setLoading] = useState(true);
const [user, setUser] = useState<CurrentUserRaw | null>(null);
const setUserRoles = (roles: CurrentUserRoles) => {
if (user) {
@ -63,32 +58,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 (
<>
<UserContext.Provider value={{ user, setUser, setUserRoles }}>
@ -97,13 +66,7 @@ const App = () => {
<BaseStyles />
<Router history={history}>
<PopupProvider>
{loading ? (
<div>loading</div>
) : (
<>
<Routes history={history} />
</>
)}
<Routes history={history} />
</PopupProvider>
</Router>
<StyledContainer

View File

@ -52,7 +52,20 @@ const Auth = () => {
}).then(async x => {
const { status } = x;
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

@ -0,0 +1,24 @@
import React from 'react';
import { DragDebugWrapper } from './Styles';
type DragDebugProps = {
zone: ImpactZone | null;
depthTarget: number;
draggedNodes: Array<string> | null;
};
const DragDebug: React.FC<DragDebugProps> = ({ zone, depthTarget, draggedNodes }) => {
let aboveID = null;
let belowID = null;
if (zone) {
aboveID = zone.above ? zone.above.node.id : null;
belowID = zone.below ? zone.below.node.id : null;
}
return (
<DragDebugWrapper>{`aboveID=${aboveID} / belowID=${belowID} / depthTarget=${depthTarget} draggedNodes=${
draggedNodes ? draggedNodes.toString() : null
}`}</DragDebugWrapper>
);
};
export default DragDebug;

View File

@ -0,0 +1,41 @@
import React from 'react';
import { getDimensions } from './utils';
import { DragIndicatorBar } from './Styles';
type DragIndicatorProps = {
container: React.RefObject<HTMLDivElement>;
zone: ImpactZone;
depthTarget: number;
};
const DragIndicator: React.FC<DragIndicatorProps> = ({ container, zone, depthTarget }) => {
let top = 0;
let width = 0;
if (zone.below === null) {
if (zone.above) {
const entry = getDimensions(zone.above.dimensions.entry);
const children = getDimensions(zone.above.dimensions.children);
if (children) {
top = children.top;
width = children.width - depthTarget * 35;
} else if (entry) {
top = entry.bottom;
width = entry.width - depthTarget * 35;
}
}
} else if (zone.below) {
const entry = getDimensions(zone.below.dimensions.entry);
if (entry) {
top = entry.top;
width = entry.width - depthTarget * 35;
}
}
let left = 0;
if (container && container.current) {
left = container.current.getBoundingClientRect().left + (depthTarget - 1) * 35;
width = container.current.getBoundingClientRect().width - depthTarget * 35;
}
return <DragIndicatorBar top={top} left={left} width={width} />;
};
export default DragIndicator;

View File

@ -0,0 +1,385 @@
import React, { useRef, useCallback, useState, useMemo, useEffect } from 'react';
import { Dot } from 'shared/icons';
import styled from 'styled-components';
import {
findNextDraggable,
getDimensions,
getTargetDepth,
getNodeAbove,
getBelowParent,
findNodeAbove,
getNodeOver,
getLastChildInBranch,
findNodeDepth,
} from './utils';
import { useDrag } from './useDrag';
const Container = styled.div`
display: flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
border-radius: 9px;
background: rgba(${p => p.theme.colors.primary});
svg {
fill: rgba(${p => p.theme.colors.text.primary});
stroke: rgba(${p => p.theme.colors.text.primary});
}
`;
type DraggerProps = {
container: React.RefObject<HTMLDivElement>;
draggedNodes: { nodes: Array<string>; first?: OutlineNode | null };
isDragging: boolean;
onDragEnd: (zone: ImpactZone) => void;
initialPos: { x: number; y: number };
pageRef: React.RefObject<HTMLDivElement>;
};
let timer: any = null;
type windowScrollOptions = {
maxScrollX: number;
maxScrollY: number;
isInTopEdge: boolean;
isInBottomEdge: boolean;
edgeTop: number;
edgeBottom: number;
edgeSize: number;
viewportY: number;
$page: React.RefObject<HTMLDivElement>;
};
function adjustWindowScroll({
maxScrollY,
maxScrollX,
$page,
isInTopEdge,
isInBottomEdge,
edgeTop,
edgeBottom,
edgeSize,
viewportY,
}: windowScrollOptions) {
// Get the current scroll position of the document.
if ($page.current) {
var currentScrollX = $page.current.scrollLeft;
var currentScrollY = $page.current.scrollTop;
// Determine if the window can be scrolled in any particular direction.
var canScrollUp = currentScrollY > 0;
var canScrollDown = currentScrollY < maxScrollY;
// Since we can potentially scroll in two directions at the same time,
// let's keep track of the next scroll, starting with the current scroll.
// Each of these values can then be adjusted independently in the logic
// below.
var nextScrollX = currentScrollX;
var nextScrollY = currentScrollY;
// As we examine the mouse position within the edge, we want to make the
// incremental scroll changes more "intense" the closer that the user
// gets the viewport edge. As such, we'll calculate the percentage that
// the user has made it "through the edge" when calculating the delta.
// Then, that use that percentage to back-off from the "max" step value.
var maxStep = 50;
// Should we scroll up?
if (isInTopEdge && canScrollUp) {
var intensity = (edgeTop - viewportY) / edgeSize;
nextScrollY = nextScrollY - maxStep * intensity;
// Should we scroll down?
} else if (isInBottomEdge && canScrollDown) {
var intensity = (viewportY - edgeBottom) / edgeSize;
nextScrollY = nextScrollY + maxStep * intensity;
}
// Sanitize invalid maximums. An invalid scroll offset won't break the
// subsequent .scrollTo() call; however, it will make it harder to
// determine if the .scrollTo() method should have been called in the
// first place.
nextScrollX = Math.max(0, Math.min(maxScrollX, nextScrollX));
nextScrollY = Math.max(0, Math.min(maxScrollY, nextScrollY));
if (nextScrollX !== currentScrollX || nextScrollY !== currentScrollY) {
$page.current.scrollTo(nextScrollX, nextScrollY);
return true;
} else {
return false;
}
}
}
const Dragger: React.FC<DraggerProps> = ({
draggedNodes,
container,
onDragEnd,
isDragging,
initialPos,
pageRef: $page,
}) => {
const [pos, setPos] = useState<{ x: number; y: number }>(initialPos);
const { outline, impact, setImpact } = useDrag();
const $handle = useRef<HTMLDivElement>(null);
const handleMouseUp = useCallback(() => {
onDragEnd(impact ? impact.zone : { below: null, above: null });
}, [impact]);
const handleMouseMove = useCallback(
e => {
var t0 = performance.now();
e.preventDefault();
const { clientX, clientY, pageX, pageY } = e;
setPos({ x: clientX, y: clientY });
const { curDepth, curPosition, curDraggable } = getNodeOver({ x: clientX, y: clientY }, outline.current);
let depthTarget: number = 0;
let aboveNode: null | OutlineNode = null;
let belowNode: null | OutlineNode = null;
const edgeSize = 50;
const viewportWidth = document.documentElement.clientWidth;
const viewportHeight = document.documentElement.clientHeight;
var edgeTop = edgeSize + 80;
var edgeBottom = viewportHeight - edgeSize;
var isInTopEdge = clientY < edgeTop;
var isInBottomEdge = clientY > edgeBottom;
if ((isInBottomEdge || isInTopEdge) && $page.current) {
var documentWidth = Math.max(
$page.current.scrollWidth,
$page.current.offsetWidth,
$page.current.clientWidth,
$page.current.scrollWidth,
$page.current.offsetWidth,
$page.current.clientWidth,
);
var documentHeight = Math.max(
$page.current.scrollHeight,
$page.current.offsetHeight,
$page.current.clientHeight,
$page.current.scrollHeight,
$page.current.offsetHeight,
$page.current.clientHeight,
);
var maxScrollX = documentWidth - viewportWidth;
var maxScrollY = documentHeight - viewportHeight;
(function checkForWindowScroll() {
clearTimeout(timer);
if (
adjustWindowScroll({
maxScrollX,
maxScrollY,
edgeBottom,
$page,
edgeTop,
edgeSize,
isInBottomEdge,
isInTopEdge,
viewportY: clientY,
})
) {
timer = setTimeout(checkForWindowScroll, 30);
}
})();
} else {
clearTimeout(timer);
}
if (curPosition === 'before') {
belowNode = curDraggable;
} else {
aboveNode = curDraggable;
}
// if belowNode has the depth of 1, then the above element will be a part of a different branch
const { relationships, nodes } = outline.current;
if (!belowNode || !aboveNode) {
if (belowNode) {
aboveNode = findNodeAbove(outline.current, curDepth, belowNode);
} else if (aboveNode) {
let targetBelowNode: RelationshipChild | null = null;
const parent = relationships.get(aboveNode.parent);
if (aboveNode.children !== 0 && !aboveNode.collapsed) {
const abr = relationships.get(aboveNode.id);
if (abr) {
const newTarget = abr.children[0];
if (newTarget) {
targetBelowNode = newTarget;
}
}
} else if (parent) {
const aboveNodeIndex = parent.children.findIndex(c => aboveNode && c.id === aboveNode.id);
if (aboveNodeIndex !== -1) {
if (aboveNodeIndex === parent.children.length - 1) {
targetBelowNode = getBelowParent(aboveNode, outline.current);
} else {
const nextChild = parent.children[aboveNodeIndex + 1];
targetBelowNode = nextChild ?? null;
}
}
}
if (targetBelowNode) {
const depthNodes = nodes.get(targetBelowNode.depth);
if (depthNodes) {
belowNode = depthNodes.get(targetBelowNode.id) ?? null;
}
}
}
}
// if outside outline, get either first or last item in list based on mouse Y
if (!aboveNode && !belowNode) {
if (container && container.current) {
const bounds = container.current.getBoundingClientRect();
if (clientY < bounds.top + bounds.height / 2) {
const rootChildren = outline.current.relationships.get('root');
const rootDepth = outline.current.nodes.get(1);
if (rootChildren && rootDepth) {
const firstChild = rootChildren.children[0];
belowNode = rootDepth.get(firstChild.id) ?? null;
aboveNode = null;
}
} else {
// TODO: enhance to actually get last child item, not last top level branch
const rootChildren = outline.current.relationships.get('root');
const rootDepth = outline.current.nodes.get(1);
if (rootChildren && rootDepth) {
const lastChild = rootChildren.children[rootChildren.children.length - 1];
const lastParentNode = rootDepth.get(lastChild.id) ?? null;
if (lastParentNode) {
const lastBranchChild = getLastChildInBranch(outline.current, lastParentNode);
if (lastBranchChild) {
const lastChildDepth = outline.current.nodes.get(lastBranchChild.depth);
if (lastChildDepth) {
aboveNode = lastChildDepth.get(lastBranchChild.id) ?? null;
}
}
}
}
}
}
}
if (aboveNode) {
const foundDepth = findNodeDepth(outline.current.published, aboveNode.id);
if (foundDepth === null) return;
for (let i = 0; i < draggedNodes.nodes.length; i++) {
const nodeID = draggedNodes.nodes[i];
if (foundDepth.ancestors.find(c => c === nodeID)) {
if (draggedNodes.first) {
belowNode = draggedNodes.first;
aboveNode = findNodeAbove(outline.current, aboveNode ? aboveNode.depth : 1, draggedNodes.first);
} else {
const foundDepth = findNodeDepth(outline.current.published, nodeID);
if (foundDepth === null) return;
const nodeDepth = outline.current.nodes.get(foundDepth.depth);
const targetNode = nodeDepth ? nodeDepth.get(nodeID) : null;
if (targetNode) {
belowNode = targetNode;
aboveNode = findNodeAbove(outline.current, foundDepth.depth, targetNode);
}
}
}
}
}
// calculate available depths
let minDepth = 1;
let maxDepth = 2;
if (aboveNode) {
const aboveParent = relationships.get(aboveNode.parent);
if (aboveNode.children !== 0 && !aboveNode.collapsed) {
minDepth = aboveNode.depth + 1;
maxDepth = aboveNode.depth + 1;
} else if (aboveParent) {
minDepth = aboveNode.depth;
maxDepth = aboveNode.depth + 1;
const aboveNodeIndex = aboveParent.children.findIndex(c => aboveNode && c.id === aboveNode.id);
if (aboveNodeIndex === aboveParent.children.length - 1) {
minDepth = belowNode ? belowNode.depth : minDepth;
}
}
}
if (aboveNode) {
const dimensions = outline.current.dimensions.get(aboveNode.id);
const entry = getDimensions(dimensions?.entry);
if (entry) {
depthTarget = getTargetDepth(clientX, entry.left, { min: minDepth, max: maxDepth });
}
}
let aboveImpact: null | ImpactZoneData = null;
let belowImpact: null | ImpactZoneData = null;
if (aboveNode) {
const aboveDim = outline.current.dimensions.get(aboveNode.id);
if (aboveDim) {
aboveImpact = {
node: aboveNode,
dimensions: aboveDim,
};
}
}
if (belowNode) {
const belowDim = outline.current.dimensions.get(belowNode.id);
if (belowDim) {
belowImpact = {
node: belowNode,
dimensions: belowDim,
};
}
}
setImpact({
zone: {
above: aboveImpact,
below: belowImpact,
},
depth: depthTarget,
});
},
[outline.current.nodes],
);
useEffect(() => {
document.addEventListener('mouseup', handleMouseUp);
document.addEventListener('mousemove', handleMouseMove);
return () => {
document.removeEventListener('mouseup', handleMouseUp);
document.removeEventListener('mousemove', handleMouseMove);
};
}, []);
const styles = useMemo(() => {
const position: 'fixed' | 'relative' = isDragging ? 'fixed' : 'relative';
return {
cursor: isDragging ? '-webkit-grabbing' : '-webkit-grab',
transform: `translate(${pos.x - 10}px, ${pos.y - 4}px)`,
transition: isDragging ? 'none' : 'transform 500ms',
zIndex: isDragging ? 2 : 1,
position,
};
}, [isDragging, pos]);
return (
<>
{pos && (
<Container ref={$handle} style={styles}>
<Dot width={18} height={18} />
</Container>
)}
</>
);
};
export default Dragger;

View File

@ -0,0 +1,377 @@
import React, { useRef, useEffect, useCallback, useState } from 'react';
import { Dot, CaretDown, CaretRight } from 'shared/icons';
import _ from 'lodash';
import marked from 'marked';
import VisibilitySensor from 'react-visibility-sensor';
import {
EntryChildren,
EntryWrapper,
EntryContent,
EntryInnerContent,
EntryHandle,
ExpandButton,
EntryContentEditor,
EntryContentDisplay,
} from './Styles';
import { useDrag } from './useDrag';
import { getCaretPosition, setCurrentCursorPosition } from './utils';
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
type EditorProps = {
text: string;
initFocus: null | { caret: null | number };
autoFocus: number | null;
onChangeCurrentText: (text: string) => void;
onDeleteEntry: (caret: number) => void;
onBlur: () => void;
handleChangeText: (caret: number) => void;
onDepthChange: (delta: number) => void;
onCreateEntry: () => void;
onNodeFocused: () => void;
};
const Editor: React.FC<EditorProps> = ({
text,
onCreateEntry,
initFocus,
autoFocus,
onChangeCurrentText,
onDepthChange,
onDeleteEntry,
onNodeFocused,
handleChangeText,
onBlur,
}) => {
const $editor = useRef<HTMLInputElement>(null);
useOnOutsideClick($editor, true, () => onBlur(), null);
useEffect(() => {
if (autoFocus && $editor.current) {
$editor.current.focus();
$editor.current.setSelectionRange(autoFocus, autoFocus);
onNodeFocused();
}
}, [autoFocus]);
useEffect(() => {
if (initFocus && $editor.current) {
$editor.current.focus();
if (initFocus.caret) {
$editor.current.setSelectionRange(initFocus.caret ?? 0, initFocus.caret ?? 0);
}
onNodeFocused();
}
}, []);
return (
<EntryContentEditor
value={text}
ref={$editor}
onChange={e => {
onChangeCurrentText(e.currentTarget.value);
}}
onKeyDown={e => {
if (e.keyCode === 13) {
e.preventDefault();
// onCreateEntry(parentID, position * 2);
onCreateEntry();
return;
} else if (e.keyCode === 9) {
e.preventDefault();
onDepthChange(e.shiftKey ? -1 : 1);
} else if (e.keyCode === 8) {
const caretPos = e.currentTarget.selectionEnd;
if (caretPos === 0) {
// handleChangeText.flush();
// onDeleteEntry(depth, id, currentText, caretPos);
onDeleteEntry(caretPos);
e.preventDefault();
return;
}
} else if (e.key === 'z' && e.ctrlKey) {
e.preventDefault();
return;
}
handleChangeText(e.currentTarget.selectionEnd ?? 0);
// setCaretPos(e.currentTarget.selectionEnd ?? 0);
// handleChangeText();
}}
/>
);
};
type EntryProps = {
id: string;
collapsed?: boolean;
onToggleCollapse: (id: string, collapsed: boolean) => void;
parentID: string;
onStartDrag: (e: { id: string; clientX: number; clientY: number }) => void;
onStartSelect: (e: { id: string; depth: number }) => void;
isRoot?: boolean;
selection: null | Array<{ id: string }>;
draggedNodes: null | Array<string>;
onNodeFocused: (id: string) => void;
text: string;
entries: Array<ItemElement>;
onTextChange: (id: string, prex: string, next: string, caret: number) => void;
onCancelDrag: () => void;
autoFocus: null | { caret: null | number };
onCreateEntry: (parent: string, nextPositon: number) => void;
position: number;
chain?: Array<string>;
onHandleClick: (id: string) => void;
onDepthChange: (id: string, parent: string, position: number, depth: number, depthDelta: number) => void;
onDeleteEntry: (depth: number, id: string, text: string, caretPos: number) => void;
depth?: number;
};
const Entry: React.FC<EntryProps> = ({
id,
text,
parentID,
isRoot = false,
selection,
onToggleCollapse,
autoFocus,
onStartSelect,
onHandleClick,
onTextChange,
position,
onNodeFocused,
onDepthChange,
onCreateEntry,
onDeleteEntry,
onCancelDrag,
onStartDrag,
collapsed = false,
draggedNodes,
entries,
chain = [],
depth = 0,
}) => {
const $entry = useRef<HTMLDivElement>(null);
const $children = useRef<HTMLDivElement>(null);
const { setNodeDimensions, clearNodeDimensions } = useDrag();
if (autoFocus) {
}
const $snapshot = useRef<{ now: string; prev: string }>({ now: text, prev: text });
const [currentText, setCurrentText] = useState(text);
const [caretPos, setCaretPos] = useState(0);
const $firstRun = useRef<boolean>(true);
useEffect(() => {
if ($firstRun.current) {
$firstRun.current = false;
return;
}
console.log('updating text');
setCurrentText(text);
}, [text]);
const [editor, setEditor] = useState<{ open: boolean; caret: null | number }>({
open: false,
caret: null,
});
useEffect(() => {
if (autoFocus) setEditor({ open: true, caret: null });
}, [autoFocus]);
useEffect(() => {
$snapshot.current.now = currentText;
}, [currentText]);
const handleChangeText = useCallback(
_.debounce(() => {
onTextChange(id, $snapshot.current.prev, $snapshot.current.now, caretPos);
$snapshot.current.prev = $snapshot.current.now;
}, 500),
[],
);
const [visible, setVisible] = useState(false);
useEffect(() => {
if (isRoot) return;
if (!visible) {
clearNodeDimensions(id);
return;
}
if ($entry && $entry.current) {
setNodeDimensions(id, {
entry: $entry,
children: entries.length !== 0 ? $children : null,
});
}
return () => {
clearNodeDimensions(id);
};
}, [position, depth, entries, visible]);
let showHandle = true;
if (draggedNodes && draggedNodes.length === 1 && draggedNodes.find(c => c === id)) {
showHandle = false;
}
let isSelected = false;
if (selection && selection.find(c => c.id === id)) {
isSelected = true;
}
const renderMap: Array<number> = [];
const renderer = {
text(text: any) {
const localId = renderMap.length;
renderMap.push(text.length);
return `<span id="${id}_${localId}">${text}</span>`;
},
codespan(text: any) {
const localId = renderMap.length;
renderMap.push(text.length + 2);
return `<span class="markdown-code" id="${id}_${localId}">${text}</span>`;
},
strong(text: string) {
const idx = parseInt(text.split('"')[1].split('_')[1]);
renderMap[idx] += 4;
return text.replace('<span', '<span class="markdown-strong"');
},
em(text: string) {
const idx = parseInt(text.split('"')[1].split('_')[1]);
renderMap[idx] += 2;
return text.replace('<span', '<span class="markdown-em"');
},
del(text: string) {
const idx = parseInt(text.split('"')[1].split('_')[1]);
renderMap[idx] += 2;
return text.replace('<span', '<span class="markdown-del"');
},
};
// @ts-ignore
marked.use({ renderer });
const handleMouseDown = useCallback(
_.debounce((e: any) => {
onStartDrag({ id, clientX: e.clientX, clientY: e.clientY });
}, 100),
[],
);
return (
<VisibilitySensor
onChange={v => {
if (v) {
setVisible(v);
}
}}
>
<EntryWrapper isSelected={isSelected} isDragging={!showHandle}>
{!isRoot && (
<EntryContent>
{entries.length !== 0 && (
<ExpandButton onClick={() => onToggleCollapse(id, !collapsed)}>
{collapsed ? <CaretRight width={20} height={20} /> : <CaretDown width={20} height={20} />}
</ExpandButton>
)}
{showHandle && (
<EntryHandle
onMouseUp={() => {
handleMouseDown.cancel();
onHandleClick(id);
}}
onMouseDown={e => {
handleMouseDown(e);
}}
>
<Dot width={18} height={18} />
</EntryHandle>
)}
<EntryInnerContent
onMouseDown={() => {
onStartSelect({ id, depth });
}}
ref={$entry}
>
{editor.open ? (
<Editor
onDepthChange={delta => onDepthChange(id, parentID, depth, position, delta)}
onBlur={() => setEditor({ open: false, caret: null })}
onNodeFocused={() => onNodeFocused(id)}
autoFocus={autoFocus ? (autoFocus.caret ? autoFocus.caret : 0) : null}
initFocus={editor.open ? { caret: editor.caret } : null}
text={currentText}
onDeleteEntry={caret => {
handleChangeText.flush();
onDeleteEntry(depth, id, currentText, caret);
}}
onCreateEntry={() => {
onCreateEntry(parentID, position * 2);
}}
onChangeCurrentText={text => setCurrentText(text)}
handleChangeText={caret => {
setCaretPos(caret);
handleChangeText();
}}
/>
) : (
<EntryContentDisplay
onClick={e => {
let offset = 0;
let textNode: any;
if (document.caretPositionFromPoint) {
// standard
const range = document.caretPositionFromPoint(e.pageX, e.pageY);
console.dir(range);
if (range) {
textNode = range.offsetNode;
offset = range.offset;
}
} else if (document.caretRangeFromPoint) {
// WebKit
const range = document.caretRangeFromPoint(e.pageX, e.pageY);
if (range) {
textNode = range.startContainer;
offset = range.startOffset;
}
}
const id = textNode.parentNode.id.split('_');
const index = parseInt(id[1]);
let caret = offset;
for (let i = 0; i < index; i++) {
caret += renderMap[i];
}
setEditor({ open: true, caret });
}}
dangerouslySetInnerHTML={{ __html: marked.parseInline(text) }}
/>
)}
</EntryInnerContent>
</EntryContent>
)}
{entries.length !== 0 && !collapsed && (
<EntryChildren ref={$children} isRoot={isRoot}>
{entries
.sort((a, b) => a.position - b.position)
.map(entry => (
<Entry
onDeleteEntry={onDeleteEntry}
onHandleClick={onHandleClick}
onDepthChange={onDepthChange}
parentID={id}
key={entry.id}
onTextChange={onTextChange}
position={entry.position}
text={entry.text}
depth={depth + 1}
draggedNodes={draggedNodes}
collapsed={entry.collapsed}
id={entry.id}
autoFocus={entry.focus}
onNodeFocused={onNodeFocused}
onStartSelect={onStartSelect}
onStartDrag={onStartDrag}
onCancelDrag={onCancelDrag}
entries={entry.children ?? []}
chain={[...chain, id]}
selection={selection}
onToggleCollapse={onToggleCollapse}
onCreateEntry={onCreateEntry}
/>
))}
</EntryChildren>
)}
</EntryWrapper>
</VisibilitySensor>
);
};
export default Entry;

View File

@ -0,0 +1,260 @@
import styled, { css } from 'styled-components';
import { mixin } from 'shared/utils/styles';
export const EntryWrapper = styled.div<{ isDragging: boolean; isSelected: boolean }>`
position: relative;
${props =>
props.isDragging &&
css`
&:before {
border-radius: 3px;
content: '';
position: absolute;
top: 2px;
right: -5px;
left: -5px;
bottom: -2px;
background-color: #eceef0;
}
`}
${props =>
props.isSelected &&
css`
&:before {
border-radius: 3px;
content: '';
position: absolute;
top: 2px;
right: -5px;
bottom: -2px;
left: -5px;
background-color: ${mixin.rgba(props.theme.colors.primary, 0.75)};
}
`}
`;
export const EntryChildren = styled.div<{ isRoot: boolean }>`
position: relative;
${props =>
!props.isRoot &&
css`
margin-left: 10px;
padding-left: 25px;
border-left: 1px solid ${mixin.rgba(props.theme.colors.text.primary, 0.6)};
`}
`;
export const PageContent = styled.div`
min-height: calc(100vh - 146px);
width: 100%;
position: relative;
display: flex;
flex-direction: column;
box-shadow: none;
user-select: none;
margin-left: auto;
margin-right: auto;
max-width: 700px;
padding-left: 56px;
padding-right: 56px;
padding-top: 24px;
padding-bottom: 24px;
text-size-adjust: none;
`;
export const DragHandle = styled.div<{ top: number; left: number }>`
display: flex;
align-items: center;
justify-content: center;
position: fixed;
left: 0;
top: 0;
transform: translate3d(${props => props.left}px, ${props => props.top}px, 0);
transition: transform 0.2s cubic-bezier(0.2, 0, 0, 1);
width: 18px;
height: 18px;
color: rgb(75, 81, 85);
border-radius: 9px;
`;
export const RootWrapper = styled.div``;
export const EntryHandle = styled.div`
display: flex;
align-items: center;
justify-content: center;
position: absolute;
left: 501px;
top: 7px;
width: 18px;
height: 18px;
color: ${p => p.theme.colors.text.primary};
border-radius: 9px;
&:hover {
background: ${p => p.theme.colors.primary};
}
svg {
fill: ${p => p.theme.colors.text.primary};
stroke: ${p => p.theme.colors.text.primary};
}
`;
export const EntryContentDisplay = styled.div`
display: inline-flex;
align-items: center;
width: 100%;
font-size: 15px;
white-space: pre-wrap;
background: none;
outline: none;
border: none;
line-height: 24px;
min-height: 24px;
overflow-wrap: break-word;
position: relative;
padding: 0;
margin: 0;
color: ${p => p.theme.colors.text.primary};
user-select: none;
cursor: text;
.markdown-del {
text-decoration: line-through;
}
.markdown-code {
margin-top: -4px;
font-size: 16px;
line-height: 19px;
color: ${props => props.theme.colors.primary};
font-family: monospace;
padding: 4px 5px 0;
font-family: 'Consolas', Courier, monospace;
background: ${props => props.theme.colors.bg.primary};
display: inline-block;
vertical-align: middle;
border-radius: 4px;
}
.markdown-em {
margin-top: -4px;
font-style: italic;
}
.markdown-strong {
font-weight: 700;
color: #fff;
}
&:focus {
outline: 0;
}
`;
export const EntryContentEditor = styled.input`
width: 100%;
font-size: 15px;
padding: 0;
margin: 0;
white-space: pre-wrap;
background: none;
outline: none;
border: none;
line-height: 24px;
min-height: 24px;
overflow-wrap: break-word;
position: relative;
user-select: text;
color: ${p => p.theme.colors.text.primary};
&::selection {
background: #a49de8;
}
&:focus {
outline: 0;
}
`;
export const EntryInnerContent = styled.div`
padding-top: 4px;
font-size: 15px;
white-space: pre-wrap;
background: none;
outline: none;
border: none;
line-height: 24px;
min-height: 24px;
overflow-wrap: break-word;
position: relative;
user-select: text;
color: ${p => p.theme.colors.text.primary};
&::selection {
background: #a49de8;
}
&:focus {
outline: 0;
}
`;
export const DragDebugWrapper = styled.div`
position: absolute;
left: 42px;
bottom: 24px;
color: #fff;
`;
export const DragIndicatorBar = styled.div<{ left: number; top: number; width: number }>`
position: fixed;
width: ${props => props.width}px;
top: ${props => props.top}px;
left: ${props => props.left}px;
height: 4px;
border-radius: 3px;
background: rgb(204, 204, 204);
`;
export const ExpandButton = styled.div`
top: 6px;
cursor: default;
color: transparent;
position: absolute;
top: 6px;
display: flex;
align-items: center;
justify-content: center;
left: 478px;
width: 20px;
height: 20px;
svg {
fill: transparent;
}
`;
export const EntryContent = styled.div`
position: relative;
margin-left: -500px;
padding-left: 524px;
&:hover ${ExpandButton} svg {
fill: ${props => props.theme.colors.text.primary};
}
`;
export const PageContainer = styled.div`
overflow-y: auto;
overflow-x: hidden;
`;
export const PageName = styled.div`
position: relative;
margin-left: -100px;
padding-left: 100px;
margin-bottom: 10px;
border-color: rgb(170, 170, 170);
font-size: 26px;
font-weight: bold;
color: #fff;
`;
export const PageNameContent = styled.div`
white-space: pre-wrap;
line-height: 34px;
min-height: 34px;
overflow-wrap: break-word;
position: relative;
user-select: text;
`;
export const PageNameText = styled.span``;

View File

@ -0,0 +1,784 @@
import React, { useState, useRef, useEffect, useMemo, useCallback, useContext, memo, createRef } from 'react';
import { DotCircle } from 'shared/icons';
import styled from 'styled-components/macro';
import GlobalTopNavbar from 'App/TopNavbar';
import _ from 'lodash';
import produce from 'immer';
import Entry from './Entry';
import DragIndicator from './DragIndicator';
import Dragger from './Dragger';
import DragDebug from './DragDebug';
import { DragContext } from './useDrag';
import {
PageContainer,
DragDebugWrapper,
DragIndicatorBar,
PageContent,
EntryChildren,
EntryInnerContent,
EntryWrapper,
EntryContent,
RootWrapper,
EntryHandle,
PageNameContent,
PageNameText,
PageName,
} from './Styles';
import {
transformToTree,
findNode,
findNodeDepth,
getNumberOfChildren,
validateDepth,
getDimensions,
findNextDraggable,
getNodeOver,
getCorrectNode,
findCommonParent,
getNodeAbove,
findNodeAbove,
} from './utils';
import NOOP from 'shared/utils/noop';
enum CommandType {
MOVE,
MERGE,
CHANGE_TEXT,
DELETE,
CREATE,
}
type MoveData = {
prev: { position: number; parent: string | null };
next: { position: number; parent: string | null };
};
type ChangeTextData = {
node: {
id: string;
parentID: string;
position: number;
};
caret: number;
prev: string;
next: string;
};
type DeleteData = {
node: {
id: string;
parentID: string;
position: number;
text: string;
};
};
type OutlineCommand = {
nodes: Array<{
id: string;
type: CommandType;
data: MoveData | DeleteData | ChangeTextData;
}>;
};
type ItemCollapsed = {
id: string;
collapsed: boolean;
};
function generateItems(c: number) {
const items: Array<ItemElement> = [];
for (let i = 0; i < c; i++) {
items.push({
collapsed: false,
focus: null,
id: `entry-gen-${i}`,
text: `entry-gen-${i}`,
parent: 'root',
position: 4096 * (6 + i),
});
}
return items;
}
const listItems: Array<ItemElement> = [
{ id: 'root', text: '', position: 4096, parent: null, collapsed: false, focus: null },
{ id: 'entry-1', text: 'entry-1', position: 4096, parent: 'root', collapsed: false, focus: null },
{ id: 'entry-1-3', text: 'entry-1-3', position: 4096 * 3, parent: 'entry-1', collapsed: false, focus: null },
{ id: 'entry-1-3-1', text: 'entry-1-3-1', position: 4096, parent: 'entry-1-3', collapsed: false, focus: null },
{ id: 'entry-1-3-2', text: 'entry-1-3-2', position: 4096 * 2, parent: 'entry-1-3', collapsed: false, focus: null },
{ id: 'entry-1-3-3', text: 'entry-1-3-3', position: 4096 * 3, parent: 'entry-1-3', collapsed: false, focus: null },
{
id: 'entry-1-3-3-1',
text: '*Hello!* I am `doing super` well ~how~ are **you**?',
position: 4096 * 1,
parent: 'entry-1-3-3',
collapsed: false,
focus: null,
},
{
id: 'entry-1-3-3-1-1',
text: 'entry-1-3-3-1-1',
position: 4096 * 1,
parent: 'entry-1-3-3-1',
collapsed: false,
focus: null,
},
{ id: 'entry-2', text: 'entry-2', position: 4096 * 2, parent: 'root', collapsed: false, focus: null },
{ id: 'entry-3', text: 'entry-3', position: 4096 * 3, parent: 'root', collapsed: false, focus: null },
{ id: 'entry-4', text: 'entry-4', position: 4096 * 4, parent: 'root', collapsed: false, focus: null },
{ id: 'entry-5', text: 'entry-5', position: 4096 * 5, parent: 'root', collapsed: false, focus: null },
...generateItems(100),
];
const Outline: React.FC = () => {
const [items, setItems] = useState(listItems);
const [selecting, setSelecting] = useState<{
isSelecting: boolean;
node: { id: string; depth: number } | null;
}>({ isSelecting: false, node: null });
const [selection, setSelection] = useState<null | { nodes: Array<{ id: string }>; first?: OutlineNode | null }>(null);
const [dragging, setDragging] = useState<{
show: boolean;
draggedNodes: null | Array<string>;
initialPos: { x: number; y: number };
}>({ show: false, draggedNodes: null, initialPos: { x: 0, y: 0 } });
const [impact, setImpact] = useState<null | {
listPosition: number;
zone: ImpactZone;
depthTarget: number;
}>(null);
const selectRef = useRef<{ isSelecting: boolean; hasSelection: boolean; node: { id: string; depth: number } | null }>(
{
isSelecting: false,
node: null,
hasSelection: false,
},
);
const impactRef = useRef<null | { listPosition: number; depth: number; zone: ImpactZone }>(null);
useEffect(() => {
if (impact) {
impactRef.current = { zone: impact.zone, depth: impact.depthTarget, listPosition: impact.listPosition };
}
}, [impact]);
useEffect(() => {
selectRef.current.isSelecting = selecting.isSelecting;
selectRef.current.node = selecting.node;
}, [selecting]);
const $content = useRef<HTMLDivElement>(null);
const outline = useRef<OutlineData>({
published: new Map<string, string>(),
dimensions: new Map<string, NodeDimensions>(),
nodes: new Map<number, Map<string, OutlineNode>>(),
relationships: new Map<string, NodeRelationships>(),
});
const tree = transformToTree(_.cloneDeep(items));
let root: any = null;
if (tree.length === 1) {
root = tree[0];
}
const outlineHistory = useRef<{ commands: Array<OutlineCommand>; current: number }>({ current: -1, commands: [] });
useEffect(() => {
outline.current.relationships = new Map<string, NodeRelationships>();
outline.current.published = new Map<string, string>();
outline.current.nodes = new Map<number, Map<string, OutlineNode>>();
const collapsedMap = items.reduce((map, next) => {
if (next.collapsed) {
map.set(next.id, true);
}
return map;
}, new Map<string, boolean>());
items.forEach(item => outline.current.published.set(item.id, item.parent ?? 'root'));
for (let i = 0; i < items.length; i++) {
const { collapsed, position, id, parent: curParent } = items[i];
if (id === 'root') {
continue;
}
const parent = curParent ?? 'root';
outline.current.published.set(id, parent ?? 'root');
const foundDepth = findNodeDepth(outline.current.published, id);
if (foundDepth === null) {
continue;
}
const { depth, ancestors } = foundDepth;
const collapsedParent = ancestors.slice(0, -1).find(a => collapsedMap.get(a));
if (collapsedParent) {
continue;
}
const children = getNumberOfChildren(root, ancestors);
if (!outline.current.nodes.has(depth)) {
outline.current.nodes.set(depth, new Map<string, OutlineNode>());
}
const targetDepthNodes = outline.current.nodes.get(depth);
if (targetDepthNodes) {
targetDepthNodes.set(id, {
id,
children,
position,
depth,
ancestors,
collapsed,
parent,
});
}
if (!outline.current.relationships.has(parent)) {
outline.current.relationships.set(parent, {
self: {
depth: depth - 1,
id: parent,
},
children: [],
numberOfSubChildren: 0,
});
}
const nodeRelations = outline.current.relationships.get(parent);
if (nodeRelations) {
outline.current.relationships.set(parent, {
self: nodeRelations.self,
numberOfSubChildren: nodeRelations.numberOfSubChildren + children,
children: [...nodeRelations.children, { id, position, depth, children }].sort(
(a, b) => a.position - b.position,
),
});
}
}
}, [items]);
const handleKeyDown = useCallback(e => {
if (e.code === 'KeyZ' && e.ctrlKey) {
const currentCommand = outlineHistory.current.commands[outlineHistory.current.current];
if (currentCommand) {
setItems(prevItems =>
produce(prevItems, draftItems => {
currentCommand.nodes.forEach(node => {
const idx = prevItems.findIndex(c => c.id === node.id);
if (node.type === CommandType.MOVE) {
if (idx === -1) return;
const data = node.data as MoveData;
draftItems[idx].parent = data.prev.parent;
draftItems[idx].position = data.prev.position;
} else if (node.type === CommandType.CHANGE_TEXT) {
if (idx === -1) return;
const data = node.data as ChangeTextData;
draftItems[idx] = produce(prevItems[idx], draftItem => {
draftItem.text = data.prev;
draftItem.focus = { caret: data.caret };
});
} else if (node.type === CommandType.DELETE) {
const data = node.data as DeleteData;
draftItems.push({
id: data.node.id,
position: data.node.position,
parent: data.node.parentID,
text: '',
focus: { caret: null },
children: [],
collapsed: false,
});
}
});
outlineHistory.current.current--;
}),
);
}
} else if (e.code === 'KeyY' && e.ctrlKey) {
const currentCommand = outlineHistory.current.commands[outlineHistory.current.current + 1];
if (currentCommand) {
setItems(prevItems =>
produce(prevItems, draftItems => {
currentCommand.nodes.forEach(node => {
const idx = prevItems.findIndex(c => c.id === node.id);
if (idx !== -1) {
if (node.type === CommandType.MOVE) {
const data = node.data as MoveData;
draftItems[idx].parent = data.next.parent;
draftItems[idx].position = data.next.position;
}
}
});
outlineHistory.current.current++;
}),
);
}
}
}, []);
const handleMouseUp = useCallback(
e => {
if (selectRef.current.hasSelection && !selectRef.current.isSelecting) {
setSelection(null);
}
if (selectRef.current.isSelecting) {
setSelecting({ isSelecting: false, node: null });
}
},
[dragging, selecting],
);
const handleMouseMove = useCallback(e => {
if (selectRef.current.isSelecting && selectRef.current.node) {
const { clientX, clientY } = e;
const dimensions = outline.current.dimensions.get(selectRef.current.node.id);
if (dimensions) {
const entry = getDimensions(dimensions.entry);
if (entry) {
const isAbove = clientY < entry.top;
const isBelow = clientY > entry.bottom;
if (!isAbove && !isBelow && selectRef.current.hasSelection) {
const nodeDepth = outline.current.nodes.get(selectRef.current.node.depth);
const aboveNode = nodeDepth ? nodeDepth.get(selectRef.current.node.id) : null;
if (aboveNode) {
setSelection({ nodes: [{ id: selectRef.current.node.id }], first: aboveNode });
selectRef.current.hasSelection = false;
}
}
if (isAbove || isBelow) {
e.preventDefault();
const { curDraggable } = getNodeOver({ x: clientX, y: clientY }, outline.current);
const nodeDepth = outline.current.nodes.get(selectRef.current.node.depth);
const selectedNode = nodeDepth ? nodeDepth.get(selectRef.current.node.id) : null;
let aboveNode: OutlineNode | undefined | null = null;
let belowNode: OutlineNode | undefined | null = null;
if (isBelow) {
aboveNode = selectedNode;
belowNode = curDraggable;
} else {
aboveNode = curDraggable;
belowNode = selectedNode;
}
if (aboveNode && belowNode) {
const aboveDim = outline.current.dimensions.get(aboveNode.id);
const belowDim = outline.current.dimensions.get(belowNode.id);
if (aboveDim && belowDim) {
const aboveDimBounds = getDimensions(aboveDim.entry);
const belowDimBounds = getDimensions(belowDim.children ? belowDim.children : belowDim.entry);
const aboveDimY = aboveDimBounds ? aboveDimBounds.bottom : 0;
const belowDimY = belowDimBounds ? belowDimBounds.top : 0;
const inbetweenNodes: Array<{ id: string }> = [];
for (const [id, dimension] of outline.current.dimensions.entries()) {
if (id === aboveNode.id || id === belowNode.id) {
inbetweenNodes.push({ id });
continue;
}
const targetNodeBounds = getDimensions(dimension.entry);
if (targetNodeBounds) {
if (
Math.round(aboveDimY) <= Math.round(targetNodeBounds.top) &&
Math.round(belowDimY) >= Math.round(targetNodeBounds.bottom)
) {
inbetweenNodes.push({ id });
}
}
}
const filteredNodes = inbetweenNodes.filter(n => {
const parent = outline.current.published.get(n.id);
if (parent) {
const foundParent = inbetweenNodes.find(c => c.id === parent);
if (foundParent) {
return false;
}
}
return true;
});
selectRef.current.hasSelection = true;
setSelection({ nodes: filteredNodes, first: aboveNode });
}
}
}
}
}
}
}, []);
useEffect(() => {
document.addEventListener('mouseup', handleMouseUp);
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('mouseup', handleMouseUp);
document.removeEventListener('mousemove', handleMouseMove);
document.addEventListener('keydown', handleKeyDown);
};
}, []);
const $page = useRef<HTMLDivElement>(null);
const $pageName = useRef<HTMLDivElement>(null);
if (!root) {
return null;
}
return (
<>
<GlobalTopNavbar onSaveProjectName={NOOP} projectID={null} name={null} />
<DragContext.Provider
value={{
outline,
impact,
setImpact: data => {
if (data) {
const { zone, depth } = data;
let listPosition = 65535;
if (zone.above && zone.above.node.depth + 1 <= depth && zone.above.node.collapsed) {
const aboveChildren = items
.filter(i => (zone.above ? i.parent === zone.above.node.id : false))
.sort((a, b) => a.position - b.position);
const lastChild = aboveChildren[aboveChildren.length - 1];
if (lastChild) {
listPosition = lastChild.position * 2.0;
}
} else {
const correctNode = getCorrectNode(outline.current, zone.above ? zone.above.node : null, depth);
const listAbove = validateDepth(correctNode, depth);
const listBelow = validateDepth(zone.below ? zone.below.node : null, depth);
if (listAbove && listBelow) {
listPosition = (listAbove.position + listBelow.position) / 2.0;
} else if (listAbove && !listBelow) {
listPosition = listAbove.position * 2.0;
} else if (!listAbove && listBelow) {
listPosition = listBelow.position / 2.0;
}
}
if (!zone.above && zone.below) {
const newPosition = zone.below.node.position / 2.0;
setImpact(() => ({
zone,
listPosition: newPosition,
depthTarget: depth,
}));
}
if (zone.above) {
// console.log(`prev=${prev} next=${next} targetPosition=${targetPosition}`);
// let targetID = depthTarget === 1 ? 'root' : node.ancestors[depthTarget - 1];
// targetID = targetID ?? node.id;
setImpact(() => ({
zone,
listPosition,
depthTarget: depth,
}));
}
} else {
setImpact(null);
}
},
setNodeDimensions: (nodeID, ref) => {
outline.current.dimensions.set(nodeID, ref);
},
clearNodeDimensions: nodeID => {
outline.current.dimensions.delete(nodeID);
},
}}
>
<>
<PageContainer ref={$page}>
<PageContent>
<RootWrapper ref={$content}>
<PageName>
<PageNameContent ref={$pageName}>
<PageNameText>entry-1-3-1</PageNameText>
</PageNameContent>
</PageName>
<Entry
onDepthChange={(id, parentID, position, depth, depthDelta) => {
if (depthDelta === -1) {
const parentRelation = outline.current.relationships.get(parentID);
if (parentRelation) {
const nodeIdx = parentRelation.children
.sort((a, b) => a.position - b.position)
.findIndex(c => c.id === id);
if (parentRelation.children.length !== 0) {
const grandparent = outline.current.published.get(parentID);
if (grandparent) {
const grandparentNode = outline.current.relationships.get(grandparent);
if (grandparentNode) {
const parents = grandparentNode.children.sort((a, b) => a.position - b.position);
const parentIdx = parents.findIndex(c => c.id === parentID);
if (parentIdx === -1) return;
let position = parents[parentIdx].position * 2;
const nextParent = parents[parentIdx + 1];
if (nextParent) {
position = (parents[parentIdx].position + nextParent.position) / 2.0;
}
setItems(prevItems =>
produce(prevItems, draftItems => {
const idx = prevItems.findIndex(c => c.id === id);
draftItems[idx] = produce(prevItems[idx], draftItem => {
draftItem.parent = grandparent;
draftItem.position = position;
draftItem.focus = { caret: 0 };
});
}),
);
}
}
}
}
} else {
const parent = outline.current.relationships.get(parentID);
if (parent) {
const nodeIdx = parent.children
.sort((a, b) => a.position - b.position)
.findIndex(c => c.id === id);
const aboveNode = parent.children[nodeIdx - 1];
if (aboveNode) {
const aboveNodeRelations = outline.current.relationships.get(aboveNode.id);
let position = 65535;
if (aboveNodeRelations) {
const children = aboveNodeRelations.children.sort((a, b) => a.position - b.position);
if (children.length !== 0) {
position = children[children.length - 1].position * 2;
}
}
setItems(prevItems =>
produce(prevItems, draftItems => {
const idx = prevItems.findIndex(c => c.id === id);
draftItems[idx] = produce(prevItems[idx], draftItem => {
draftItem.parent = aboveNode.id;
draftItem.position = position;
draftItem.focus = { caret: 0 };
});
}),
);
}
}
}
}}
onTextChange={(id, prev, next, caret) => {
outlineHistory.current.current += 1;
const data: ChangeTextData = {
node: {
id,
position: 0,
parentID: '',
},
caret,
prev,
next,
};
const command: OutlineCommand = {
nodes: [
{
id,
type: CommandType.CHANGE_TEXT,
data,
},
],
};
outlineHistory.current.commands[outlineHistory.current.current] = command;
if (outlineHistory.current.commands[outlineHistory.current.current + 1]) {
outlineHistory.current.commands.splice(outlineHistory.current.current + 1);
}
setItems(prevItems =>
produce(prevItems, draftItems => {
const idx = prevItems.findIndex(c => c.id === id);
if (idx !== -1) {
draftItems[idx] = produce(prevItems[idx], draftItem => {
draftItem.text = next;
});
}
}),
);
}}
text=""
autoFocus={null}
onDeleteEntry={(depth, id, text, caretPos) => {
const nodeDepth = outline.current.nodes.get(depth);
if (nodeDepth) {
const node = nodeDepth.get(id);
if (node) {
const nodeAbove = findNodeAbove(outline.current, depth, node);
setItems(prevItems => {
return produce(prevItems, draftItems => {
draftItems = prevItems.filter(c => c.id !== id);
const idx = prevItems.findIndex(c => c.id === nodeAbove?.id);
if (idx !== -1) {
draftItems[idx] = produce(prevItems[idx], draftItem => {
draftItem.focus = { caret: draftItem.text.length };
const cType = CommandType.DELETE;
const data: DeleteData = {
node: {
id,
position: node.position,
parentID: node.parent,
text: '',
},
};
if (text !== '') {
draftItem.text += text;
}
const command: OutlineCommand = {
nodes: [
{
id,
type: cType,
data,
},
],
};
outlineHistory.current.current += 1;
outlineHistory.current.commands[outlineHistory.current.current] = command;
if (outlineHistory.current.commands[outlineHistory.current.current + 1]) {
outlineHistory.current.commands.splice(outlineHistory.current.current + 1);
}
});
}
return draftItems;
});
});
}
}
}}
onCreateEntry={(parent, position) => {
setItems(prevItems =>
produce(prevItems, draftItems => {
draftItems.push({
id: '' + Math.random(),
collapsed: false,
position,
text: '',
focus: {
caret: null,
},
parent,
children: [],
});
}),
);
}}
onNodeFocused={id => {
setItems(prevItems =>
produce(prevItems, draftItems => {
const idx = draftItems.findIndex(c => c.id === id);
draftItems[idx] = produce(draftItems[idx], draftItem => {
draftItem.focus = null;
});
}),
);
}}
onStartSelect={({ id, depth }) => {
setSelection(null);
setSelecting({ isSelecting: true, node: { id, depth } });
}}
onToggleCollapse={(id, collapsed) => {
setItems(prevItems =>
produce(prevItems, draftItems => {
const idx = prevItems.findIndex(c => c.id === id);
if (idx !== -1) {
draftItems[idx].collapsed = collapsed;
}
}),
);
}}
id="root"
parentID="root"
isRoot
selection={selection ? selection.nodes : null}
draggedNodes={dragging.draggedNodes}
position={root.position}
entries={root.children}
onCancelDrag={() => {
setImpact(null);
setDragging({ show: false, draggedNodes: null, initialPos: { x: 0, y: 0 } });
}}
onHandleClick={id => {}}
onStartDrag={e => {
if (e.id !== 'root') {
if (selectRef.current.hasSelection && selection && selection.nodes.find(c => c.id === e.id)) {
setImpact(null);
setDragging({
show: true,
draggedNodes: [...selection.nodes.map(c => c.id)],
initialPos: { x: e.clientX, y: e.clientY },
});
} else {
setImpact(null);
setDragging({ show: true, draggedNodes: [e.id], initialPos: { x: e.clientX, y: e.clientY } });
}
}
}}
/>
</RootWrapper>
</PageContent>
</PageContainer>
{dragging.show && dragging.draggedNodes && (
<Dragger
container={$content}
initialPos={dragging.initialPos}
pageRef={$page}
draggedNodes={{ nodes: dragging.draggedNodes, first: selection ? selection.first : null }}
isDragging={dragging.show}
onDragEnd={() => {
if (dragging.draggedNodes && impactRef.current) {
const { zone, depth, listPosition } = impactRef.current;
const noZone = !zone.above && !zone.below;
if (!noZone) {
let parentID = 'root';
if (zone.above) {
parentID = zone.above.node.ancestors[depth - 1];
}
let reparent = true;
for (let i = 0; i < dragging.draggedNodes.length; i++) {
const draggedID = dragging.draggedNodes[i];
const prevItem = items.find(i => i.id === draggedID);
if (prevItem && prevItem.position === listPosition && prevItem.parent === parentID) {
reparent = false;
break;
}
}
// TODO: set reparent if list position changed but parent did not
//
if (reparent) {
// UPDATE OUTLINE DATA AFTER NODE MOVE
setItems(itemsPrev =>
produce(itemsPrev, draftItems => {
if (dragging.draggedNodes) {
const command: OutlineCommand = { nodes: [] };
outlineHistory.current.current += 1;
dragging.draggedNodes.forEach(n => {
const curDragging = itemsPrev.findIndex(i => i.id === n);
command.nodes.push({
id: n,
type: CommandType.MOVE,
data: {
prev: {
parent: draftItems[curDragging].parent,
position: draftItems[curDragging].position,
},
next: {
parent: parentID,
position: listPosition,
},
},
});
draftItems[curDragging].parent = parentID;
draftItems[curDragging].position = listPosition;
});
outlineHistory.current.commands[outlineHistory.current.current] = command;
if (outlineHistory.current.commands[outlineHistory.current.current + 1]) {
outlineHistory.current.commands.splice(outlineHistory.current.current + 1);
}
}
}),
);
}
}
}
setImpact(null);
setDragging({ show: false, draggedNodes: null, initialPos: { x: 0, y: 0 } });
}}
/>
)}
</>
</DragContext.Provider>
{impact && <DragIndicator depthTarget={impact.depthTarget} container={$content} zone={impact.zone} />}
{impact && (
<DragDebug zone={impact.zone ?? null} draggedNodes={dragging.draggedNodes} depthTarget={impact.depthTarget} />
)}
</>
);
};
export default Outline;

View File

@ -0,0 +1,22 @@
import React, { useContext } from 'react';
type DragContextData = {
impact: null | { zone: ImpactZone; depthTarget: number };
outline: React.MutableRefObject<OutlineData>;
setNodeDimensions: (
nodeID: string,
ref: { entry: React.RefObject<HTMLElement>; children: React.RefObject<HTMLElement> | null },
) => void;
clearNodeDimensions: (nodeID: string) => void;
setImpact: (data: ImpactData | null) => void;
};
export const DragContext = React.createContext<DragContextData | null>(null);
export const useDrag = () => {
const ctx = useContext(DragContext);
if (ctx) {
return ctx;
}
throw new Error('context is null');
};

View File

@ -0,0 +1,409 @@
import _ from 'lodash';
export function getCorrectNode(data: OutlineData, node: OutlineNode | null, depth: number) {
if (node) {
if (depth === node.depth) {
return node;
}
const parent = node.ancestors[depth];
if (parent) {
const parentNode = data.relationships.get(parent);
if (parentNode) {
const parentDepth = parentNode.self.depth;
const nodeDepth = data.nodes.get(parentDepth);
return nodeDepth ? nodeDepth.get(parent) : null;
}
}
}
return null;
}
export function validateDepth(node: OutlineNode | null | undefined, depth: number) {
if (node) {
return node.depth === depth ? node : null;
}
return null;
}
export function getNodeAbove(node: OutlineNode, startingParent: RelationshipChild, outline: OutlineData) {
let hasChildren = true;
let nodeAbove: null | RelationshipChild = null;
let aboveTargetID = startingParent.id;
while (hasChildren) {
const targetParent = outline.relationships.get(aboveTargetID);
if (targetParent) {
const parentNodes = outline.nodes.get(targetParent.self.depth);
const parentNode = parentNodes ? parentNodes.get(targetParent.self.id) : null;
if (targetParent.children.length === 0) {
if (parentNode) {
nodeAbove = {
id: parentNode.id,
depth: parentNode.depth,
position: parentNode.position,
children: parentNode.children,
};
}
hasChildren = false;
continue;
}
nodeAbove = targetParent.children[targetParent.children.length - 1];
if (targetParent.numberOfSubChildren === 0) {
hasChildren = false;
} else {
aboveTargetID = nodeAbove.id;
}
} else {
const target = outline.relationships.get(node.ancestors[0]);
if (target) {
const targetChild = target.children.find(i => i.id === aboveTargetID);
if (targetChild) {
nodeAbove = targetChild;
}
hasChildren = false;
}
}
}
return nodeAbove;
}
export function getBelowParent(node: OutlineNode, outline: OutlineData) {
const { relationships, nodes } = outline;
const parentDepth = nodes.get(node.depth - 1);
const parent = parentDepth ? parentDepth.get(node.parent) : null;
if (parent) {
const grandfather = relationships.get(parent.parent);
if (grandfather) {
const parentIndex = grandfather.children.findIndex(c => c.id === parent.id);
if (parentIndex !== -1) {
if (parentIndex === grandfather.children.length - 1) {
const root = relationships.get(node.ancestors[0]);
if (root) {
const ancestorIndex = root.children.findIndex(c => c.id === node.ancestors[1]);
if (ancestorIndex !== -1) {
const nextAncestor = root.children[ancestorIndex + 1];
if (nextAncestor) {
return nextAncestor;
}
}
}
} else {
const nextChild = grandfather.children[parentIndex + 1];
if (nextChild) {
return nextChild;
}
}
}
}
}
return null;
}
export function getDimensions(ref: React.RefObject<HTMLElement> | null | undefined) {
if (ref && ref.current) {
return ref.current.getBoundingClientRect();
}
return null;
}
export function getTargetDepth(mouseX: number, handleLeft: number, availableDepths: { min: number; max: number }) {
if (mouseX > handleLeft) {
return availableDepths.max;
}
let curDepth = availableDepths.max - 1;
for (let x = availableDepths.min; x < availableDepths.max; x++) {
const breakpoint = handleLeft - x * 35;
if (mouseX > breakpoint) {
return curDepth;
}
curDepth -= 1;
}
return availableDepths.min;
}
export function findNextDraggable(pos: { x: number; y: number }, outline: OutlineData, curDepth: number) {
let index = 0;
const currentDepthNodes = outline.nodes.get(curDepth);
let nodeAbove: null | RelationshipChild = null;
if (!currentDepthNodes) {
return null;
}
for (const [id, node] of currentDepthNodes) {
const dimensions = outline.dimensions.get(id);
const target = dimensions ? getDimensions(dimensions.entry) : null;
const children = dimensions ? getDimensions(dimensions.children) : null;
if (target) {
if (pos.y <= target.bottom && pos.y >= target.top) {
const middlePoint = target.top + target.height / 2;
const position: ImpactPosition = pos.y > middlePoint ? 'after' : 'before';
return {
found: true,
node,
position,
};
}
}
if (children) {
if (pos.y <= children.bottom && pos.y >= children.top) {
const position: ImpactPosition = 'after';
return { found: false, node, position };
}
}
index += 1;
}
return null;
}
export function transformToTree(arr: any) {
const nodes: any = {};
return arr.filter(function(obj: any) {
var id = obj['id'],
parentId = obj['parent'];
nodes[id] = _.defaults(obj, nodes[id], { children: [] });
parentId && (nodes[parentId] = nodes[parentId] || { children: [] })['children'].push(obj);
return !parentId;
});
}
export function findNode(parentID: string, nodeID: string, data: OutlineData) {
const nodeRelations = data.relationships.get(parentID);
if (nodeRelations) {
const nodeDepth = data.nodes.get(nodeRelations.self.depth + 1);
if (nodeDepth) {
const node = nodeDepth.get(nodeID);
return node ?? null;
}
}
return null;
}
export function findNodeDepth(published: Map<string, string>, id: string) {
let currentID = id;
let breaker = 0;
let depth = 0;
let ancestors = [id];
while (currentID !== 'root') {
const nextID = published.get(currentID);
if (nextID) {
ancestors = [nextID, ...ancestors];
currentID = nextID;
depth += 1;
breaker += 1;
if (breaker > 100) {
throw new Error('node depth breaker was thrown');
}
} else {
return null;
}
}
return { depth, ancestors };
}
export function getNumberOfChildren(root: ItemElement, ancestors: Array<string>) {
let currentBranch = root;
for (let i = 1; i < ancestors.length; i++) {
const nextBranch = currentBranch.children ? currentBranch.children.find(c => c.id === ancestors[i]) : null;
if (nextBranch) {
currentBranch = nextBranch;
} else {
throw new Error('unable to find next branch');
}
}
return currentBranch.children ? currentBranch.children.length : 0;
}
export function findNodeAbove(outline: OutlineData, curDepth: number, belowNode: OutlineNode) {
let targetAboveNode: null | RelationshipChild = null;
if (curDepth === 1) {
const relations = outline.relationships.get(belowNode.ancestors[0]);
if (relations) {
const parentIndex = relations.children.findIndex(n => belowNode && n.id === belowNode.ancestors[1]);
if (parentIndex !== -1) {
const aboveParent = relations.children[parentIndex - 1];
if (parentIndex === 0) {
targetAboveNode = null;
} else {
targetAboveNode = getNodeAbove(belowNode, aboveParent, outline);
}
}
}
} else {
const relations = outline.relationships.get(belowNode.parent);
if (relations) {
const currentIndex = relations.children.findIndex(n => belowNode && n.id === belowNode.id);
// is first child, so use parent
if (currentIndex === 0) {
const parentNodes = outline.nodes.get(belowNode.depth - 1);
const parentNode = parentNodes ? parentNodes.get(belowNode.parent) : null;
if (parentNode) {
targetAboveNode = {
id: belowNode.parent,
depth: belowNode.depth - 1,
position: parentNode.position,
children: parentNode.children,
};
}
} else if (currentIndex !== -1) {
// is not first child, so first prev sibling
const aboveParentNode = relations.children[currentIndex - 1];
if (aboveParentNode) {
targetAboveNode = getNodeAbove(belowNode, aboveParentNode, outline);
if (targetAboveNode === null) {
targetAboveNode = aboveParentNode;
}
}
}
}
}
if (targetAboveNode) {
const depthNodes = outline.nodes.get(targetAboveNode.depth);
if (depthNodes) {
return depthNodes.get(targetAboveNode.id) ?? null;
}
}
return null;
}
export function getNodeOver(mouse: { x: number; y: number }, outline: OutlineData) {
let curDepth = 1;
let curDraggables: any;
let curDraggable: any;
let curPosition: ImpactPosition = 'after';
while (outline.nodes.size + 1 > curDepth) {
curDraggables = outline.nodes.get(curDepth);
if (curDraggables) {
const nextDraggable = findNextDraggable(mouse, outline, curDepth);
if (nextDraggable) {
curDraggable = nextDraggable.node;
curPosition = nextDraggable.position;
if (nextDraggable.found) {
break;
}
curDepth += 1;
} else {
break;
}
}
}
return {
curDepth,
curDraggable,
curPosition,
};
}
export function findCommonParent(outline: OutlineData, aboveNode: OutlineNode, belowNode: OutlineNode) {
let aboveParentID = null;
let depth = 0;
for (let aIdx = aboveNode.ancestors.length - 1; aIdx !== 0; aIdx--) {
depth = aIdx;
const aboveNodeParent = aboveNode.ancestors[aIdx];
for (let bIdx = belowNode.ancestors.length - 1; bIdx !== 0; bIdx--) {
if (belowNode.ancestors[bIdx] === aboveNodeParent) {
aboveParentID = aboveNodeParent;
}
}
}
if (aboveParentID) {
const parent = outline.relationships.get(aboveParentID) ?? null;
if (parent) {
return {
parent,
depth,
};
}
return null;
}
return null;
}
export function getLastChildInBranch(outline: OutlineData, lastParentNode: OutlineNode) {
let curParentRelation = outline.relationships.get(lastParentNode.id);
if (!curParentRelation) {
return { id: lastParentNode.id, depth: 1 };
}
let hasChildren = lastParentNode.children !== 0;
let depth = 1;
let finalID: null | string = null;
while (hasChildren) {
if (curParentRelation) {
const lastChild = curParentRelation.children.sort((a, b) => a.position - b.position)[
curParentRelation.children.length - 1
];
depth += 1;
if (lastChild.children === 0) {
finalID = lastChild.id;
break;
}
curParentRelation = outline.relationships.get(lastChild.id);
} else {
hasChildren = false;
}
}
if (finalID !== null) {
return { id: finalID, depth };
}
return null;
}
export function getCaretPosition(editableDiv: any) {
/*
let caretPos = 0;
let sel: any = null;
let range: any = null;
if (window.getSelection) {
sel = window.getSelection();
if (sel && sel.rangeCount) {
range = sel.getRangeAt(0);
if (range.commonAncestorContainer.parentNode === editableDiv.current) {
caretPos = range.endOffset;
}
}
}
*/
return editableDiv.selectionEnd;
}
export function createRange(node: any, chars: any, range: any) {
if (!range) {
range = document.createRange();
range.selectNode(node);
range.setStart(node, 0);
}
if (chars.count === 0) {
range.setEnd(node, chars.count);
} else if (node && chars.count > 0) {
if (node.nodeType === Node.TEXT_NODE) {
if (node.textContent.length < chars.count) {
chars.count -= node.textContent.length;
} else {
range.setEnd(node, chars.count);
chars.count = 0;
}
} else {
for (var lp = 0; lp < node.childNodes.length; lp++) {
range = createRange(node.childNodes[lp], chars, range);
if (chars.count === 0) {
break;
}
}
}
}
return range;
}
export function setCurrentCursorPosition(element: any, chars: any) {
if (chars >= 0) {
const selection = window.getSelection();
const range = createRange(element, { count: chars }, false);
if (range && selection) {
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
}
}
}

View File

@ -5,7 +5,6 @@ import { TaskMetaFilters, TaskMeta, TaskMetaMatch, DueDateFilterType } from 'sha
import Input from 'shared/components/ControlledInput';
import { Popup, usePopup } from 'shared/components/PopupMenu';
import produce from 'immer';
import moment from 'moment';
import { mixin } from 'shared/utils/styles';
import Member from 'shared/components/Member';
@ -13,7 +12,7 @@ const FilterMember = styled(Member)`
margin: 2px 0;
&:hover {
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;
font-size: 14px;
&: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`
color: rgba(${props => props.theme.colors.text.primary}, 0.4);
color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.4)};
font-size: 12px;
padding-left: 4px;
padding-right: 4px;

View File

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

View File

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

View File

@ -136,14 +136,14 @@ const ProjectActionWrapper = styled.div<{ disabled?: boolean }>`
display: flex;
align-items: center;
font-size: 15px;
color: rgba(${props => props.theme.colors.text.primary});
color: ${props => props.theme.colors.text.primary};
&:not(:last-of-type) {
margin-right: 16px;
}
&:hover {
color: rgba(${props => props.theme.colors.text.secondary});
color: ${props => props.theme.colors.text.secondary};
}
${props =>
props.disabled &&
@ -280,7 +280,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
cache =>
produce(cache, draftCache => {
draftCache.findProject.taskGroups = draftCache.findProject.taskGroups.filter(
(taskGroup: TaskGroup) => taskGroup.id !== deletedTaskGroupData.data.deleteTaskGroup.taskGroup.id,
(taskGroup: TaskGroup) => taskGroup.id !== deletedTaskGroupData.data?.deleteTaskGroup.taskGroup.id,
);
}),
{ projectID },
@ -296,9 +296,11 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
cache =>
produce(cache, draftCache => {
const { taskGroups } = cache.findProject;
const idx = taskGroups.findIndex(taskGroup => taskGroup.id === newTaskData.data.createTask.taskGroup.id);
const idx = taskGroups.findIndex(taskGroup => taskGroup.id === newTaskData.data?.createTask.taskGroup.id);
if (idx !== -1) {
draftCache.findProject.taskGroups[idx].tasks.push({ ...newTaskData.data.createTask });
if (newTaskData.data) {
draftCache.findProject.taskGroups[idx].tasks.push({ ...newTaskData.data.createTask });
}
}
}),
{ projectID },
@ -313,7 +315,9 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
FindProjectDocument,
cache =>
produce(cache, draftCache => {
draftCache.findProject.taskGroups.push({ ...newTaskGroupData.data.createTaskGroup, tasks: [] });
if (newTaskGroupData.data) {
draftCache.findProject.taskGroups.push({ ...newTaskGroupData.data.createTaskGroup, tasks: [] });
}
}),
{ projectID },
);
@ -332,7 +336,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
cache =>
produce(cache, draftCache => {
const idx = cache.findProject.taskGroups.findIndex(
t => t.id === resp.data.deleteTaskGroupTasks.taskGroupID,
t => t.id === resp.data?.deleteTaskGroupTasks.taskGroupID,
);
if (idx !== -1) {
draftCache.findProject.taskGroups[idx].tasks = [];
@ -348,7 +352,9 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
FindProjectDocument,
cache =>
produce(cache, draftCache => {
draftCache.findProject.taskGroups.push(resp.data.duplicateTaskGroup.taskGroup);
if (resp.data) {
draftCache.findProject.taskGroups.push(resp.data.duplicateTaskGroup.taskGroup);
}
}),
{ projectID },
);
@ -364,19 +370,24 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
FindProjectDocument,
cache =>
produce(cache, draftCache => {
const { previousTaskGroupID, task } = newTask.data.updateTaskLocation;
if (previousTaskGroupID !== task.taskGroup.id) {
const { taskGroups } = cache.findProject;
const oldTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === previousTaskGroupID);
const newTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === task.taskGroup.id);
if (oldTaskGroupIdx !== -1 && newTaskGroupIdx !== -1) {
draftCache.findProject.taskGroups[oldTaskGroupIdx].tasks = taskGroups[oldTaskGroupIdx].tasks.filter(
(t: Task) => t.id !== task.id,
);
draftCache.findProject.taskGroups[newTaskGroupIdx].tasks = [
...taskGroups[newTaskGroupIdx].tasks,
{ ...task },
];
if (newTask.data) {
const { previousTaskGroupID, task } = newTask.data.updateTaskLocation;
if (previousTaskGroupID !== task.taskGroup.id) {
const { taskGroups } = cache.findProject;
const oldTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === previousTaskGroupID);
const newTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === task.taskGroup.id);
if (oldTaskGroupIdx !== -1 && newTaskGroupIdx !== -1) {
const previousTask = cache.findProject.taskGroups[oldTaskGroupIdx].tasks.find(t => t.id === task.id);
draftCache.findProject.taskGroups[oldTaskGroupIdx].tasks = taskGroups[oldTaskGroupIdx].tasks.filter(
(t: Task) => t.id !== task.id,
);
if (previousTask) {
draftCache.findProject.taskGroups[newTaskGroupIdx].tasks = [
...taskGroups[newTaskGroupIdx].tasks,
{ ...previousTask },
];
}
}
}
}
}),

View File

@ -138,21 +138,23 @@ const Details: React.FC<DetailsProps> = ({
FindTaskDocument,
cache =>
produce(cache, draftCache => {
const { prevChecklistID, checklistID, checklistItem } = response.data.updateTaskChecklistItemLocation;
if (checklistID !== prevChecklistID) {
const oldIdx = cache.findTask.checklists.findIndex(c => c.id === prevChecklistID);
const newIdx = cache.findTask.checklists.findIndex(c => c.id === checklistID);
if (oldIdx > -1 && newIdx > -1) {
const item = cache.findTask.checklists[oldIdx].items.find(i => i.id === checklistItem.id);
if (item) {
draftCache.findTask.checklists[oldIdx].items = cache.findTask.checklists[oldIdx].items.filter(
i => i.id !== checklistItem.id,
);
draftCache.findTask.checklists[newIdx].items.push({
...item,
position: checklistItem.position,
taskChecklistID: checklistID,
});
if (response.data) {
const { prevChecklistID, taskChecklistID, checklistItem } = response.data.updateTaskChecklistItemLocation;
if (taskChecklistID !== prevChecklistID) {
const oldIdx = cache.findTask.checklists.findIndex(c => c.id === prevChecklistID);
const newIdx = cache.findTask.checklists.findIndex(c => c.id === taskChecklistID);
if (oldIdx > -1 && newIdx > -1) {
const item = cache.findTask.checklists[oldIdx].items.find(i => i.id === checklistItem.id);
if (item) {
draftCache.findTask.checklists[oldIdx].items = cache.findTask.checklists[oldIdx].items.filter(
i => i.id !== checklistItem.id,
);
draftCache.findTask.checklists[newIdx].items.push({
...item,
position: checklistItem.position,
taskChecklistID: taskChecklistID,
});
}
}
}
}
@ -188,7 +190,7 @@ const Details: React.FC<DetailsProps> = ({
produce(cache, draftCache => {
const { checklists } = cache.findTask;
draftCache.findTask.checklists = checklists.filter(
c => c.id !== deleteData.data.deleteTaskChecklist.taskChecklist.id,
c => c.id !== deleteData.data?.deleteTaskChecklist.taskChecklist.id,
);
const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists);
draftCache.findTask.badges.checklist = {
@ -212,8 +214,10 @@ const Details: React.FC<DetailsProps> = ({
FindTaskDocument,
cache =>
produce(cache, draftCache => {
const item = createData.data.createTaskChecklist;
draftCache.findTask.checklists.push({ ...item });
if (createData.data) {
const item = createData.data.createTaskChecklist;
draftCache.findTask.checklists.push({ ...item });
}
}),
{ taskID },
);
@ -227,36 +231,14 @@ const Details: React.FC<DetailsProps> = ({
FindTaskDocument,
cache =>
produce(cache, draftCache => {
const item = deleteData.data.deleteTaskChecklistItem.taskChecklistItem;
const targetIdx = cache.findTask.checklists.findIndex(c => c.id === item.taskChecklistID);
if (targetIdx > -1) {
draftCache.findTask.checklists[targetIdx].items = cache.findTask.checklists[targetIdx].items.filter(
c => item.id !== c.id,
);
}
const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists);
draftCache.findTask.badges.checklist = {
__typename: 'ChecklistBadge',
complete,
total,
};
}),
{ taskID },
);
},
});
const [createTaskChecklistItem] = useCreateTaskChecklistItemMutation({
update: (client, newTaskItem) => {
updateApolloCache<FindTaskQuery>(
client,
FindTaskDocument,
cache =>
produce(cache, draftCache => {
const item = newTaskItem.data.createTaskChecklistItem;
const { checklists } = cache.findTask;
const idx = checklists.findIndex(c => c.id === item.taskChecklistID);
if (idx !== -1) {
draftCache.findTask.checklists[idx].items.push({ ...item });
if (deleteData.data) {
const item = deleteData.data.deleteTaskChecklistItem.taskChecklistItem;
const targetIdx = cache.findTask.checklists.findIndex(c => c.id === item.taskChecklistID);
if (targetIdx > -1) {
draftCache.findTask.checklists[targetIdx].items = cache.findTask.checklists[targetIdx].items.filter(
c => item.id !== c.id,
);
}
const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists);
draftCache.findTask.badges.checklist = {
__typename: 'ChecklistBadge',
@ -269,7 +251,33 @@ const Details: React.FC<DetailsProps> = ({
);
},
});
const { loading, data, refetch } = useFindTaskQuery({ variables: { taskID } });
const [createTaskChecklistItem] = useCreateTaskChecklistItemMutation({
update: (client, newTaskItem) => {
updateApolloCache<FindTaskQuery>(
client,
FindTaskDocument,
cache =>
produce(cache, draftCache => {
if (newTaskItem.data) {
const item = newTaskItem.data.createTaskChecklistItem;
const { checklists } = cache.findTask;
const idx = checklists.findIndex(c => c.id === item.taskChecklistID);
if (idx !== -1) {
draftCache.findTask.checklists[idx].items.push({ ...item });
const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists);
draftCache.findTask.badges.checklist = {
__typename: 'ChecklistBadge',
complete,
total,
};
}
}
}),
{ taskID },
);
},
});
const { loading, data, refetch } = useFindTaskQuery({ variables: { taskID }, fetchPolicy: 'cache-and-network' });
const [setTaskComplete] = useSetTaskCompleteMutation();
const [updateTaskDueDate] = useUpdateTaskDueDateMutation({
onCompleted: () => {
@ -309,7 +317,7 @@ const Details: React.FC<DetailsProps> = ({
task={data.findTask}
onChecklistDrop={checklist => {
updateTaskChecklistLocation({
variables: { checklistID: checklist.id, position: checklist.position },
variables: { taskChecklistID: checklist.id, position: checklist.position },
optimisticResponse: {
__typename: 'Mutation',
@ -324,20 +332,24 @@ const Details: React.FC<DetailsProps> = ({
},
});
}}
onChecklistItemDrop={(prevChecklistID, checklistID, checklistItem) => {
onChecklistItemDrop={(prevChecklistID, taskChecklistID, checklistItem) => {
updateTaskChecklistItemLocation({
variables: { checklistID, checklistItemID: checklistItem.id, position: checklistItem.position },
variables: {
taskChecklistID,
taskChecklistItemID: checklistItem.id,
position: checklistItem.position,
},
optimisticResponse: {
__typename: 'Mutation',
updateTaskChecklistItemLocation: {
__typename: 'UpdateTaskChecklistItemLocationPayload',
prevChecklistID,
checklistID,
taskChecklistID,
checklistItem: {
__typename: 'TaskChecklistItem',
position: checklistItem.position,
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 produce from 'immer';
import {
useUpdateProjectMemberRoleMutation,
useCreateProjectMemberMutation,
useDeleteProjectMemberMutation,
useSetTaskCompleteMutation,
useToggleTaskLabelMutation,
useUpdateProjectNameMutation,
useFindProjectQuery,
useUpdateTaskGroupNameMutation,
useUpdateTaskNameMutation,
useUpdateProjectLabelMutation,
useCreateTaskMutation,
useDeleteProjectLabelMutation,
useDeleteTaskMutation,
useUpdateTaskLocationMutation,
useUpdateTaskGroupLocationMutation,
useCreateTaskGroupMutation,
useDeleteTaskGroupMutation,
useUpdateTaskDescriptionMutation,
useAssignTaskMutation,
DeleteTaskDocument,
FindProjectDocument,
useCreateProjectLabelMutation,
useUnassignTaskMutation,
useUpdateTaskDueDateMutation,
FindProjectQuery,
useUsersQuery,
} from 'shared/generated/graphql';
import LabelManager from 'shared/components/PopupMenu/LabelManager';
import LabelEditor from 'shared/components/PopupMenu/LabelEditor';
@ -57,7 +36,9 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
FindProjectDocument,
cache =>
produce(cache, draftCache => {
draftCache.findProject.labels.push({ ...newLabelData.data.createProjectLabel });
if (newLabelData.data) {
draftCache.findProject.labels.push({ ...newLabelData.data.createProjectLabel });
}
}),
{
projectID,
@ -74,7 +55,7 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
cache =>
produce(cache, draftCache => {
draftCache.findProject.labels = cache.findProject.labels.filter(
label => label.id !== newLabelData.data.deleteProjectLabel.id,
label => label.id !== newLabelData.data?.deleteProjectLabel.id,
);
}),
{ projectID },

View File

@ -3,6 +3,7 @@ import React, { useState, useRef, useEffect, useContext } from 'react';
import updateApolloCache from 'shared/utils/cache';
import GlobalTopNavbar, { ProjectPopup } from 'App/TopNavbar';
import styled from 'styled-components/macro';
import AsyncSelect from 'react-select/async';
import { usePopup, Popup } from 'shared/components/PopupMenu';
import {
useParams,
@ -15,11 +16,12 @@ import {
} from 'react-router-dom';
import {
useUpdateProjectMemberRoleMutation,
useCreateProjectMemberMutation,
useInviteProjectMembersMutation,
useDeleteProjectMemberMutation,
useToggleTaskLabelMutation,
useUpdateProjectNameMutation,
useFindProjectQuery,
useDeleteInvitedProjectMemberMutation,
useUpdateTaskNameMutation,
useCreateTaskMutation,
useDeleteTaskMutation,
@ -30,19 +32,27 @@ import {
FindProjectDocument,
FindProjectQuery,
} from 'shared/generated/graphql';
import produce from 'immer';
import UserContext, { useCurrentUser } from 'App/context';
import Input from 'shared/components/Input';
import Member from 'shared/components/Member';
import EmptyBoard from 'shared/components/EmptyBoard';
import NOOP from 'shared/utils/noop';
import { 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 Details from './Details';
import LabelManagerEditor from './LabelManagerEditor';
import { mixin } from '../../shared/utils/styles';
const CARD_LABEL_VARIANT_STORAGE_KEY = 'card_label_variant';
const RFC2822_EMAIL = /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/;
const useStateWithLocalStorage = (localStorageKey: string): [string, React.Dispatch<React.SetStateAction<string>>] => {
const [value, setValue] = React.useState<string>(localStorage.getItem(localStorageKey) || '');
@ -61,7 +71,7 @@ const UserMember = styled(Member)`
padding: 4px 0;
cursor: pointer;
&: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;
`;
@ -70,29 +80,299 @@ const MemberList = styled.div`
margin: 8px 0;
`;
type InviteUserData = {
email?: string;
suerID?: string;
};
type UserManagementPopupProps = {
projectID: string;
users: Array<User>;
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 (
<Popup tab={0} title="Invite a user">
<SearchInput width="100%" variant="alternate" placeholder="Email address or name" name="search" />
<MemberList>
{users
.filter(u => u.id !== projectMembers.find(p => p.id === u.id)?.id)
.map(user => (
<UserMember
key={user.id}
onCardMemberClick={() => onAddProjectMember(user.id)}
showName
member={user}
taskID=""
/>
))}
</MemberList>
<InviteContainer>
<AsyncSelect
getOptionValue={option => option.value.id}
placeholder="Email address or username"
noOptionsMessage={() => null}
onChange={(e: any) => {
setInvitedUsers(e);
}}
isMulti
autoFocus
cacheOptions
styles={colourStyles}
defaultOption
components={{
MultiValue: OptionValue,
Option: UserOption,
IndicatorSeparator: null,
DropdownIndicator: null,
}}
loadOptions={(i, cb) => fetchMembers(client, projectID, {}, i, cb)}
/>
</InviteContainer>
<InviteButton
onClick={() => {
if (invitedUsers) {
onInviteProjectMembers(
invitedUsers.map(user => {
if (user.value.type === 0) {
return {
userID: user.value.id,
};
}
return {
email: user.value.id,
};
}),
);
}
}}
disabled={invitedUsers === null}
hoverVariant="none"
fontSize="16px"
>
Send Invite
</InviteButton>
</Popup>
);
};
@ -135,11 +415,32 @@ const Project = () => {
const [value, setValue] = useStateWithLocalStorage(CARD_LABEL_VARIANT_STORAGE_KEY);
const [updateProjectMemberRole] = useUpdateProjectMemberRoleMutation();
const [deleteTask] = useDeleteTaskMutation();
const [deleteTask] = useDeleteTaskMutation({
update: (client, resp) =>
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
if (resp.data) {
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 { loading, data } = useFindProjectQuery({
const { loading, data, error } = useFindProjectQuery({
variables: { projectID },
});
@ -150,21 +451,45 @@ const Project = () => {
FindProjectDocument,
cache =>
produce(cache, draftCache => {
draftCache.findProject.name = newName.data.updateProjectName.name;
draftCache.findProject.name = newName.data?.updateProjectName.name ?? '';
}),
{ projectID },
);
},
});
const [createProjectMember] = useCreateProjectMemberMutation({
const [inviteProjectMembers] = useInviteProjectMembersMutation({
update: (client, response) => {
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
draftCache.findProject.members.push({ ...response.data.createProjectMember.member });
if (response.data) {
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 },
);
@ -178,7 +503,7 @@ const Project = () => {
cache =>
produce(cache, draftCache => {
draftCache.findProject.members = cache.findProject.members.filter(
m => m.id !== response.data.deleteProjectMember.member.id,
m => m.id !== response.data?.deleteProjectMember.member.id,
);
}),
{ projectID },
@ -205,6 +530,9 @@ const Project = () => {
</>
);
}
if (error) {
history.push('/projects');
}
if (data) {
labelsRef.current = data.findProject.labels;
@ -221,6 +549,10 @@ const Project = () => {
deleteProjectMember({ variables: { userID, projectID } });
hidePopup();
}}
onRemoveInvitedFromBoard={email => {
deleteInvitedProjectMember({ variables: { projectID, email } });
hidePopup();
}}
onSaveProjectName={projectName => {
updateProjectName({ variables: { projectID, name: projectName } });
}}
@ -228,8 +560,10 @@ const Project = () => {
showPopup(
$target,
<UserManagementPopup
onAddProjectMember={userID => {
createProjectMember({ variables: { userID, projectID } });
projectID={projectID}
onInviteProjectMembers={members => {
inviteProjectMembers({ variables: { projectID, members } });
hidePopup();
}}
users={data.users}
projectMembers={data.findProject.members}
@ -240,8 +574,9 @@ const Project = () => {
menuType={[{ name: 'Board', link: location.pathname }]}
currentTab={0}
projectMembers={data.findProject.members}
projectInvitedMembers={data.findProject.invitedMembers}
projectID={projectID}
teamID={data.findProject.team.id}
teamID={data.findProject.team ? data.findProject.team.id : null}
name={data.findProject.name}
/>
<Route path={`${match.path}`} exact render={() => <Redirect to={`${match.url}/board`} />} />
@ -284,6 +619,7 @@ const Project = () => {
}}
onDeleteTask={deletedTask => {
deleteTask({ variables: { taskID: deletedTask.id } });
history.push(`${match.url}/board`);
}}
onOpenAddLabelPopup={(task, $targetRef) => {
taskLabelsRef.current = task.labels;

View File

@ -8,8 +8,6 @@ import {
useCreateProjectMutation,
GetProjectsDocument,
GetProjectsQuery,
MeQuery,
MeDocument,
} from 'shared/generated/graphql';
import { Link } from 'react-router-dom';
@ -22,33 +20,8 @@ import Input from 'shared/components/Input';
import updateApolloCache from 'shared/utils/cache';
import produce from 'immer';
import NOOP from 'shared/utils/noop';
const EmptyStateContent = styled.div`
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%;
`;
import theme from 'App/ThemeStyles';
import { mixin } from '../shared/utils/styles';
type CreateTeamData = { teamName: string };
@ -58,6 +31,10 @@ type CreateTeamFormProps = {
const CreateTeamFormContainer = styled.form``;
const CreateTeamButton = styled(Button)`
width: 100%;
`;
const CreateTeamForm: React.FC<CreateTeamFormProps> = ({ onCreateTeam }) => {
const { register, handleSubmit } = useForm<CreateTeamData>();
const createTeam = (data: CreateTeamData) => {
@ -79,7 +56,7 @@ const CreateTeamForm: React.FC<CreateTeamFormProps> = ({ onCreateTeam }) => {
};
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-position: 50%;
color: #fff;
@ -201,7 +178,7 @@ const SectionActionLink = styled(Link)`
const ProjectSectionTitle = styled.h3`
font-size: 16px;
color: rgba(${props => props.theme.colors.text.primary});
color: ${props => props.theme.colors.text.primary};
`;
const ProjectsContainer = styled.div`
@ -211,13 +188,6 @@ const ProjectsContainer = styled.div`
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)`
padding: 6px 12px;
position: absolute;
@ -225,10 +195,6 @@ const AddTeamButton = styled(Button)`
right: 12px;
`;
const CreateFirstTeam = styled(Button)`
margin-top: 8px;
`;
type ShowNewProject = {
open: boolean;
initialTeamID: null | string;
@ -244,7 +210,9 @@ const Projects = () => {
update: (client, newProject) => {
updateApolloCache<GetProjectsQuery>(client, GetProjectsDocument, cache =>
produce(cache, draftCache => {
draftCache.projects.push({ ...newProject.data.createProject });
if (newProject.data) {
draftCache.projects.push({ ...newProject.data.createProject });
}
}),
);
},
@ -256,7 +224,9 @@ const Projects = () => {
update: (client, createData) => {
updateApolloCache<GetProjectsQuery>(client, GetProjectsDocument, cache =>
produce(cache, draftCache => {
draftCache.teams.push({ ...createData.data.createTeam });
if (createData.data) {
draftCache.teams.push({ ...createData.data?.createTeam });
}
}),
);
},
@ -265,10 +235,17 @@ const Projects = () => {
return <GlobalTopNavbar onSaveProjectName={NOOP} projectID={null} name={null} />;
}
const colors = ['#e362e3', '#7a6ff0', '#37c5ab', '#aa62e3', '#e8384f'];
const colors = theme.colors.multiColors;
if (data && user) {
const { projects, teams, organizations } = data;
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
.sort((a, b) => {
const textA = a.name.toUpperCase();
@ -280,7 +257,7 @@ const Projects = () => {
id: team.id,
name: team.name,
projects: projects
.filter(project => project.team.id === team.id)
.filter(project => project.team && project.team.id === team.id)
.sort((a, b) => {
const textA = a.name.toUpperCase();
const textB = b.name.toUpperCase();
@ -321,39 +298,35 @@ const Projects = () => {
Add Team
</AddTeamButton>
)}
{projectTeams.length === 0 && (
<EmptyStateContent>
<EmptyState width={425} height={425} />
<EmptyStateTitle>No teams exist</EmptyStateTitle>
<EmptyStatePrompt>Create a new team to get started</EmptyStatePrompt>
<CreateFirstTeam
variant="outline"
onClick={$target => {
showPopup(
$target,
<Popup
title="Create team"
tab={0}
onClose={() => {
hidePopup();
}}
>
<CreateTeamForm
onCreateTeam={teamName => {
if (organizationID) {
createTeam({ variables: { name: teamName, organizationID } });
hidePopup();
}
}}
/>
</Popup>,
);
}}
>
Create new team
</CreateFirstTeam>
</EmptyStateContent>
)}
<div>
<ProjectSectionTitleWrapper>
<ProjectSectionTitle>Personal Projects</ProjectSectionTitle>
</ProjectSectionTitleWrapper>
<ProjectList>
{personalProjects.map((project, idx) => (
<ProjectListItem key={project.id}>
<ProjectTile color={colors[idx % 5]} to={`/projects/${project.id}`}>
<ProjectTileFade />
<ProjectTileDetails>
<ProjectTileName>{project.name}</ProjectTileName>
</ProjectTileDetails>
</ProjectTile>
</ProjectListItem>
))}
<ProjectListItem>
<ProjectAddTile
onClick={() => {
setShowNewProject({ open: true, initialTeamID: 'no-team' });
}}
>
<ProjectTileFade />
<ProjectAddTileDetails>
<ProjectTileName centered>Create new project</ProjectTileName>
</ProjectAddTileDetails>
</ProjectAddTile>
</ProjectListItem>
</ProjectList>
</div>
{projectTeams.map(team => {
return (
<div key={team.id}>
@ -407,7 +380,7 @@ const Projects = () => {
initialTeamID={showNewProject.initialTeamID}
onCreateProject={(name, teamID) => {
if (user) {
createProject({ variables: { teamID, name, userID: user.id } });
createProject({ variables: { teamID, name } });
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 ControlledInput from 'shared/components/ControlledInput';
import NOOP from 'shared/utils/noop';
import { mixin } from 'shared/utils/styles';
const MemberListWrapper = styled.div`
flex: 1 1;
@ -34,7 +35,7 @@ const UserMember = styled(Member)`
padding: 4px 0;
cursor: pointer;
&: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;
`;
@ -119,12 +120,12 @@ export const MiniProfileActionItem = styled.span<{ disabled?: boolean }>`
? css`
user-select: none;
pointer-events: none;
color: rgba(${props.theme.colors.text.primary}, 0.4);
color: ${mixin.rgba(props.theme.colors.text.primary, 0.4)};
`
: css`
cursor: pointer;
&: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`
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`
@ -146,13 +147,13 @@ export const Separator = styled.div`
export const WarningText = styled.span`
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;
`;
export const DeleteDescription = styled.div`
font-size: 14px;
color: rgba(${props => props.theme.colors.text.primary});
color: ${props => props.theme.colors.text.primary};
`;
export const RemoveMemberButton = styled(Button)`
@ -305,14 +306,14 @@ const MemberItemOption = styled(Button)`
`;
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`
display: flex;
flex-flow: row wrap;
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;
padding: 12px 0 12px 40px;
position: relative;
@ -336,11 +337,11 @@ const MemberProfile = styled(TaskAssignee)`
`;
const MemberItemName = styled.p`
color: rgba(${props => props.theme.colors.text.secondary});
color: ${props => props.theme.colors.text.secondary};
`;
const MemberItemUsername = styled.p`
color: rgba(${props => props.theme.colors.text.primary});
color: ${props => props.theme.colors.text.primary};
`;
const MemberListHeader = styled.div`
@ -349,12 +350,12 @@ const MemberListHeader = styled.div`
`;
const ListTitle = styled.h3`
font-size: 18px;
color: rgba(${props => props.theme.colors.text.secondary});
color: ${props => props.theme.colors.text.secondary};
margin-bottom: 12px;
`;
const ListDesc = styled.span`
font-size: 16px;
color: rgba(${props => props.theme.colors.text.primary});
color: ${props => props.theme.colors.text.primary};
`;
const FilterSearch = styled(Input)`
margin: 0;
@ -386,11 +387,11 @@ const FilterTabItem = styled.li`
font-weight: 700;
text-decoration: none;
padding: 6px 8px;
color: rgba(${props => props.theme.colors.text.primary});
color: ${props => props.theme.colors.text.primary};
&:hover {
border-radius: 6px;
background: rgba(${props => props.theme.colors.primary});
color: rgba(${props => props.theme.colors.text.secondary});
background: ${props => props.theme.colors.primary};
color: ${props => props.theme.colors.text.secondary};
}
`;
@ -429,11 +430,13 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
GetTeamDocument,
cache =>
produce(cache, draftCache => {
draftCache.findTeam.members.push({
...response.data.createTeamMember.teamMember,
member: { __typename: 'MemberList', projects: [], teams: [] },
owned: { __typename: 'OwnedList', projects: [], teams: [] },
});
if (response.data) {
draftCache.findTeam.members.push({
...response.data.createTeamMember.teamMember,
member: { __typename: 'MemberList', projects: [], teams: [] },
owned: { __typename: 'OwnedList', projects: [], teams: [] },
});
}
}),
{ teamID },
);
@ -458,7 +461,7 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
cache =>
produce(cache, draftCache => {
draftCache.findTeam.members = cache.findTeam.members.filter(
member => member.id !== response.data.deleteTeamMember.userID,
member => member.id !== response.data?.deleteTeamMember.userID,
);
}),
{ teamID },

View File

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

View File

@ -33,7 +33,7 @@ const Wrapper = styled.div`
`;
type TeamPopupProps = {
history: History<History.PoorMansUnknown>;
history: History<any>;
name: string;
teamID: string;
};
@ -44,9 +44,9 @@ export const TeamPopup: React.FC<TeamPopupProps> = ({ history, name, teamID }) =
update: (client, deleteData) => {
updateApolloCache<GetProjectsQuery>(client, GetProjectsDocument, cache =>
produce(cache, draftCache => {
draftCache.teams = cache.teams.filter((team: any) => team.id !== deleteData.data.deleteTeam.team.id);
draftCache.teams = cache.teams.filter((team: any) => team.id !== deleteData.data?.deleteTeam.team.id);
draftCache.projects = cache.projects.filter(
(project: any) => project.team.id !== deleteData.data.deleteTeam.team.id,
(project: any) => project.team.id !== deleteData.data?.deleteTeam.team.id,
);
}),
);

View File

@ -8,15 +8,28 @@ import { HttpLink } from 'apollo-link-http';
import { onError } from 'apollo-link-error';
import { enableMapSet } from 'immer';
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 cache from './App/cache';
import App from './App';
// https://able.bio/AnasT/apollo-graphql-async-access-token-refresh--470t1c8
dayjs.extend(isSameOrAfter);
dayjs.extend(weekday);
dayjs.extend(isBetween);
dayjs.extend(customParseFormat);
enableMapSet();
moment.updateLocale('en', {
dayjs.extend(updateLocale);
dayjs.updateLocale('en', {
week: {
dow: 1, // First day of week is Monday
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 { action } from '@storybook/addon-actions';
import theme from 'App/ThemeStyles';
import AddList from '.';
export default {
@ -7,7 +8,7 @@ export default {
title: 'AddList',
parameters: {
backgrounds: [
{ name: 'gray', value: '#262c49', default: true },
{ name: 'gray', value: theme.colors.bg.secondary, default: true },
{ name: 'white', value: '#ffffff' },
],
},

View File

@ -67,7 +67,7 @@ export const ListNameEditorWrapper = styled.div`
display: flex;
`;
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;
box-shadow: inset 0 0 0 2px #0079bf;
transition: margin 85ms ease-in, background 85ms ease-in;
@ -91,7 +91,7 @@ export const ListNameEditor = styled(TextareaAutosize)`
color: #c2c6dc;
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')}
onDeleteInvitedUser={action('delete invited user')}
/>
</ThemeProvider>
</>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,6 +12,7 @@ import Button from 'shared/components/Button';
import TextareaAutosize from 'react-autosize-textarea';
import Control from 'react-select/src/components/Control';
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import { mixin } from 'shared/utils/styles';
const Wrapper = styled.div`
margin-bottom: 24px;
@ -38,7 +39,7 @@ const WindowChecklistTitle = styled.div`
const WindowTitleText = styled.h3`
cursor: pointer;
color: rgba(${props => props.theme.colors.text.primary});
color: ${props => props.theme.colors.text.primary};
margin: 6px 0;
display: inline-block;
width: auto;
@ -73,7 +74,7 @@ const ChecklistProgressPercent = styled.span`
`;
const ChecklistProgressBar = styled.div`
background: rgba(${props => props.theme.colors.bg.primary});
background: ${props => props.theme.colors.bg.primary};
border-radius: 4px;
clear: both;
height: 8px;
@ -83,7 +84,7 @@ const ChecklistProgressBar = styled.div`
`;
const ChecklistProgressBarCurrent = styled.div<{ width: number }>`
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;
left: 0;
position: absolute;
@ -111,7 +112,7 @@ const ChecklistIcon = styled.div`
`;
const ChecklistItemCheckedIcon = styled(CheckSquare)`
fill: rgba(${props => props.theme.colors.primary});
fill: ${props => props.theme.colors.primary};
`;
const ChecklistItemDetails = styled.div`
@ -133,7 +134,7 @@ const ChecklistItemTextControls = styled.div`
`;
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;'}
line-height: 20px;
font-size: 16px;
@ -155,14 +156,14 @@ const ControlButton = styled.div`
margin-left: 4px;
padding: 4px 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;
width: 32px;
height: 32px;
align-items: center;
justify-content: center;
&: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;
font-size: 16px;
line-height: 20px;
border: 1px solid rgba(${props => props.theme.colors.primary});
border: 1px solid ${props => props.theme.colors.primary};
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});
background-color: rgba(${props => props.theme.colors.bg.primary}, 0.4);
border-color: ${props => props.theme.colors.border};
background-color: ${props => mixin.rgba(props.theme.colors.bg.primary, 0.4)};
&:focus {
border-color: rgba(${props => props.theme.colors.primary});
border-color: ${props => props.theme.colors.primary};
}
`;
const AssignUserButton = styled(AccountPlus)`
fill: rgba(${props => props.theme.colors.text.primary});
fill: ${props => props.theme.colors.text.primary};
`;
const ClockButton = styled(Clock)`
fill: rgba(${props => props.theme.colors.text.primary});
fill: ${props => props.theme.colors.text.primary};
`;
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 }>`
@ -224,7 +225,7 @@ const ChecklistItemWrapper = styled.div<{ ref: any }>`
}
&: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} {
opacity: 1;
@ -246,10 +247,10 @@ const CancelButton = styled.div`
cursor: pointer;
margin: 5px;
& svg {
fill: rgba(${props => props.theme.colors.text.primary});
fill: ${props => props.theme.colors.text.primary};
}
&: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;
&: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;
align-items: 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 }>`
@ -24,11 +24,11 @@ const Container = styled.div<{ color?: string }>`
? css`
background: ${props.color};
& ${LabelText} {
color: rgba(${props.theme.colors.text.secondary});
color: ${props.theme.colors.text.secondary};
}
`
: 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`
background: rgba(${props => props.theme.colors.bg.primary});
background: ${props => props.theme.colors.bg.primary};
padding: 45px;
margin: 25px;
display: flex;

View File

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

View File

@ -2,6 +2,7 @@ import React, { createRef, useState } from 'react';
import styled from 'styled-components';
import { action } from '@storybook/addon-actions';
import DropdownMenu from '.';
import theme from '../../../App/ThemeStyles';
export default {
component: DropdownMenu,
@ -10,7 +11,7 @@ export default {
backgrounds: [
{ name: 'white', value: '#ffffff' },
{ 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;
font-size: 14px;
&:hover {
background: rgb(115, 103, 240);
background: ${props => props.theme.colors.primary};
}
`;

View File

@ -19,23 +19,23 @@ display: flex
}
& .react-datepicker-time__header {
color: rgba(${props => props.theme.colors.text.primary});
color: ${props => props.theme.colors.text.primary};
}
& .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-box ul.react-datepicker__time-list
li.react-datepicker__time-list-item:hover {
color: rgba(${props => props.theme.colors.text.secondary});
background: rgba(${props => props.theme.colors.bg.secondary});
color: ${props => props.theme.colors.text.secondary};
background: ${props => props.theme.colors.bg.secondary};
}
& .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 {
background: rgba(${props => props.theme.colors.bg.primary});
border: 1px solid rgba(${props => props.theme.colors.border});
background: ${props => props.theme.colors.bg.primary};
border: 1px solid ${props => props.theme.colors.border};
}
& .react-datepicker * {
@ -75,12 +75,12 @@ display: flex
}
& .react-datepicker__day--selected {
border-radius: 50%;
background: rgba(115, 103, 240);
background: ${props => props.theme.colors.primary};
color: #fff;
}
& .react-datepicker__day--selected:hover {
border-radius: 50%;
background: rgba(115, 103, 240);
background: ${props => props.theme.colors.primary};
color: #fff;
}
& .react-datepicker__header {
@ -88,7 +88,7 @@ display: flex
border: none;
}
& .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 moment from 'moment';
import dayjs from 'dayjs';
import styled from 'styled-components';
import DatePicker from 'react-datepicker';
import _ from 'lodash';
@ -43,7 +43,7 @@ const HeaderSelectLabel = styled.div`
color: #c2c6dc;
&:hover {
background: rgba(115, 103, 240);
background: ${props => props.theme.colors.primary};
color: #c2c6dc;
}
`;
@ -60,8 +60,8 @@ const HeaderSelect = styled.select`
appearance: none;
&:hover {
background: #262c49;
border: 1px solid rgba(115, 103, 240);
background: ${props => props.theme.colors.bg.secondary};
border: 1px solid ${props => props.theme.colors.primary};
outline: none !important;
box-shadow: none;
color: #c2c6dc;
@ -93,7 +93,7 @@ const HeaderButton = styled.button`
border: none;
border-radius: 3px;
&:hover {
background: rgba(115, 103, 240);
background: ${props => props.theme.colors.primary};
color: #fff;
}
`;
@ -110,12 +110,12 @@ const HeaderActions = styled.div`
`;
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 [startDate, setStartDate] = useState(new Date());
useEffect(() => {
const newDate = moment(startDate).format('YYYY-MM-DD');
const newDate = dayjs(startDate).format('YYYY-MM-DD');
setValue('endDate', newDate);
}, [startDate]);
@ -135,7 +135,7 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
'December',
];
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()) {
onDueDateChange(task, newDate.toDate());
}

View File

@ -1,6 +1,7 @@
import React from 'react';
import styled, { keyframes } from 'styled-components/macro';
import { mixin } from 'shared/utils/styles';
import theme from '../../../App/ThemeStyles';
export const BoardContainer = styled.div`
position: relative;
@ -34,9 +35,9 @@ export const Container = styled.div`
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`
0% {

View File

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

View File

@ -1,5 +1,6 @@
import React, { useState, useEffect, useRef } from 'react';
import styled, { css } from 'styled-components/macro';
import theme from '../../../App/ThemeStyles';
const InputWrapper = styled.div<{ width: string }>`
position: relative;
@ -53,18 +54,18 @@ const InputInput = styled.input<{
transition: all 0.3s ease;
&:focus {
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};
}
&:focus ~ ${InputLabel} {
color: rgba(115, 103, 240);
color: ${props => props.theme.colors.primary};
transform: translate(-3px, -90%);
}
${props =>
props.hasValue &&
css`
& ~ ${InputLabel} {
color: rgba(115, 103, 240);
color: ${props.theme.colors.primary};
transform: translate(-3px, -90%);
}
`}
@ -138,8 +139,8 @@ const Input = React.forwardRef(
$ref: any,
) => {
const [hasValue, setHasValue] = useState(defaultValue !== '');
const borderColor = variant === 'normal' ? 'rgba(0, 0, 0, 0.2)' : '#414561';
const focusBg = variant === 'normal' ? 'rgba(38, 44, 73, )' : 'rgba(16, 22, 58, 1)';
const borderColor = variant === 'normal' ? 'rgba(0,0,0,0.2)' : theme.colors.alternate;
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
// 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 TextareaAutosize from 'react-autosize-textarea';
import { mixin } from 'shared/utils/styles';
export const Container = styled.div`
width: 272px;
@ -34,7 +33,7 @@ export const AddCardButton = styled.a`
&:hover {
color: #c2c6dc;
text-decoration: none;
background: rgb(115, 103, 240);
background: ${props => props.theme.colors.primary};
}
`;
export const Wrapper = styled.div`
@ -73,7 +72,6 @@ export const HeaderName = styled(TextareaAutosize)`
box-shadow: none;
font-weight: 600;
margin: -4px 0;
padding: 4px 8px;
letter-spacing: normal;
word-spacing: normal;
@ -97,7 +95,7 @@ export const Header = styled.div<{ isEditing: boolean }>`
props.isEditing &&
css`
& ${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;
text-decoration: none;
&:hover {
background: rgb(115, 103, 240);
background: ${props => props.theme.colors.primary};
}
`;

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { TaskMetaFilters, DueDateFilterType } from 'shared/components/Lists';
import moment from 'moment';
import dayjs from 'dayjs';
enum ShouldFilter {
NO_FILTER,
@ -24,8 +24,8 @@ export default function shouldMetaFilter(task: Task, filters: TaskMetaFilters) {
isFiltered = shouldFilter(!(task.dueDate && task.dueDate !== null));
}
if (task.dueDate) {
const taskDueDate = moment(task.dueDate);
const today = moment();
const taskDueDate = dayjs(task.dueDate);
const today = dayjs();
let start;
let end;
switch (filters.dueDate.type) {
@ -40,7 +40,7 @@ export default function shouldMetaFilter(task: Task, filters: TaskMetaFilters) {
taskDueDate.isBefore(
today
.clone()
.add(1, 'days')
.add(1, 'day')
.endOf('day'),
),
);
@ -60,12 +60,12 @@ export default function shouldMetaFilter(task: Task, filters: TaskMetaFilters) {
start = today
.clone()
.weekday(0)
.add(7, 'days')
.add(7, 'day')
.startOf('day');
end = today
.clone()
.weekday(6)
.add(7, 'days')
.add(7, 'day')
.endOf('day');
isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
break;
@ -73,7 +73,7 @@ export default function shouldMetaFilter(task: Task, filters: TaskMetaFilters) {
start = today.clone().startOf('day');
end = today
.clone()
.add(7, 'days')
.add(7, 'day')
.endOf('day');
isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
break;
@ -81,7 +81,7 @@ export default function shouldMetaFilter(task: Task, filters: TaskMetaFilters) {
start = today.clone().startOf('day');
end = today
.clone()
.add(14, 'days')
.add(14, 'day')
.endOf('day');
isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
break;
@ -89,7 +89,7 @@ export default function shouldMetaFilter(task: Task, filters: TaskMetaFilters) {
start = today.clone().startOf('day');
end = today
.clone()
.add(21, 'days')
.add(21, 'day')
.endOf('day');
isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
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 Button from 'shared/components/Button';
import { mixin } from 'shared/utils/styles';
export const Wrapper = styled.div`
background: #eff2f7;
@ -68,7 +69,7 @@ export const FormIcon = styled.div`
export const FormError = styled.span`
font-size: 0.875rem;
color: rgb(234, 84, 85);
color: ${props => props.theme.colors.danger};
`;
export const LoginButton = styled(Button)``;
@ -99,5 +100,5 @@ export const LogoWrapper = styled.div`
padding-bottom: 16px;
margin-bottom: 24px;
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 { useForm } from 'react-hook-form';
import LoadingSpinner from 'shared/components/LoadingSpinner';
import {
Form,
LogoWrapper,
@ -73,6 +74,7 @@ const Login = ({ onSubmit }: LoginProps) => {
<ActionButtons>
<RegisterButton variant="outline">Register</RegisterButton>
{!isComplete && <LoadingSpinner size="32px" thickness="2px" borderSize="48px" />}
<LoginButton type="submit" disabled={!isComplete}>
Login
</LoginButton>

View File

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

View File

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

View File

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

View File

@ -30,9 +30,9 @@ const CloseIcon = styled(Cross)`
top: 16px;
right: -32px;
cursor: pointer;
fill: rgba(${props => props.theme.colors.text.primary});
fill: ${props => props.theme.colors.text.primary};
&: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 { mixin } from 'shared/utils/styles';
export const Logo = styled.div``;
@ -9,7 +10,7 @@ export const LogoTitle = styled.div`
font-size: 24px;
font-weight: 600;
transition: visibility, opacity, transform 0.25s ease;
color: #7367f0;
color: #22ff00;
`;
export const ActionContainer = styled.div`
position: relative;
@ -46,8 +47,8 @@ export const ActionButtonWrapper = styled.div<{ active?: boolean }>`
${props =>
props.active &&
css`
background: rgb(115, 103, 240);
box-shadow: 0 0 10px 1px rgba(115, 103, 240, 0.7);
background: ${props.theme.colors.primary};
box-shadow: 0 0 10px 1px ${mixin.rgba(props.theme.colors.primary, 0.7)};
`}
border-radius: 6px;
cursor: pointer;
@ -73,7 +74,7 @@ export const LogoWrapper = styled.div`
color: rgb(222, 235, 255);
cursor: pointer;
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`
@ -87,12 +88,12 @@ export const Container = styled.aside`
transform: translateZ(0px);
background: #10163a;
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 {
width: 260px;
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} {
bottom: -12px;
@ -106,6 +107,6 @@ export const Container = styled.aside`
}
&: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 Select from 'react-select';
import { ArrowLeft, Cross } from 'shared/icons';
import theme from '../../../App/ThemeStyles';
function getBackgroundColor(isDisabled: boolean, isSelected: boolean, isFocused: boolean) {
if (isDisabled) {
return null;
}
if (isSelected) {
return mixin.darken('#262c49', 0.25);
return mixin.darken(theme.colors.bg.secondary, 0.25);
}
if (isFocused) {
return mixin.darken('#262c49', 0.15);
return mixin.darken(theme.colors.bg.secondary, 0.15);
}
return null;
}
@ -97,8 +98,8 @@ const ProjectName = styled.input`
font-weight: 400;
&:focus {
background: ${mixin.darken('#262c49', 0.15)};
box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px;
background: ${props => mixin.darken(props.theme.colors.bg.secondary, 0.15)};
box-shadow: ${props => props.theme.colors.primary} 0px 0px 0px 1px;
}
`;
const ProjectNameLabel = styled.label`
@ -126,35 +127,35 @@ const colourStyles = {
control: (styles: any, data: any) => {
return {
...styles,
backgroundColor: data.isMenuOpen ? mixin.darken('#262c49', 0.15) : '#262c49',
boxShadow: data.menuIsOpen ? 'rgb(115, 103, 240) 0px 0px 0px 1px' : 'none',
backgroundColor: data.isMenuOpen ? mixin.darken(theme.colors.bg.secondary, 0.15) : theme.colors.bg.secondary,
boxShadow: data.menuIsOpen ? `${theme.colors.primary} 0px 0px 0px 1px` : 'none',
borderRadius: '3px',
borderWidth: '1px',
borderStyle: 'solid',
borderImage: 'initial',
borderColor: '#414561',
borderColor: theme.colors.alternate,
':hover': {
boxShadow: 'rgb(115, 103, 240) 0px 0px 0px 1px',
boxShadow: `${theme.colors.primary} 0px 0px 0px 1px`,
borderRadius: '3px',
borderWidth: '1px',
borderStyle: 'solid',
borderImage: 'initial',
borderColor: '#414561',
borderColor: theme.colors.alternate,
},
':active': {
boxShadow: 'rgb(115, 103, 240) 0px 0px 0px 1px',
boxShadow: `${theme.colors.primary} 0px 0px 0px 1px`,
borderRadius: '3px',
borderWidth: '1px',
borderStyle: 'solid',
borderImage: 'initial',
borderColor: 'rgb(115, 103, 240)',
borderColor: `${theme.colors.primary}`,
},
};
},
menu: (styles: any) => {
return {
...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' } }),
@ -167,11 +168,11 @@ const colourStyles = {
cursor: isDisabled ? 'not-allowed' : 'default',
':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': {
...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 {
color: #fff;
background: rgb(115, 103, 240);
border-color: rgb(115, 103, 240);
background: ${props => props.theme.colors.primary};
border-color: ${props => props.theme.colors.primary};
}
`;
type NewProjectProps = {
initialTeamID: string | null;
teams: Array<Team>;
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 [projectName, setProjectName] = useState('');
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 (
<Overlay>
<Content>
@ -262,7 +263,7 @@ const NewProject: React.FC<NewProjectProps> = ({ initialTeamID, teams, onClose,
onChange={(e: any) => {
setTeam(e.value);
}}
value={options.filter(d => d.value === team)}
value={options.find(d => d.value === team)}
styles={colourStyles}
classNamePrefix="teamSelect"
options={options}
@ -271,8 +272,8 @@ const NewProject: React.FC<NewProjectProps> = ({ initialTeamID, teams, onClose,
</ProjectInfo>
<CreateButton
onClick={() => {
if (team && projectName !== '') {
onCreateProject(projectName, team);
if (projectName !== '') {
onCreateProject(projectName, team === 'no-team' ? null : team);
}
}}
>

View File

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

View File

@ -1,6 +1,7 @@
import styled, { css } from 'styled-components';
import { mixin } from 'shared/utils/styles';
import ControlledInput from 'shared/components/ControlledInput';
import theme from 'App/ThemeStyles';
export const Container = styled.div<{
invertY: boolean;
@ -176,7 +177,7 @@ export const LabelIcon = styled.div`
cursor: pointer;
&:hover {
background: rgb(115, 103, 240);
background: ${props => props.theme.colors.primary};
}
`;
@ -233,8 +234,8 @@ export const FieldName = styled.input`
font-weight: 400;
&:focus {
box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px;
background: ${mixin.darken('#262c49', 0.15)};
box-shadow: ${props => props.theme.colors.primary} 0px 0px 0px 1px;
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`
background: rgb(115, 103, 240);
background: ${props => props.theme.colors.primary};
box-shadow: none;
border: none;
color: #fff;
@ -296,7 +297,7 @@ export const DeleteButton = styled.input`
&:hover {
color: #fff;
background: rgb(115, 103, 240);
background: ${props => props.theme.colors.primary};
border-color: transparent;
}
`;
@ -317,7 +318,7 @@ export const CreateLabelButton = styled.button`
cursor: pointer;
&: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 NOOP from 'shared/utils/noop';
import produce from 'immer';
import theme from 'App/ThemeStyles';
import {
Container,
ContainerDiamond,
@ -18,7 +19,7 @@ import {
function getPopupOptions(options?: PopupOptions) {
const popupOptions = {
borders: true,
diamondColor: '#262c49',
diamondColor: theme.colors.bg.secondary,
targetPadding: '10px',
showDiamond: true,
width: 316,

View File

@ -24,7 +24,7 @@ export const ListActionItem = styled.span`
margin: 0 -12px;
text-decoration: none;
&: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 TextareaAutosize from 'react-autosize-textarea';
import { mixin } from 'shared/utils/styles';
export const Wrapper = styled.div<{ open: boolean }>`
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`
cursor: pointer;
background: rgb(115, 103, 240);
background: ${props => props.theme.colors.primary};
box-shadow: none;
border: none;
color: #fff;

View File

@ -1,5 +1,6 @@
import styled from 'styled-components';
import Button from 'shared/components/Button';
import { mixin } from 'shared/utils/styles';
export const Wrapper = styled.div`
background: #eff2f7;
@ -68,7 +69,7 @@ export const FormIcon = styled.div`
export const FormError = styled.span`
font-size: 0.875rem;
color: rgb(234, 84, 85);
color: ${props => props.theme.colors.danger};
`;
export const LoginButton = styled(Button)``;
@ -99,5 +100,5 @@ export const LogoWrapper = styled.div`
padding-bottom: 16px;
margin-bottom: 24px;
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 INITIALS_PATTERN = /[a-zA-Z]{2,3}/i;
const Register = ({ onSubmit }: RegisterProps) => {
const Register = ({ onSubmit, registered = false }: RegisterProps) => {
const [isComplete, setComplete] = useState(true);
const { register, handleSubmit, errors, setError } = useForm<RegisterFormData>();
const loginSubmit = (data: RegisterFormData) => {
@ -43,103 +43,112 @@ const Register = ({ onSubmit }: RegisterProps) => {
<Taskcafe width={42} height={42} />
<LogoTitle>Taskcafé</LogoTitle>
</LogoWrapper>
<Title>Register</Title>
<SubTitle>Please create the system admin user</SubTitle>
<Form onSubmit={handleSubmit(loginSubmit)}>
<FormLabel htmlFor="fullname">
Full name
<FormTextInput
type="text"
id="fullname"
name="fullname"
ref={register({ required: 'Full name is required' })}
/>
<FormIcon>
<User width={20} height={20} />
</FormIcon>
</FormLabel>
{errors.username && <FormError>{errors.username.message}</FormError>}
<FormLabel htmlFor="username">
Username
<FormTextInput
type="text"
id="username"
name="username"
ref={register({ required: 'Username is required' })}
/>
<FormIcon>
<User width={20} height={20} />
</FormIcon>
</FormLabel>
{errors.username && <FormError>{errors.username.message}</FormError>}
<FormLabel htmlFor="email">
Email
<FormTextInput
type="text"
id="email"
name="email"
ref={register({
required: 'Email is required',
pattern: { value: EMAIL_PATTERN, message: 'Must be a valid email' },
})}
/>
<FormIcon>
<User width={20} height={20} />
</FormIcon>
</FormLabel>
{errors.email && <FormError>{errors.email.message}</FormError>}
<FormLabel htmlFor="initials">
Initials
<FormTextInput
type="text"
id="initials"
name="initials"
ref={register({
required: 'Initials is required',
pattern: {
value: INITIALS_PATTERN,
message: 'Initials must be between 2 to 3 characters.',
},
})}
/>
<FormIcon>
<User width={20} height={20} />
</FormIcon>
</FormLabel>
{errors.initials && <FormError>{errors.initials.message}</FormError>}
<FormLabel htmlFor="password">
Password
<FormTextInput
type="password"
id="password"
name="password"
ref={register({ required: 'Password is required' })}
/>
<FormIcon>
<Lock width={20} height={20} />
</FormIcon>
</FormLabel>
{errors.password && <FormError>{errors.password.message}</FormError>}
<FormLabel htmlFor="password_confirm">
Password (Confirm)
<FormTextInput
type="password"
id="password_confirm"
name="password_confirm"
ref={register({ required: 'Password (confirm) is required' })}
/>
<FormIcon>
<Lock width={20} height={20} />
</FormIcon>
</FormLabel>
{errors.password_confirm && <FormError>{errors.password_confirm.message}</FormError>}
{registered ? (
<>
<Title>Thanks for registering</Title>
<SubTitle>Please check your inbox for a confirmation email.</SubTitle>
</>
) : (
<>
<Title>Register</Title>
<SubTitle>Please create your user</SubTitle>
<Form onSubmit={handleSubmit(loginSubmit)}>
<FormLabel htmlFor="fullname">
Full name
<FormTextInput
type="text"
id="fullname"
name="fullname"
ref={register({ required: 'Full name is required' })}
/>
<FormIcon>
<User width={20} height={20} />
</FormIcon>
</FormLabel>
{errors.username && <FormError>{errors.username.message}</FormError>}
<FormLabel htmlFor="username">
Username
<FormTextInput
type="text"
id="username"
name="username"
ref={register({ required: 'Username is required' })}
/>
<FormIcon>
<User width={20} height={20} />
</FormIcon>
</FormLabel>
{errors.username && <FormError>{errors.username.message}</FormError>}
<FormLabel htmlFor="email">
Email
<FormTextInput
type="text"
id="email"
name="email"
ref={register({
required: 'Email is required',
pattern: { value: EMAIL_PATTERN, message: 'Must be a valid email' },
})}
/>
<FormIcon>
<User width={20} height={20} />
</FormIcon>
</FormLabel>
{errors.email && <FormError>{errors.email.message}</FormError>}
<FormLabel htmlFor="initials">
Initials
<FormTextInput
type="text"
id="initials"
name="initials"
ref={register({
required: 'Initials is required',
pattern: {
value: INITIALS_PATTERN,
message: 'Initials must be between 2 to 3 characters.',
},
})}
/>
<FormIcon>
<User width={20} height={20} />
</FormIcon>
</FormLabel>
{errors.initials && <FormError>{errors.initials.message}</FormError>}
<FormLabel htmlFor="password">
Password
<FormTextInput
type="password"
id="password"
name="password"
ref={register({ required: 'Password is required' })}
/>
<FormIcon>
<Lock width={20} height={20} />
</FormIcon>
</FormLabel>
{errors.password && <FormError>{errors.password.message}</FormError>}
<FormLabel htmlFor="password_confirm">
Password (Confirm)
<FormTextInput
type="password"
id="password_confirm"
name="password_confirm"
ref={register({ required: 'Password (confirm) is required' })}
/>
<FormIcon>
<Lock width={20} height={20} />
</FormIcon>
</FormLabel>
{errors.password_confirm && <FormError>{errors.password_confirm.message}</FormError>}
<ActionButtons>
<RegisterButton type="submit" disabled={!isComplete}>
Register
</RegisterButton>
</ActionButtons>
</Form>
<ActionButtons>
<RegisterButton type="submit" disabled={!isComplete}>
Register
</RegisterButton>
</ActionButtons>
</Form>
</>
)}
</LoginFormContainer>
</LoginFormWrapper>
</Column>

View File

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

View File

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

View File

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

View File

@ -0,0 +1,23 @@
import React from 'react';
import styled from 'styled-components';
import { TaskActivityData, ActivityType } from 'shared/generated/graphql';
type ActivityMessageProps = {
type: ActivityType;
data: Array<TaskActivityData>;
};
function getVariable(data: Array<TaskActivityData>, name: string) {
const target = data.find(d => d.name === name);
return target ? target.value : null;
}
const ActivityMessage: React.FC<ActivityMessageProps> = ({ type, data }) => {
switch (type) {
case ActivityType.TaskAdded:
return <>`added this task to ${getVariable(data, 'TaskGroup')}`</>;
}
return <h1>hello</h1>;
};
export default ActivityMessage;

View File

@ -3,8 +3,6 @@ import TextareaAutosize from 'react-autosize-textarea';
import { mixin } from 'shared/utils/styles';
import Button from 'shared/components/Button';
import TaskAssignee from 'shared/components/TaskAssignee';
import { User, Trash, Paperclip } from 'shared/icons';
import Member from 'shared/components/Member';
export const Container = styled.div`
display: flex;
@ -33,35 +31,35 @@ export const MarkCompleteButton = styled.button<{ invert: boolean }>`
${props =>
props.invert
? css`
background: rgba(${props.theme.colors.success});
background: ${props.theme.colors.success};
& svg {
fill: rgba(${props.theme.colors.text.secondary});
fill: ${props.theme.colors.text.secondary};
}
& span {
color: rgba(${props.theme.colors.text.secondary});
color: ${props.theme.colors.text.secondary};
}
&:hover {
background: rgba(${props.theme.colors.success}, 0.8);
background: ${mixin.rgba(props.theme.colors.success, 0.8)};
}
`
: css`
background: none;
border: 1px solid rgba(${props.theme.colors.text.secondary});
border: 1px solid ${props.theme.colors.text.secondary};
& svg {
fill: rgba(${props.theme.colors.text.secondary});
fill: ${props.theme.colors.text.secondary};
}
& span {
color: rgba(${props.theme.colors.text.secondary});
color: ${props.theme.colors.text.secondary};
}
&:hover {
background: rgba(${props.theme.colors.success}, 0.08);
border: 1px solid rgba(${props.theme.colors.success});
background: ${mixin.rgba(props.theme.colors.success, 0.08)};
border: 1px solid ${props.theme.colors.success};
}
&:hover svg {
fill: rgba(${props.theme.colors.success});
fill: ${props.theme.colors.success};
}
&: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;
min-height: 24px;
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;
letter-spacing: 0.5px;
text-transform: uppercase;
@ -93,7 +91,7 @@ export const SidebarTitle = styled.div`
export const SidebarButton = styled.div`
font-size: 14px;
color: rgba(${props => props.theme.colors.text.primary});
color: ${props => props.theme.colors.text.primary};
min-height: 32px;
width: 100%;
@ -168,7 +166,7 @@ export const TaskDetailsTitle = styled(TextareaAutosize)`
}
&: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;
min-height: 24px;
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;
letter-spacing: 0.5px;
text-transform: uppercase;
@ -187,7 +185,7 @@ export const AssignedUsersSection = styled.div`
padding-right: 32px;
padding-top: 24px;
padding-bottom: 24px;
border-bottom: 1px solid #414561;
border-bottom: 1px solid ${props => props.theme.colors.alternate};
display: flex;
flex-direction: column;
`;
@ -205,10 +203,10 @@ export const AssignUserIcon = styled.div`
justify-content: center;
align-items: center;
&: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 {
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;
border: 1px solid transparent;
&:hover {
border: 1px solid ${mixin.darken('#414561', 0.15)};
border: 1px solid ${props => mixin.darken(props.theme.colors.alternate, 0.15)};
}
&:hover ${AssignUserIcon} {
border: 1px solid #414561;
border: 1px solid ${props => props.theme.colors.alternate};
}
`;
export const AssignUserLabel = styled.span`
flex: 1 1 auto;
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`
@ -245,7 +243,7 @@ export const ExtraActionsSection = styled.div`
`;
export const ActionButtonsTitle = styled.h3`
color: rgba(${props => props.theme.colors.text.primary});
color: ${props => props.theme.colors.text.primary};
font-size: 12px;
font-weight: 500;
letter-spacing: 0.04em;
@ -255,7 +253,7 @@ export const ActionButton = styled(Button)`
margin-top: 8px;
margin-left: -10px;
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;
transition: transform 0.2s ease;
& span {
@ -264,7 +262,7 @@ export const ActionButton = styled(Button)`
&:hover {
box-shadow: none;
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;
svg {
fill: rgba(${props => props.theme.colors.text.primary}, 0.75);
fill: ${props => mixin.rgba(props.theme.colors.text.primary, 0.75)};
}
&: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`
color: rgba(${props => props.theme.colors.text.primary});
color: ${props => props.theme.colors.text.primary};
font-size: 12px;
font-weight: 500;
letter-spacing: 0.04em;
@ -362,7 +360,7 @@ export const MetaDetailContent = styled.div`
`;
export const TaskDetailsAddLabel = styled.div`
border-radius: 3px;
background: ${mixin.darken('#262c49', 0.15)};
background: ${props => mixin.darken(props.theme.colors.bg.secondary, 0.15)};
cursor: pointer;
&:hover {
opacity: 0.8;
@ -377,7 +375,7 @@ export const TaskDetailsAddLabelIcon = styled.div`
align-items: center;
justify-content: center;
border-radius: 3px;
background: ${mixin.darken('#262c49', 0.15)};
background: ${props => mixin.darken(props.theme.colors.bg.secondary, 0.15)};
cursor: pointer;
&:hover {
opacity: 0.8;
@ -452,11 +450,11 @@ export const TabBarSection = 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;
margin-bottom: -1px;
margin-right: 36px;
color: rgba(${props => props.theme.colors.text.primary});
color: ${props => props.theme.colors.text.primary};
`;
export const CommentContainer = styled.div`
@ -491,7 +489,7 @@ export const CommentTextArea = styled(TextareaAutosize)`
line-height: 28px;
padding: 4px 6px;
border-radius: 6px;
color: rgba(${props => props.theme.colors.text.primary});
color: ${props => props.theme.colors.text.primary};
background: #1f243e;
border: none;
transition: max-height 200ms, height 200ms, min-height 200ms;
@ -539,30 +537,31 @@ export const ActivityItem = styled.div`
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
display: flex;
`;
export const ActivityItemHeader = styled.div`
display: flex;
flex-direction: column;
padding-left: 8px;
`;
export const ActivityItemHeaderUser = styled(TaskAssignee)`
margin-right: 4px;
`;
export const ActivityItemHeaderUser = styled(TaskAssignee)``;
export const ActivityItemHeaderTitle = styled.div`
margin-left: 4px;
line-height: 18px;
display: flex;
align-items: center;
color: ${props => props.theme.colors.text.primary};
padding-bottom: 2px;
`;
export const ActivityItemHeaderTitleName = styled.span`
color: rgba(${props => props.theme.colors.text.primary});
font-weight: 500;
padding-right: 2px;
`;
export const ActivityItemTimestamp = styled.span<{ margin: number }>`
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;
`;
@ -575,15 +574,15 @@ export const ActivityItemComment = styled.div`
border-radius: 3px;
${mixin.boxShadowCard}
position: relative;
color: rgba(${props => props.theme.colors.text.primary});
color: ${props => props.theme.colors.text.primary};
padding: 8px 12px;
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`
margin-left: 2px;
color: rgba(${props => props.theme.colors.text.primary});
color: ${props => props.theme.colors.text.primary};
`;
export const ViewRawButton = styled.button`
@ -594,9 +593,9 @@ export const ViewRawButton = styled.button`
right: 4px;
bottom: -24px;
cursor: pointer;
color: rgba(${props => props.theme.colors.text.primary}, 0.25);
color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.25)};
&:hover {
color: rgba(${props => props.theme.colors.text.primary});
color: ${props => props.theme.colors.text.primary};
}
`;

View File

@ -17,10 +17,14 @@ import dark from 'shared/utils/editorTheme';
import styled from 'styled-components';
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
import moment from 'moment';
import dayjs from 'dayjs';
import ActivityMessage from './ActivityMessage';
import Task from 'shared/icons/Task';
import {
ActivityItemHeader,
ActivityItemTimestamp,
ActivityItem,
TaskDetailLabel,
CommentContainer,
MetaDetailContent,
@ -67,9 +71,13 @@ import {
CommentInnerWrapper,
ActivitySection,
TaskDetailsEditor,
ActivityItemHeaderUser,
ActivityItemHeaderTitle,
ActivityItemHeaderTitleName,
} from './Styles';
import Checklist, { ChecklistItem, ChecklistItems } from '../Checklist';
import onDragEnd from './onDragEnd';
import TaskAssignee from 'shared/components/TaskAssignee';
const ChecklistContainer = styled.div``;
@ -182,7 +190,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
}}
>
{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>
)}
@ -425,7 +433,36 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
<TabBarSection>
<TabBarItem>Activity</TabBarItem>
</TabBarSection>
<ActivitySection />
<ActivitySection>
{task.activity &&
task.activity.map(activity => (
<ActivityItem>
<ActivityItemHeaderUser
size={32}
member={{
id: activity.causedBy.id,
fullName: activity.causedBy.fullName,
profileIcon: activity.causedBy.profileIcon
? activity.causedBy.profileIcon
: {
url: null,
initials: activity.causedBy.fullName.charAt(0),
bgColor: '#fff',
},
}}
/>
<ActivityItemHeader>
<ActivityItemHeaderTitle>
<ActivityItemHeaderTitleName>{activity.causedBy.fullName}</ActivityItemHeaderTitleName>
<ActivityMessage type={activity.type} data={activity.data} />
</ActivityItemHeaderTitle>
<ActivityItemTimestamp margin={0}>
{dayjs(activity.createdAt).format('MMM D [at] h:mm A')}
</ActivityItemTimestamp>
</ActivityItemHeader>
</ActivityItem>
))}
</ActivitySection>
</InnerContentContainer>
<CommentContainer>
{me && (

View File

@ -24,7 +24,7 @@ const Textarea = styled(TextareaAutosize)`
font-size: 20px;
padding: 3px 10px 3px 8px;
&: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};
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`
@ -28,9 +29,9 @@ export const NavbarHeader = styled.header`
display: flex;
align-items: center;
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);
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`
color: rgb(94, 108, 132);
@ -124,7 +125,7 @@ export const ProjectTabs = styled.div`
export const ProjectTab = styled(NavLink)`
font-size: 80%;
color: rgba(${props => props.theme.colors.text.primary});
color: ${props => props.theme.colors.text.primary};
font-size: 15px;
cursor: pointer;
display: flex;
@ -141,22 +142,22 @@ export const ProjectTab = styled(NavLink)`
}
&:hover {
box-shadow: inset 0 -2px rgba(${props => props.theme.colors.text.secondary});
color: rgba(${props => props.theme.colors.text.secondary});
box-shadow: inset 0 -2px ${props => props.theme.colors.text.secondary};
color: ${props => props.theme.colors.text.secondary};
}
&.active {
box-shadow: inset 0 -2px rgba(${props => props.theme.colors.secondary});
color: rgba(${props => props.theme.colors.secondary});
box-shadow: inset 0 -2px ${props => props.theme.colors.secondary};
color: ${props => props.theme.colors.secondary};
}
&.active:hover {
box-shadow: inset 0 -2px rgba(${props => props.theme.colors.secondary});
color: rgba(${props => props.theme.colors.secondary});
box-shadow: inset 0 -2px ${props => props.theme.colors.secondary};
color: ${props => props.theme.colors.secondary};
}
`;
export const ProjectName = styled.h1`
color: rgba(${props => props.theme.colors.text.primary});
color: ${props => props.theme.colors.text.primary};
font-weight: 600;
font-size: 20px;
padding: 3px 10px 3px 8px;
@ -185,7 +186,7 @@ export const ProjectNameTextarea = styled(TextareaAutosize)`
font-size: 20px;
padding: 3px 10px 3px 8px;
&: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;
cursor: pointer;
&:hover {
background: rgb(115, 103, 240);
background: ${props => props.theme.colors.primary};
}
`;
@ -227,7 +228,7 @@ export const ProjectSettingsButton = styled.button`
justify-content: center;
cursor: pointer;
&: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`
width: 1px;
background: rgba(${props => props.theme.colors.border});
background: ${props => props.theme.colors.border};
height: 34px;
margin: 0 20px;
`;
@ -260,11 +261,11 @@ export const LogoContainer = styled(Link)`
export const TaskcafeTitle = styled.h2`
margin-left: 5px;
color: rgba(${props => props.theme.colors.text.primary});
color: ${props => props.theme.colors.text.primary};
font-size: 20px;
`;
export const TaskcafeLogo = styled(Taskcafe)`
fill: rgba(${props => props.theme.colors.text.primary});
stroke: rgba(${props => props.theme.colors.text.primary});
fill: ${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 BaseStyles from 'App/BaseStyles';
import { action } from '@storybook/addon-actions';
import DropdownMenu from 'shared/components/DropdownMenu';
import TopNavbar from '.';
import theme from '../../../App/ThemeStyles';
export default {
component: TopNavbar,
@ -15,7 +15,7 @@ export default {
backgrounds: [
{ name: 'white', value: '#ffffff' },
{ 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 { Home, Star, Bell, AngleDown, BarChart, CheckCircle } from 'shared/icons';
import { Home, Star, Bell, AngleDown, BarChart, CheckCircle, ListUnordered } from 'shared/icons';
import styled from 'styled-components';
import ProfileIcon from 'shared/components/ProfileIcon';
import { usePopup } from 'shared/components/PopupMenu';
import { RoleCode } from 'shared/generated/graphql';
import NOOP from 'shared/utils/noop';
import { useHistory } from 'react-router';
import {
TaskcafeLogo,
TaskcafeTitle,
@ -173,8 +174,11 @@ type NavBarProps = {
user: TaskUser | null;
onOpenSettings: ($target: React.RefObject<HTMLElement>) => void;
projectMembers?: Array<TaskUser> | null;
projectInvitedMembers?: Array<InvitedUser> | null;
onRemoveFromBoard?: (userID: string) => void;
onMemberProfile?: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
onInvitedMemberProfile?: ($targetRef: React.RefObject<HTMLElement>, email: string) => void;
};
const NavBar: React.FC<NavBarProps> = ({
@ -184,10 +188,12 @@ const NavBar: React.FC<NavBarProps> = ({
onChangeProjectOwner,
currentTab,
onMemberProfile,
onInvitedMemberProfile,
canEditProjectName = false,
onOpenProjectFinder,
onFavorite,
onSetTab,
projectInvitedMembers,
onChangeRole,
name,
onRemoveFromBoard,
@ -204,6 +210,7 @@ const NavBar: React.FC<NavBarProps> = ({
onProfileClick($target);
}
};
const history = useHistory();
const { showPopup } = usePopup();
return (
<NavbarWrapper>
@ -245,19 +252,38 @@ const NavBar: React.FC<NavBarProps> = ({
<TaskcafeTitle>Taskcafé</TaskcafeTitle>
</LogoContainer>
<GlobalActions>
{projectMembers && onMemberProfile && (
{projectMembers && projectInvitedMembers && onMemberProfile && onInvitedMemberProfile && (
<>
<ProjectMembers>
{projectMembers.map((member, idx) => (
<ProjectMember
showRoleIcons
zIndex={projectMembers.length - idx}
zIndex={projectMembers.length - idx + projectInvitedMembers.length}
key={member.id}
size={28}
member={member}
onMemberProfile={onMemberProfile}
/>
))}
{projectInvitedMembers.map((member, idx) => (
<ProjectMember
showRoleIcons
zIndex={projectInvitedMembers.length - idx}
key={member.email}
size={28}
invited
member={{
id: member.email,
fullName: member.email,
profileIcon: {
url: null,
initials: member.email.charAt(0),
bgColor: '#fff',
},
}}
onMemberProfile={onInvitedMemberProfile}
/>
))}
{canInviteUser && (
<InviteButton
onClick={$target => {
@ -283,6 +309,9 @@ const NavBar: React.FC<NavBarProps> = ({
<IconContainer disabled onClick={NOOP}>
<CheckCircle width={20} height={20} />
</IconContainer>
<IconContainer disabled onClick={NOOP}>
<ListUnordered width={20} height={20} />
</IconContainer>
<IconContainer disabled onClick={onNotificationClick}>
<Bell color="#c2c6dc" size={20} />
</IconContainer>

View File

@ -113,6 +113,14 @@ export type UserAccount = {
member: MemberList;
};
export type InvitedUserAccount = {
__typename?: 'InvitedUserAccount';
id: Scalars['ID'];
email: Scalars['String'];
invitedOn: Scalars['Time'];
member: MemberList;
};
export type Team = {
__typename?: 'Team';
id: Scalars['ID'];
@ -121,14 +129,21 @@ export type Team = {
members: Array<Member>;
};
export type InvitedMember = {
__typename?: 'InvitedMember';
email: Scalars['String'];
invitedOn: Scalars['Time'];
};
export type Project = {
__typename?: 'Project';
id: Scalars['ID'];
createdAt: Scalars['Time'];
name: Scalars['String'];
team: Team;
team?: Maybe<Team>;
taskGroups: Array<TaskGroup>;
members: Array<Member>;
invitedMembers: Array<InvitedMember>;
labels: Array<ProjectLabel>;
};
@ -153,6 +168,41 @@ export type TaskBadges = {
checklist?: Maybe<ChecklistBadge>;
};
export type CausedBy = {
__typename?: 'CausedBy';
id: Scalars['ID'];
fullName: Scalars['String'];
profileIcon?: Maybe<ProfileIcon>;
};
export type TaskActivityData = {
__typename?: 'TaskActivityData';
name: Scalars['String'];
value: Scalars['String'];
};
export enum ActivityType {
TaskAdded = 'TASK_ADDED',
TaskMoved = 'TASK_MOVED',
TaskMarkedComplete = 'TASK_MARKED_COMPLETE',
TaskMarkedIncomplete = 'TASK_MARKED_INCOMPLETE',
TaskDueDateChanged = 'TASK_DUE_DATE_CHANGED',
TaskDueDateAdded = 'TASK_DUE_DATE_ADDED',
TaskDueDateRemoved = 'TASK_DUE_DATE_REMOVED',
TaskChecklistChanged = 'TASK_CHECKLIST_CHANGED',
TaskChecklistAdded = 'TASK_CHECKLIST_ADDED',
TaskChecklistRemoved = 'TASK_CHECKLIST_REMOVED'
}
export type TaskActivity = {
__typename?: 'TaskActivity';
id: Scalars['ID'];
type: ActivityType;
data: Array<TaskActivityData>;
causedBy: CausedBy;
createdAt: Scalars['Time'];
};
export type Task = {
__typename?: 'Task';
id: Scalars['ID'];
@ -168,6 +218,7 @@ export type Task = {
labels: Array<TaskLabel>;
checklists: Array<TaskChecklist>;
badges: TaskBadges;
activity: Array<TaskActivity>;
};
export type Organization = {
@ -194,6 +245,11 @@ export type TaskChecklist = {
items: Array<TaskChecklistItem>;
};
export enum ShareStatus {
Invited = 'INVITED',
Joined = 'JOINED'
}
export enum RoleLevel {
Admin = 'ADMIN',
Member = 'MEMBER'
@ -209,7 +265,10 @@ export enum ObjectType {
Org = 'ORG',
Team = 'TEAM',
Project = 'PROJECT',
Task = 'TASK'
Task = 'TASK',
TaskGroup = 'TASK_GROUP',
TaskChecklist = 'TASK_CHECKLIST',
TaskChecklistItem = 'TASK_CHECKLIST_ITEM'
}
export type Query = {
@ -218,11 +277,13 @@ export type Query = {
findTask: Task;
findTeam: Team;
findUser: UserAccount;
invitedUsers: Array<InvitedUserAccount>;
labelColors: Array<LabelColor>;
me: MePayload;
notifications: Array<Notification>;
organizations: Array<Organization>;
projects: Array<Project>;
searchMembers: Array<MemberSearchResult>;
taskGroups: Array<TaskGroup>;
teams: Array<Team>;
users: Array<UserAccount>;
@ -253,6 +314,11 @@ export type QueryProjectsArgs = {
input?: Maybe<ProjectsFilter>;
};
export type QuerySearchMembersArgs = {
input: MemberSearchFilter;
};
export type Mutation = {
__typename?: 'Mutation';
addTaskLabel: Task;
@ -260,7 +326,6 @@ export type Mutation = {
clearProfileAvatar: UserAccount;
createProject: Project;
createProjectLabel: ProjectLabel;
createProjectMember: CreateProjectMemberPayload;
createRefreshToken: RefreshToken;
createTask: Task;
createTaskChecklist: TaskChecklist;
@ -269,6 +334,8 @@ export type Mutation = {
createTeam: Team;
createTeamMember: CreateTeamMemberPayload;
createUserAccount: UserAccount;
deleteInvitedProjectMember: DeleteInvitedProjectMemberPayload;
deleteInvitedUserAccount: DeleteInvitedUserAccountPayload;
deleteProject: DeleteProjectPayload;
deleteProjectLabel: ProjectLabel;
deleteProjectMember: DeleteProjectMemberPayload;
@ -281,6 +348,7 @@ export type Mutation = {
deleteTeamMember: DeleteTeamMemberPayload;
deleteUserAccount: DeleteUserAccountPayload;
duplicateTaskGroup: DuplicateTaskGroupPayload;
inviteProjectMembers: InviteProjectMembersPayload;
logoutUser: Scalars['Boolean'];
removeTaskLabel: Task;
setTaskChecklistItemComplete: TaskChecklistItem;
@ -330,11 +398,6 @@ export type MutationCreateProjectLabelArgs = {
};
export type MutationCreateProjectMemberArgs = {
input: CreateProjectMember;
};
export type MutationCreateRefreshTokenArgs = {
input: NewRefreshToken;
};
@ -375,6 +438,16 @@ export type MutationCreateUserAccountArgs = {
};
export type MutationDeleteInvitedProjectMemberArgs = {
input: DeleteInvitedProjectMember;
};
export type MutationDeleteInvitedUserAccountArgs = {
input: DeleteInvitedUserAccount;
};
export type MutationDeleteProjectArgs = {
input: DeleteProject;
};
@ -435,6 +508,11 @@ export type MutationDuplicateTaskGroupArgs = {
};
export type MutationInviteProjectMembersArgs = {
input: InviteProjectMembers;
};
export type MutationLogoutUserArgs = {
input: LogoutUser;
};
@ -588,7 +666,7 @@ export type ProjectsFilter = {
};
export type FindUser = {
userId: Scalars['String'];
userID: Scalars['UUID'];
};
export type FindProject = {
@ -640,8 +718,7 @@ export type Notification = {
};
export type NewProject = {
userID: Scalars['UUID'];
teamID: Scalars['UUID'];
teamID?: Maybe<Scalars['UUID']>;
name: Scalars['String'];
};
@ -686,15 +763,32 @@ export type UpdateProjectLabelColor = {
labelColorID: Scalars['UUID'];
};
export type CreateProjectMember = {
export type DeleteInvitedProjectMember = {
projectID: Scalars['UUID'];
userID: Scalars['UUID'];
email: Scalars['String'];
};
export type CreateProjectMemberPayload = {
__typename?: 'CreateProjectMemberPayload';
export type DeleteInvitedProjectMemberPayload = {
__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'];
member: Member;
projectID: Scalars['UUID'];
members: Array<Member>;
invitedMembers: Array<InvitedMember>;
};
export type DeleteProjectMember = {
@ -722,7 +816,7 @@ export type UpdateProjectMemberRolePayload = {
};
export type NewTask = {
taskGroupID: Scalars['String'];
taskGroupID: Scalars['UUID'];
name: Scalars['String'];
position: Scalars['Float'];
};
@ -765,34 +859,34 @@ export type NewTaskLocation = {
};
export type DeleteTaskInput = {
taskID: Scalars['String'];
taskID: Scalars['UUID'];
};
export type DeleteTaskPayload = {
__typename?: 'DeleteTaskPayload';
taskID: Scalars['String'];
taskID: Scalars['UUID'];
};
export type UpdateTaskName = {
taskID: Scalars['String'];
taskID: Scalars['UUID'];
name: Scalars['String'];
};
export type UpdateTaskChecklistItemLocation = {
checklistID: Scalars['UUID'];
checklistItemID: Scalars['UUID'];
taskChecklistID: Scalars['UUID'];
taskChecklistItemID: Scalars['UUID'];
position: Scalars['Float'];
};
export type UpdateTaskChecklistItemLocationPayload = {
__typename?: 'UpdateTaskChecklistItemLocationPayload';
checklistID: Scalars['UUID'];
taskChecklistID: Scalars['UUID'];
prevChecklistID: Scalars['UUID'];
checklistItem: TaskChecklistItem;
};
export type UpdateTaskChecklistLocation = {
checklistID: Scalars['UUID'];
taskChecklistID: Scalars['UUID'];
position: Scalars['Float'];
};
@ -908,7 +1002,7 @@ export type DeleteTaskGroupPayload = {
};
export type NewTaskGroup = {
projectID: Scalars['String'];
projectID: Scalars['UUID'];
name: Scalars['String'];
position: Scalars['Float'];
};
@ -919,6 +1013,7 @@ export type AddTaskLabelInput = {
};
export type RemoveTaskLabelInput = {
taskID: Scalars['UUID'];
taskLabelID: Scalars['UUID'];
};
@ -986,6 +1081,28 @@ export type UpdateTeamMemberRolePayload = {
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'];
id: Scalars['String'];
user?: Maybe<UserAccount>;
status: ShareStatus;
};
export type UpdateUserInfoPayload = {
__typename?: 'UpdateUserInfoPayload';
user: UserAccount;
@ -1020,7 +1137,7 @@ export type UpdateUserRolePayload = {
};
export type NewRefreshToken = {
userId: Scalars['String'];
userID: Scalars['UUID'];
};
export type NewUserAccount = {
@ -1033,7 +1150,7 @@ export type NewUserAccount = {
};
export type LogoutUser = {
userID: Scalars['String'];
userID: Scalars['UUID'];
};
export type DeleteUserAccount = {
@ -1081,8 +1198,7 @@ export type ClearProfileAvatarMutation = (
);
export type CreateProjectMutationVariables = {
teamID: Scalars['UUID'];
userID: Scalars['UUID'];
teamID?: Maybe<Scalars['UUID']>;
name: Scalars['String'];
};
@ -1092,10 +1208,10 @@ export type CreateProjectMutation = (
& { createProject: (
{ __typename?: 'Project' }
& Pick<Project, 'id' | 'name'>
& { team: (
& { team?: Maybe<(
{ __typename?: 'Team' }
& Pick<Team, 'id' | 'name'>
) }
)> }
) }
);
@ -1119,7 +1235,7 @@ export type CreateProjectLabelMutation = (
);
export type CreateTaskGroupMutationVariables = {
projectID: Scalars['String'];
projectID: Scalars['UUID'];
name: Scalars['String'];
position: Scalars['Float'];
};
@ -1147,7 +1263,7 @@ export type DeleteProjectLabelMutation = (
);
export type DeleteTaskMutationVariables = {
taskID: Scalars['String'];
taskID: Scalars['UUID'];
};
@ -1190,10 +1306,10 @@ export type FindProjectQuery = (
& { findProject: (
{ __typename?: 'Project' }
& Pick<Project, 'name'>
& { team: (
& { team?: Maybe<(
{ __typename?: 'Team' }
& Pick<Team, 'id'>
), members: Array<(
)>, members: Array<(
{ __typename?: 'Member' }
& Pick<Member, 'id' | 'fullName' | 'username'>
& { role: (
@ -1203,6 +1319,9 @@ export type FindProjectQuery = (
{ __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
) }
)>, invitedMembers: Array<(
{ __typename?: 'InvitedMember' }
& Pick<InvitedMember, 'email' | 'invitedOn'>
)>, labels: Array<(
{ __typename?: 'ProjectLabel' }
& Pick<ProjectLabel, 'id' | 'createdDate' | 'name'>
@ -1265,7 +1384,21 @@ export type FindTaskQuery = (
& { taskGroup: (
{ __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'id' | 'name'>
), badges: (
), activity: Array<(
{ __typename?: 'TaskActivity' }
& Pick<TaskActivity, 'id' | 'type' | 'createdAt'>
& { causedBy: (
{ __typename?: 'CausedBy' }
& Pick<CausedBy, 'id' | 'fullName'>
& { profileIcon?: Maybe<(
{ __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'initials' | 'bgColor' | 'url'>
)> }
), data: Array<(
{ __typename?: 'TaskActivityData' }
& Pick<TaskActivityData, 'name' | 'value'>
)> }
)>, badges: (
{ __typename?: 'TaskBadges' }
& { checklist?: Maybe<(
{ __typename?: 'ChecklistBadge' }
@ -1357,10 +1490,10 @@ export type GetProjectsQuery = (
)>, projects: Array<(
{ __typename?: 'Project' }
& Pick<Project, 'id' | 'name'>
& { team: (
& { team?: Maybe<(
{ __typename?: 'Team' }
& Pick<Team, 'id' | 'name'>
) }
)> }
)> }
);
@ -1388,31 +1521,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 = {
projectID: Scalars['UUID'];
};
@ -1430,6 +1538,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 = {
projectID: Scalars['UUID'];
userID: Scalars['UUID'];
@ -1448,6 +1573,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 = {
projectID: Scalars['UUID'];
userID: Scalars['UUID'];
@ -1472,7 +1625,7 @@ export type UpdateProjectMemberRoleMutation = (
);
export type CreateTaskMutationVariables = {
taskGroupID: Scalars['String'];
taskGroupID: Scalars['UUID'];
name: Scalars['String'];
position: Scalars['Float'];
};
@ -1583,8 +1736,8 @@ export type SetTaskCompleteMutation = (
);
export type UpdateTaskChecklistItemLocationMutationVariables = {
checklistID: Scalars['UUID'];
checklistItemID: Scalars['UUID'];
taskChecklistID: Scalars['UUID'];
taskChecklistItemID: Scalars['UUID'];
position: Scalars['Float'];
};
@ -1593,7 +1746,7 @@ export type UpdateTaskChecklistItemLocationMutation = (
{ __typename?: 'Mutation' }
& { updateTaskChecklistItemLocation: (
{ __typename?: 'UpdateTaskChecklistItemLocationPayload' }
& Pick<UpdateTaskChecklistItemLocationPayload, 'checklistID' | 'prevChecklistID'>
& Pick<UpdateTaskChecklistItemLocationPayload, 'taskChecklistID' | 'prevChecklistID'>
& { checklistItem: (
{ __typename?: 'TaskChecklistItem' }
& Pick<TaskChecklistItem, 'id' | 'taskChecklistID' | 'position'>
@ -1616,7 +1769,7 @@ export type UpdateTaskChecklistItemNameMutation = (
);
export type UpdateTaskChecklistLocationMutationVariables = {
checklistID: Scalars['UUID'];
taskChecklistID: Scalars['UUID'];
position: Scalars['Float'];
};
@ -1833,10 +1986,10 @@ export type GetTeamQuery = (
), projects: Array<(
{ __typename?: 'Project' }
& Pick<Project, 'id' | 'name'>
& { team: (
& { team?: Maybe<(
{ __typename?: 'Team' }
& Pick<Team, 'id' | 'name'>
) }
)> }
)>, users: Array<(
{ __typename?: 'UserAccount' }
& Pick<UserAccount, 'id' | 'email' | 'fullName' | 'username'>
@ -2072,7 +2225,7 @@ export type UpdateTaskLocationMutation = (
);
export type UpdateTaskNameMutationVariables = {
taskID: Scalars['String'];
taskID: Scalars['UUID'];
name: Scalars['String'];
};
@ -2128,6 +2281,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 = {
userID: Scalars['UUID'];
newOwnerID?: Maybe<Scalars['UUID']>;
@ -2209,7 +2378,10 @@ export type UsersQueryVariables = {};
export type UsersQuery = (
{ __typename?: 'Query' }
& { users: Array<(
& { invitedUsers: Array<(
{ __typename?: 'InvitedUserAccount' }
& Pick<InvitedUserAccount, 'id' | 'email' | 'invitedOn'>
)>, users: Array<(
{ __typename?: 'UserAccount' }
& Pick<UserAccount, 'id' | 'email' | 'fullName' | 'username'>
& { role: (
@ -2361,8 +2533,8 @@ export type ClearProfileAvatarMutationHookResult = ReturnType<typeof useClearPro
export type ClearProfileAvatarMutationResult = ApolloReactCommon.MutationResult<ClearProfileAvatarMutation>;
export type ClearProfileAvatarMutationOptions = ApolloReactCommon.BaseMutationOptions<ClearProfileAvatarMutation, ClearProfileAvatarMutationVariables>;
export const CreateProjectDocument = gql`
mutation createProject($teamID: UUID!, $userID: UUID!, $name: String!) {
createProject(input: {teamID: $teamID, userID: $userID, name: $name}) {
mutation createProject($teamID: UUID, $name: String!) {
createProject(input: {teamID: $teamID, name: $name}) {
id
name
team {
@ -2388,7 +2560,6 @@ export type CreateProjectMutationFn = ApolloReactCommon.MutationFunction<CreateP
* const [createProjectMutation, { data, loading, error }] = useCreateProjectMutation({
* variables: {
* teamID: // value for 'teamID'
* userID: // value for 'userID'
* name: // value for 'name'
* },
* });
@ -2442,7 +2613,7 @@ export type CreateProjectLabelMutationHookResult = ReturnType<typeof useCreatePr
export type CreateProjectLabelMutationResult = ApolloReactCommon.MutationResult<CreateProjectLabelMutation>;
export type CreateProjectLabelMutationOptions = ApolloReactCommon.BaseMutationOptions<CreateProjectLabelMutation, CreateProjectLabelMutationVariables>;
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}) {
id
name
@ -2510,7 +2681,7 @@ export type DeleteProjectLabelMutationHookResult = ReturnType<typeof useDeletePr
export type DeleteProjectLabelMutationResult = ApolloReactCommon.MutationResult<DeleteProjectLabelMutation>;
export type DeleteProjectLabelMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteProjectLabelMutation, DeleteProjectLabelMutationVariables>;
export const DeleteTaskDocument = gql`
mutation deleteTask($taskID: String!) {
mutation deleteTask($taskID: UUID!) {
deleteTask(input: {taskID: $taskID}) {
taskID
}
@ -2602,6 +2773,10 @@ export const FindProjectDocument = gql`
bgColor
}
}
invitedMembers {
email
invitedOn
}
labels {
id
createdDate
@ -2704,6 +2879,24 @@ export const FindTaskDocument = gql`
id
name
}
activity {
id
type
causedBy {
id
fullName
profileIcon {
initials
bgColor
url
}
}
createdAt
data {
name
value
}
}
badges {
checklist {
total
@ -2883,53 +3076,6 @@ export function useMeLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptio
export type MeQueryHookResult = ReturnType<typeof useMeQuery>;
export type MeLazyQueryHookResult = ReturnType<typeof useMeLazyQuery>;
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`
mutation deleteProject($projectID: UUID!) {
deleteProject(input: {projectID: $projectID}) {
@ -2965,6 +3111,41 @@ export function useDeleteProjectMutation(baseOptions?: ApolloReactHooks.Mutation
export type DeleteProjectMutationHookResult = ReturnType<typeof useDeleteProjectMutation>;
export type DeleteProjectMutationResult = ApolloReactCommon.MutationResult<DeleteProjectMutation>;
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`
mutation deleteProjectMember($projectID: UUID!, $userID: UUID!) {
deleteProjectMember(input: {projectID: $projectID, userID: $userID}) {
@ -3002,6 +3183,57 @@ export function useDeleteProjectMemberMutation(baseOptions?: ApolloReactHooks.Mu
export type DeleteProjectMemberMutationHookResult = ReturnType<typeof useDeleteProjectMemberMutation>;
export type DeleteProjectMemberMutationResult = ApolloReactCommon.MutationResult<DeleteProjectMemberMutation>;
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`
mutation updateProjectMemberRole($projectID: UUID!, $userID: UUID!, $roleCode: RoleCode!) {
updateProjectMemberRole(input: {projectID: $projectID, userID: $userID, roleCode: $roleCode}) {
@ -3044,7 +3276,7 @@ export type UpdateProjectMemberRoleMutationHookResult = ReturnType<typeof useUpd
export type UpdateProjectMemberRoleMutationResult = ApolloReactCommon.MutationResult<UpdateProjectMemberRoleMutation>;
export type UpdateProjectMemberRoleMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateProjectMemberRoleMutation, UpdateProjectMemberRoleMutationVariables>;
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}) {
...TaskFields
}
@ -3297,9 +3529,9 @@ export type SetTaskCompleteMutationHookResult = ReturnType<typeof useSetTaskComp
export type SetTaskCompleteMutationResult = ApolloReactCommon.MutationResult<SetTaskCompleteMutation>;
export type SetTaskCompleteMutationOptions = ApolloReactCommon.BaseMutationOptions<SetTaskCompleteMutation, SetTaskCompleteMutationVariables>;
export const UpdateTaskChecklistItemLocationDocument = gql`
mutation updateTaskChecklistItemLocation($checklistID: UUID!, $checklistItemID: UUID!, $position: Float!) {
updateTaskChecklistItemLocation(input: {checklistID: $checklistID, checklistItemID: $checklistItemID, position: $position}) {
checklistID
mutation updateTaskChecklistItemLocation($taskChecklistID: UUID!, $taskChecklistItemID: UUID!, $position: Float!) {
updateTaskChecklistItemLocation(input: {taskChecklistID: $taskChecklistID, taskChecklistItemID: $taskChecklistItemID, position: $position}) {
taskChecklistID
prevChecklistID
checklistItem {
id
@ -3324,8 +3556,8 @@ export type UpdateTaskChecklistItemLocationMutationFn = ApolloReactCommon.Mutati
* @example
* const [updateTaskChecklistItemLocationMutation, { data, loading, error }] = useUpdateTaskChecklistItemLocationMutation({
* variables: {
* checklistID: // value for 'checklistID'
* checklistItemID: // value for 'checklistItemID'
* taskChecklistID: // value for 'taskChecklistID'
* taskChecklistItemID: // value for 'taskChecklistItemID'
* position: // value for 'position'
* },
* });
@ -3371,8 +3603,8 @@ export type UpdateTaskChecklistItemNameMutationHookResult = ReturnType<typeof us
export type UpdateTaskChecklistItemNameMutationResult = ApolloReactCommon.MutationResult<UpdateTaskChecklistItemNameMutation>;
export type UpdateTaskChecklistItemNameMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateTaskChecklistItemNameMutation, UpdateTaskChecklistItemNameMutationVariables>;
export const UpdateTaskChecklistLocationDocument = gql`
mutation updateTaskChecklistLocation($checklistID: UUID!, $position: Float!) {
updateTaskChecklistLocation(input: {checklistID: $checklistID, position: $position}) {
mutation updateTaskChecklistLocation($taskChecklistID: UUID!, $position: Float!) {
updateTaskChecklistLocation(input: {taskChecklistID: $taskChecklistID, position: $position}) {
checklist {
id
position
@ -3395,7 +3627,7 @@ export type UpdateTaskChecklistLocationMutationFn = ApolloReactCommon.MutationFu
* @example
* const [updateTaskChecklistLocationMutation, { data, loading, error }] = useUpdateTaskChecklistLocationMutation({
* variables: {
* checklistID: // value for 'checklistID'
* taskChecklistID: // value for 'taskChecklistID'
* position: // value for 'position'
* },
* });
@ -4275,7 +4507,7 @@ export type UpdateTaskLocationMutationHookResult = ReturnType<typeof useUpdateTa
export type UpdateTaskLocationMutationResult = ApolloReactCommon.MutationResult<UpdateTaskLocationMutation>;
export type UpdateTaskLocationMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateTaskLocationMutation, UpdateTaskLocationMutationVariables>;
export const UpdateTaskNameDocument = gql`
mutation updateTaskName($taskID: String!, $name: String!) {
mutation updateTaskName($taskID: UUID!, $name: String!) {
updateTaskName(input: {taskID: $taskID, name: $name}) {
id
name
@ -4380,6 +4612,40 @@ export function useCreateUserAccountMutation(baseOptions?: ApolloReactHooks.Muta
export type CreateUserAccountMutationHookResult = ReturnType<typeof useCreateUserAccountMutation>;
export type CreateUserAccountMutationResult = ApolloReactCommon.MutationResult<CreateUserAccountMutation>;
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`
mutation deleteUserAccount($userID: UUID!, $newOwnerID: UUID) {
deleteUserAccount(input: {userID: $userID, newOwnerID: $newOwnerID}) {
@ -4533,6 +4799,11 @@ export type UpdateUserRoleMutationResult = ApolloReactCommon.MutationResult<Upda
export type UpdateUserRoleMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateUserRoleMutation, UpdateUserRoleMutationVariables>;
export const UsersDocument = gql`
query users {
invitedUsers {
id
email
invitedOn
}
users {
id
email
@ -4594,4 +4865,4 @@ export function useUsersLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOp
}
export type UsersQueryHookResult = ReturnType<typeof useUsersQuery>;
export type UsersLazyQueryHookResult = ReturnType<typeof useUsersLazyQuery>;
export type UsersQueryResult = ApolloReactCommon.QueryResult<UsersQuery, UsersQueryVariables>;
export type UsersQueryResult = ApolloReactCommon.QueryResult<UsersQuery, UsersQueryVariables>;

View File

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

View File

@ -1,4 +1,4 @@
mutation createTaskGroup( $projectID: String!, $name: String!, $position: Float! ) {
mutation createTaskGroup( $projectID: UUID!, $name: String!, $position: Float! ) {
createTaskGroup(
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 }) {
taskID
}

View File

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

View File

@ -10,6 +10,24 @@ query findTask($taskID: UUID!) {
id
name
}
activity {
id
type
causedBy {
id
fullName
profileIcon {
initials
bgColor
url
}
}
createdAt
data {
name
value
}
}
badges {
checklist {
total

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