Compare commits
	
		
			2 Commits
		
	
	
		
			feat/publi
			...
			refactor/c
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					2de48e288b | ||
| 
						 | 
					7b6624ecc3 | 
@@ -13,6 +13,7 @@
 | 
				
			|||||||
    "@types/jwt-decode": "^2.2.1",
 | 
					    "@types/jwt-decode": "^2.2.1",
 | 
				
			||||||
    "@types/lodash": "^4.14.149",
 | 
					    "@types/lodash": "^4.14.149",
 | 
				
			||||||
    "@types/node": "^12.0.0",
 | 
					    "@types/node": "^12.0.0",
 | 
				
			||||||
 | 
					    "@types/query-string": "^6.3.0",
 | 
				
			||||||
    "@types/react": "^16.9.21",
 | 
					    "@types/react": "^16.9.21",
 | 
				
			||||||
    "@types/react-beautiful-dnd": "^12.1.1",
 | 
					    "@types/react-beautiful-dnd": "^12.1.1",
 | 
				
			||||||
    "@types/react-datepicker": "^2.11.0",
 | 
					    "@types/react-datepicker": "^2.11.0",
 | 
				
			||||||
@@ -41,6 +42,7 @@
 | 
				
			|||||||
    "jwt-decode": "^2.2.0",
 | 
					    "jwt-decode": "^2.2.0",
 | 
				
			||||||
    "lodash": "^4.17.20",
 | 
					    "lodash": "^4.17.20",
 | 
				
			||||||
    "prop-types": "^15.7.2",
 | 
					    "prop-types": "^15.7.2",
 | 
				
			||||||
 | 
					    "query-string": "^6.13.7",
 | 
				
			||||||
    "react": "^16.12.0",
 | 
					    "react": "^16.12.0",
 | 
				
			||||||
    "react-autosize-textarea": "^7.0.0",
 | 
					    "react-autosize-textarea": "^7.0.0",
 | 
				
			||||||
    "react-beautiful-dnd": "^13.0.0",
 | 
					    "react-beautiful-dnd": "^13.0.0",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -82,7 +82,7 @@ const AddUserInput = styled(Input)`
 | 
				
			|||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const InputError = styled.span`
 | 
					const InputError = styled.span`
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.danger});
 | 
					  color: ${props => props.theme.colors.danger};
 | 
				
			||||||
  font-size: 12px;
 | 
					  font-size: 12px;
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,17 +1,20 @@
 | 
				
			|||||||
import React from 'react';
 | 
					import React, { useEffect, useState } from 'react';
 | 
				
			||||||
import { Switch, Route } from 'react-router-dom';
 | 
					import { Switch, Route, useHistory } from 'react-router-dom';
 | 
				
			||||||
import * as H from 'history';
 | 
					import * as H from 'history';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import Dashboard from 'Dashboard';
 | 
					import Dashboard from 'Dashboard';
 | 
				
			||||||
import Admin from 'Admin';
 | 
					import Admin from 'Admin';
 | 
				
			||||||
 | 
					import Confirm from 'Confirm';
 | 
				
			||||||
import Projects from 'Projects';
 | 
					import Projects from 'Projects';
 | 
				
			||||||
import Outline from 'Outline';
 | 
					 | 
				
			||||||
import Project from 'Projects/Project';
 | 
					import Project from 'Projects/Project';
 | 
				
			||||||
import Teams from 'Teams';
 | 
					import Teams from 'Teams';
 | 
				
			||||||
import Login from 'Auth';
 | 
					import Login from 'Auth';
 | 
				
			||||||
import Install from 'Install';
 | 
					import Register from 'Register';
 | 
				
			||||||
import Profile from 'Profile';
 | 
					import Profile from 'Profile';
 | 
				
			||||||
import styled from 'styled-components';
 | 
					import styled from 'styled-components';
 | 
				
			||||||
 | 
					import JwtDecode from 'jwt-decode';
 | 
				
			||||||
 | 
					import { setAccessToken } from 'shared/utils/accessToken';
 | 
				
			||||||
 | 
					import { useCurrentUser } from 'App/context';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const MainContent = styled.div`
 | 
					const MainContent = styled.div`
 | 
				
			||||||
  padding: 0 0 0 0;
 | 
					  padding: 0 0 0 0;
 | 
				
			||||||
@@ -22,6 +25,50 @@ const MainContent = styled.div`
 | 
				
			|||||||
  flex-grow: 1;
 | 
					  flex-grow: 1;
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const AuthorizedRoutes = () => {
 | 
				
			||||||
 | 
					  const history = useHistory();
 | 
				
			||||||
 | 
					  const [loading, setLoading] = useState(true);
 | 
				
			||||||
 | 
					  const { setUser } = useCurrentUser();
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    fetch('/auth/refresh_token', {
 | 
				
			||||||
 | 
					      method: 'POST',
 | 
				
			||||||
 | 
					      credentials: 'include',
 | 
				
			||||||
 | 
					    }).then(async x => {
 | 
				
			||||||
 | 
					      const { status } = x;
 | 
				
			||||||
 | 
					      if (status === 400) {
 | 
				
			||||||
 | 
					        history.replace('/login');
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        const response: RefreshTokenResponse = await x.json();
 | 
				
			||||||
 | 
					        const { accessToken, setup } = response;
 | 
				
			||||||
 | 
					        if (setup) {
 | 
				
			||||||
 | 
					          history.replace(`/register?confirmToken=${setup.confirmToken}`);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          const claims: JWTToken = JwtDecode(accessToken);
 | 
				
			||||||
 | 
					          const currentUser = {
 | 
				
			||||||
 | 
					            id: claims.userId,
 | 
				
			||||||
 | 
					            roles: { org: claims.orgRole, teams: new Map<string, string>(), projects: new Map<string, string>() },
 | 
				
			||||||
 | 
					          };
 | 
				
			||||||
 | 
					          setUser(currentUser);
 | 
				
			||||||
 | 
					          setAccessToken(accessToken);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      setLoading(false);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					  return loading ? null : (
 | 
				
			||||||
 | 
					    <Switch>
 | 
				
			||||||
 | 
					      <MainContent>
 | 
				
			||||||
 | 
					        <Route exact path="/" component={Dashboard} />
 | 
				
			||||||
 | 
					        <Route exact path="/projects" component={Projects} />
 | 
				
			||||||
 | 
					        <Route path="/projects/:projectID" component={Project} />
 | 
				
			||||||
 | 
					        <Route path="/teams/:teamID" component={Teams} />
 | 
				
			||||||
 | 
					        <Route path="/profile" component={Profile} />
 | 
				
			||||||
 | 
					        <Route path="/admin" component={Admin} />
 | 
				
			||||||
 | 
					      </MainContent>
 | 
				
			||||||
 | 
					    </Switch>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type RoutesProps = {
 | 
					type RoutesProps = {
 | 
				
			||||||
  history: H.History;
 | 
					  history: H.History;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@@ -29,16 +76,9 @@ type RoutesProps = {
 | 
				
			|||||||
const Routes: React.FC<RoutesProps> = () => (
 | 
					const Routes: React.FC<RoutesProps> = () => (
 | 
				
			||||||
  <Switch>
 | 
					  <Switch>
 | 
				
			||||||
    <Route exact path="/login" component={Login} />
 | 
					    <Route exact path="/login" component={Login} />
 | 
				
			||||||
    <Route exact path="/install" component={Install} />
 | 
					    <Route exact path="/register" component={Register} />
 | 
				
			||||||
    <MainContent>
 | 
					    <Route exact path="/confirm" component={Confirm} />
 | 
				
			||||||
      <Route exact path="/" component={Dashboard} />
 | 
					    <AuthorizedRoutes />
 | 
				
			||||||
      <Route exact path="/projects" component={Projects} />
 | 
					 | 
				
			||||||
      <Route path="/projects/:projectID" component={Project} />
 | 
					 | 
				
			||||||
      <Route path="/teams/:teamID" component={Teams} />
 | 
					 | 
				
			||||||
      <Route path="/profile" component={Profile} />
 | 
					 | 
				
			||||||
      <Route path="/admin" component={Admin} />
 | 
					 | 
				
			||||||
      <Route path="/outline" component={Outline} />
 | 
					 | 
				
			||||||
    </MainContent>
 | 
					 | 
				
			||||||
  </Switch>
 | 
					  </Switch>
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,26 +1,28 @@
 | 
				
			|||||||
import { DefaultTheme } from 'styled-components';
 | 
					import { DefaultTheme } from 'styled-components';
 | 
				
			||||||
 | 
					import Color from 'color';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const theme: DefaultTheme = {
 | 
					const theme: DefaultTheme = {
 | 
				
			||||||
  borderRadius: {
 | 
					  borderRadius: {
 | 
				
			||||||
    primary: '3px',
 | 
					    primary: '3x',
 | 
				
			||||||
    alternate: '6px',
 | 
					    alternate: '6px',
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  colors: {
 | 
					  colors: {
 | 
				
			||||||
    primary: '115, 103, 240',
 | 
					    multiColors: ['#e362e3', '#7a6ff0', '#37c5ab', '#aa62e3', '#e8384f'],
 | 
				
			||||||
    secondary: '216, 93, 216',
 | 
					    primary: 'rgb(115, 103, 240)',
 | 
				
			||||||
    alternate: '65, 69, 97',
 | 
					    secondary: 'rgb(216, 93, 216)',
 | 
				
			||||||
    success: '40, 199, 111',
 | 
					    alternate: 'rgb(65, 69, 97)',
 | 
				
			||||||
    danger: '234, 84, 85',
 | 
					    success: 'rgb(40, 199, 111)',
 | 
				
			||||||
    warning: '255, 159, 67',
 | 
					    danger: 'rgb(234, 84, 85)',
 | 
				
			||||||
    dark: '30, 30, 30',
 | 
					    warning: 'rgb(255, 159, 67)',
 | 
				
			||||||
 | 
					    dark: 'rgb(30, 30, 30)',
 | 
				
			||||||
    text: {
 | 
					    text: {
 | 
				
			||||||
      primary: '194, 198, 220',
 | 
					      primary: 'rgb(194, 198, 220)',
 | 
				
			||||||
      secondary: '255, 255, 255',
 | 
					      secondary: 'rgb(255, 255, 255)',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    border: '65, 69, 97',
 | 
					    border: 'rgb(65, 69, 97)',
 | 
				
			||||||
    bg: {
 | 
					    bg: {
 | 
				
			||||||
      primary: '16, 22, 58',
 | 
					      primary: 'rgb(16, 22, 58)',
 | 
				
			||||||
      secondary: '38, 44, 73',
 | 
					      secondary: 'rgb(38, 44, 73)',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,6 +20,7 @@ import MiniProfile from 'shared/components/MiniProfile';
 | 
				
			|||||||
import cache from 'App/cache';
 | 
					import cache from 'App/cache';
 | 
				
			||||||
import NOOP from 'shared/utils/noop';
 | 
					import NOOP from 'shared/utils/noop';
 | 
				
			||||||
import NotificationPopup, { NotificationItem } from 'shared/components/NotifcationPopup';
 | 
					import NotificationPopup, { NotificationItem } from 'shared/components/NotifcationPopup';
 | 
				
			||||||
 | 
					import theme from './ThemeStyles';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const TeamContainer = styled.div`
 | 
					const TeamContainer = styled.div`
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
@@ -62,7 +63,7 @@ const TeamProjectBackground = styled.div<{ color: string }>`
 | 
				
			|||||||
  opacity: 1;
 | 
					  opacity: 1;
 | 
				
			||||||
  border-radius: 3px;
 | 
					  border-radius: 3px;
 | 
				
			||||||
  &:before {
 | 
					  &:before {
 | 
				
			||||||
    background: rgba(${props => props.theme.colors.bg.secondary});
 | 
					    background: ${props => props.theme.colors.bg.secondary};
 | 
				
			||||||
    bottom: 0;
 | 
					    bottom: 0;
 | 
				
			||||||
    content: '';
 | 
					    content: '';
 | 
				
			||||||
    left: 0;
 | 
					    left: 0;
 | 
				
			||||||
@@ -114,7 +115,7 @@ const TeamProjectContainer = styled.div`
 | 
				
			|||||||
  margin: 0 4px 4px 0;
 | 
					  margin: 0 4px 4px 0;
 | 
				
			||||||
  min-width: 0;
 | 
					  min-width: 0;
 | 
				
			||||||
  &:hover ${TeamProjectTitle} {
 | 
					  &:hover ${TeamProjectTitle} {
 | 
				
			||||||
    color: rgba(${props => props.theme.colors.text.secondary});
 | 
					    color: ${props => props.theme.colors.text.secondary};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  &:hover ${TeamProjectAvatar} {
 | 
					  &:hover ${TeamProjectAvatar} {
 | 
				
			||||||
    opacity: 1;
 | 
					    opacity: 1;
 | 
				
			||||||
@@ -124,7 +125,7 @@ const TeamProjectContainer = styled.div`
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const colors = ['#e362e3', '#7a6ff0', '#37c5ab', '#aa62e3', '#e8384f'];
 | 
					const colors = [theme.colors.primary, theme.colors.secondary];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ProjectFinder = () => {
 | 
					const ProjectFinder = () => {
 | 
				
			||||||
  const { loading, data } = useGetProjectsQuery();
 | 
					  const { loading, data } = useGetProjectsQuery();
 | 
				
			||||||
@@ -328,7 +329,7 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
 | 
				
			|||||||
            />
 | 
					            />
 | 
				
			||||||
          ))}
 | 
					          ))}
 | 
				
			||||||
        </NotificationPopup>,
 | 
					        </NotificationPopup>,
 | 
				
			||||||
        { width: 415, borders: false, diamondColor: '#7367f0' },
 | 
					        { width: 415, borders: false, diamondColor: theme.colors.primary },
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -28,13 +28,13 @@ const StyledContainer = styled(ToastContainer).attrs({
 | 
				
			|||||||
    color: #fff;
 | 
					    color: #fff;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  .Toastify__toast--error {
 | 
					  .Toastify__toast--error {
 | 
				
			||||||
    background: rgba(${props => props.theme.colors.danger});
 | 
					    background: ${props => props.theme.colors.danger};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  .Toastify__toast--warning {
 | 
					  .Toastify__toast--warning {
 | 
				
			||||||
    background: rgba(${props => props.theme.colors.warning});
 | 
					    background: ${props => props.theme.colors.warning};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  .Toastify__toast--success {
 | 
					  .Toastify__toast--success {
 | 
				
			||||||
    background: rgba(${props => props.theme.colors.success});
 | 
					    background: ${props => props.theme.colors.success};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  .Toastify__toast-body {
 | 
					  .Toastify__toast-body {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -52,7 +52,6 @@ type RefreshTokenResponse = {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const App = () => {
 | 
					const App = () => {
 | 
				
			||||||
  const [loading, setLoading] = useState(true);
 | 
					 | 
				
			||||||
  const [user, setUser] = useState<CurrentUserRaw | null>(null);
 | 
					  const [user, setUser] = useState<CurrentUserRaw | null>(null);
 | 
				
			||||||
  const setUserRoles = (roles: CurrentUserRoles) => {
 | 
					  const setUserRoles = (roles: CurrentUserRoles) => {
 | 
				
			||||||
    if (user) {
 | 
					    if (user) {
 | 
				
			||||||
@@ -63,32 +62,6 @@ const App = () => {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					 | 
				
			||||||
    fetch('/auth/refresh_token', {
 | 
					 | 
				
			||||||
      method: 'POST',
 | 
					 | 
				
			||||||
      credentials: 'include',
 | 
					 | 
				
			||||||
    }).then(async x => {
 | 
					 | 
				
			||||||
      const { status } = x;
 | 
					 | 
				
			||||||
      if (status === 400) {
 | 
					 | 
				
			||||||
        history.replace('/login');
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        const response: RefreshTokenResponse = await x.json();
 | 
					 | 
				
			||||||
        const { accessToken, isInstalled } = response;
 | 
					 | 
				
			||||||
        const claims: JWTToken = jwtDecode(accessToken);
 | 
					 | 
				
			||||||
        const currentUser = {
 | 
					 | 
				
			||||||
          id: claims.userId,
 | 
					 | 
				
			||||||
          roles: { org: claims.orgRole, teams: new Map<string, string>(), projects: new Map<string, string>() },
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
        setUser(currentUser);
 | 
					 | 
				
			||||||
        setAccessToken(accessToken);
 | 
					 | 
				
			||||||
        if (!isInstalled) {
 | 
					 | 
				
			||||||
          history.replace('/install');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      setLoading(false);
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }, []);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <>
 | 
					    <>
 | 
				
			||||||
      <UserContext.Provider value={{ user, setUser, setUserRoles }}>
 | 
					      <UserContext.Provider value={{ user, setUser, setUserRoles }}>
 | 
				
			||||||
@@ -97,13 +70,7 @@ const App = () => {
 | 
				
			|||||||
          <BaseStyles />
 | 
					          <BaseStyles />
 | 
				
			||||||
          <Router history={history}>
 | 
					          <Router history={history}>
 | 
				
			||||||
            <PopupProvider>
 | 
					            <PopupProvider>
 | 
				
			||||||
              {loading ? (
 | 
					              <Routes history={history} />
 | 
				
			||||||
                <div>loading</div>
 | 
					 | 
				
			||||||
              ) : (
 | 
					 | 
				
			||||||
                <>
 | 
					 | 
				
			||||||
                  <Routes history={history} />
 | 
					 | 
				
			||||||
                </>
 | 
					 | 
				
			||||||
              )}
 | 
					 | 
				
			||||||
            </PopupProvider>
 | 
					            </PopupProvider>
 | 
				
			||||||
          </Router>
 | 
					          </Router>
 | 
				
			||||||
          <StyledContainer
 | 
					          <StyledContainer
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -52,7 +52,20 @@ const Auth = () => {
 | 
				
			|||||||
    }).then(async x => {
 | 
					    }).then(async x => {
 | 
				
			||||||
      const { status } = x;
 | 
					      const { status } = x;
 | 
				
			||||||
      if (status === 200) {
 | 
					      if (status === 200) {
 | 
				
			||||||
        history.replace('/projects');
 | 
					        const response: RefreshTokenResponse = await x.json();
 | 
				
			||||||
 | 
					        const { accessToken, setup } = response;
 | 
				
			||||||
 | 
					        if (setup) {
 | 
				
			||||||
 | 
					          history.replace(`/register?confirmToken=${setup.confirmToken}`);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          const claims: JWTToken = JwtDecode(accessToken);
 | 
				
			||||||
 | 
					          const currentUser = {
 | 
				
			||||||
 | 
					            id: claims.userId,
 | 
				
			||||||
 | 
					            roles: { org: claims.orgRole, teams: new Map<string, string>(), projects: new Map<string, string>() },
 | 
				
			||||||
 | 
					          };
 | 
				
			||||||
 | 
					          setUser(currentUser);
 | 
				
			||||||
 | 
					          setAccessToken(accessToken);
 | 
				
			||||||
 | 
					          history.replace('/projects');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }, []);
 | 
					  }, []);
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										61
									
								
								frontend/src/Confirm/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								frontend/src/Confirm/index.tsx
									
									
									
									
									
										Normal 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;
 | 
				
			||||||
@@ -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;
 | 
					 | 
				
			||||||
@@ -1,24 +0,0 @@
 | 
				
			|||||||
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;
 | 
					 | 
				
			||||||
@@ -1,41 +0,0 @@
 | 
				
			|||||||
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;
 | 
					 | 
				
			||||||
@@ -1,242 +0,0 @@
 | 
				
			|||||||
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 };
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const Dragger: React.FC<DraggerProps> = ({ draggedNodes, container, onDragEnd, isDragging, initialPos }) => {
 | 
					 | 
				
			||||||
  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 => {
 | 
					 | 
				
			||||||
      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;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      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 { ancestors } = findNodeDepth(outline.current.published, aboveNode.id);
 | 
					 | 
				
			||||||
        for (let i = 0; i < draggedNodes.nodes.length; i++) {
 | 
					 | 
				
			||||||
          const nodeID = draggedNodes.nodes[i];
 | 
					 | 
				
			||||||
          if (ancestors.find(c => c === nodeID)) {
 | 
					 | 
				
			||||||
            if (draggedNodes.first) {
 | 
					 | 
				
			||||||
              belowNode = draggedNodes.first;
 | 
					 | 
				
			||||||
              aboveNode = findNodeAbove(outline.current, aboveNode ? aboveNode.depth : 1, draggedNodes.first);
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
              const { depth } = findNodeDepth(outline.current.published, nodeID);
 | 
					 | 
				
			||||||
              const nodeDepth = outline.current.nodes.get(depth);
 | 
					 | 
				
			||||||
              const targetNode = nodeDepth ? nodeDepth.get(nodeID) : null;
 | 
					 | 
				
			||||||
              if (targetNode) {
 | 
					 | 
				
			||||||
                belowNode = targetNode;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                aboveNode = findNodeAbove(outline.current, 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: 'absolute' | 'relative' = isDragging ? 'absolute' : '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;
 | 
					 | 
				
			||||||
@@ -1,155 +0,0 @@
 | 
				
			|||||||
import React, { useRef, useEffect } from 'react';
 | 
					 | 
				
			||||||
import { Dot, CaretDown, CaretRight } from 'shared/icons';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { EntryChildren, EntryWrapper, EntryContent, EntryInnerContent, EntryHandle, ExpandButton } from './Styles';
 | 
					 | 
				
			||||||
import { useDrag } from './useDrag';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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) {
 | 
					 | 
				
			||||||
        caretPos = range.endOffset;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  return caretPos;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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>;
 | 
					 | 
				
			||||||
  entries: Array<ItemElement>;
 | 
					 | 
				
			||||||
  onCancelDrag: () => void;
 | 
					 | 
				
			||||||
  position: number;
 | 
					 | 
				
			||||||
  chain?: Array<string>;
 | 
					 | 
				
			||||||
  depth?: number;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const Entry: React.FC<EntryProps> = ({
 | 
					 | 
				
			||||||
  id,
 | 
					 | 
				
			||||||
  parentID,
 | 
					 | 
				
			||||||
  isRoot = false,
 | 
					 | 
				
			||||||
  selection,
 | 
					 | 
				
			||||||
  onToggleCollapse,
 | 
					 | 
				
			||||||
  onStartSelect,
 | 
					 | 
				
			||||||
  position,
 | 
					 | 
				
			||||||
  onCancelDrag,
 | 
					 | 
				
			||||||
  onStartDrag,
 | 
					 | 
				
			||||||
  collapsed = false,
 | 
					 | 
				
			||||||
  draggedNodes,
 | 
					 | 
				
			||||||
  entries,
 | 
					 | 
				
			||||||
  chain = [],
 | 
					 | 
				
			||||||
  depth = 0,
 | 
					 | 
				
			||||||
}) => {
 | 
					 | 
				
			||||||
  const $entry = useRef<HTMLDivElement>(null);
 | 
					 | 
				
			||||||
  const $children = useRef<HTMLDivElement>(null);
 | 
					 | 
				
			||||||
  const { setNodeDimensions, clearNodeDimensions } = useDrag();
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					 | 
				
			||||||
    if (isRoot) return;
 | 
					 | 
				
			||||||
    if ($entry && $entry.current) {
 | 
					 | 
				
			||||||
      setNodeDimensions(id, {
 | 
					 | 
				
			||||||
        entry: $entry,
 | 
					 | 
				
			||||||
        children: entries.length !== 0 ? $children : null,
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return () => {
 | 
					 | 
				
			||||||
      clearNodeDimensions(id);
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  }, [position, depth, entries]);
 | 
					 | 
				
			||||||
  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;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  let onSaveTimer: any = null;
 | 
					 | 
				
			||||||
  const onSaveTimeout = 300;
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <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={() => onCancelDrag()}
 | 
					 | 
				
			||||||
              onMouseDown={e => {
 | 
					 | 
				
			||||||
                onStartDrag({ id, clientX: e.clientX, clientY: e.clientY });
 | 
					 | 
				
			||||||
              }}
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              <Dot width={18} height={18} />
 | 
					 | 
				
			||||||
            </EntryHandle>
 | 
					 | 
				
			||||||
          )}
 | 
					 | 
				
			||||||
          <EntryInnerContent
 | 
					 | 
				
			||||||
            onMouseDown={() => {
 | 
					 | 
				
			||||||
              onStartSelect({ id, depth });
 | 
					 | 
				
			||||||
            }}
 | 
					 | 
				
			||||||
            onKeyDown={e => {
 | 
					 | 
				
			||||||
              if (e.key === 'z' && e.ctrlKey) {
 | 
					 | 
				
			||||||
                if ($entry && $entry.current) {
 | 
					 | 
				
			||||||
                  console.log(getCaretPosition($entry.current));
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
              } else {
 | 
					 | 
				
			||||||
                clearTimeout(onSaveTimer);
 | 
					 | 
				
			||||||
                if ($entry && $entry.current) {
 | 
					 | 
				
			||||||
                  onSaveTimer = setTimeout(() => {
 | 
					 | 
				
			||||||
                    if ($entry && $entry.current) {
 | 
					 | 
				
			||||||
                      console.log($entry.current.textContent);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                  }, onSaveTimeout);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            }}
 | 
					 | 
				
			||||||
            contentEditable
 | 
					 | 
				
			||||||
            ref={$entry}
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            {`${id.toString()} - ${position}`}
 | 
					 | 
				
			||||||
          </EntryInnerContent>
 | 
					 | 
				
			||||||
        </EntryContent>
 | 
					 | 
				
			||||||
      )}
 | 
					 | 
				
			||||||
      {entries.length !== 0 && !collapsed && (
 | 
					 | 
				
			||||||
        <EntryChildren ref={$children} isRoot={isRoot}>
 | 
					 | 
				
			||||||
          {entries
 | 
					 | 
				
			||||||
            .sort((a, b) => a.position - b.position)
 | 
					 | 
				
			||||||
            .map(entry => (
 | 
					 | 
				
			||||||
              <Entry
 | 
					 | 
				
			||||||
                parentID={id}
 | 
					 | 
				
			||||||
                key={entry.id}
 | 
					 | 
				
			||||||
                position={entry.position}
 | 
					 | 
				
			||||||
                depth={depth + 1}
 | 
					 | 
				
			||||||
                draggedNodes={draggedNodes}
 | 
					 | 
				
			||||||
                collapsed={entry.collapsed}
 | 
					 | 
				
			||||||
                id={entry.id}
 | 
					 | 
				
			||||||
                onStartSelect={onStartSelect}
 | 
					 | 
				
			||||||
                onStartDrag={onStartDrag}
 | 
					 | 
				
			||||||
                onCancelDrag={onCancelDrag}
 | 
					 | 
				
			||||||
                entries={entry.children ?? []}
 | 
					 | 
				
			||||||
                chain={[...chain, id]}
 | 
					 | 
				
			||||||
                selection={selection}
 | 
					 | 
				
			||||||
                onToggleCollapse={onToggleCollapse}
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
            ))}
 | 
					 | 
				
			||||||
        </EntryChildren>
 | 
					 | 
				
			||||||
      )}
 | 
					 | 
				
			||||||
    </EntryWrapper>
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default Entry;
 | 
					 | 
				
			||||||
@@ -1,164 +0,0 @@
 | 
				
			|||||||
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: 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 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: rgba(${p => p.theme.colors.text.primary});
 | 
					 | 
				
			||||||
  border-radius: 9px;
 | 
					 | 
				
			||||||
  &:hover {
 | 
					 | 
				
			||||||
    background: rgba(${p => p.theme.colors.primary});
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  svg {
 | 
					 | 
				
			||||||
    fill: rgba(${p => p.theme.colors.text.primary});
 | 
					 | 
				
			||||||
    stroke: rgba(${p => p.theme.colors.text.primary});
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
`;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const EntryInnerContent = styled.div`
 | 
					 | 
				
			||||||
  padding-top: 4px;
 | 
					 | 
				
			||||||
  font-size: 15px;
 | 
					 | 
				
			||||||
  white-space: pre-wrap;
 | 
					 | 
				
			||||||
  line-height: 24px;
 | 
					 | 
				
			||||||
  min-height: 24px;
 | 
					 | 
				
			||||||
  overflow-wrap: break-word;
 | 
					 | 
				
			||||||
  position: relative;
 | 
					 | 
				
			||||||
  user-select: text;
 | 
					 | 
				
			||||||
  color: rgba(${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: absolute;
 | 
					 | 
				
			||||||
  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: rgb(${props => props.theme.colors.text.primary});
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
`;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const PageContainer = styled.div`
 | 
					 | 
				
			||||||
  overflow: scroll;
 | 
					 | 
				
			||||||
`;
 | 
					 | 
				
			||||||
@@ -1,504 +0,0 @@
 | 
				
			|||||||
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,
 | 
					 | 
				
			||||||
} from './Styles';
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  transformToTree,
 | 
					 | 
				
			||||||
  findNode,
 | 
					 | 
				
			||||||
  findNodeDepth,
 | 
					 | 
				
			||||||
  getNumberOfChildren,
 | 
					 | 
				
			||||||
  validateDepth,
 | 
					 | 
				
			||||||
  getDimensions,
 | 
					 | 
				
			||||||
  findNextDraggable,
 | 
					 | 
				
			||||||
  getNodeOver,
 | 
					 | 
				
			||||||
  getCorrectNode,
 | 
					 | 
				
			||||||
  findCommonParent,
 | 
					 | 
				
			||||||
} from './utils';
 | 
					 | 
				
			||||||
import NOOP from 'shared/utils/noop';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type OutlineCommand = {
 | 
					 | 
				
			||||||
  nodes: Array<{
 | 
					 | 
				
			||||||
    id: string;
 | 
					 | 
				
			||||||
    prev: { position: number; parent: string | null };
 | 
					 | 
				
			||||||
    next: { position: number; parent: string | null };
 | 
					 | 
				
			||||||
  }>;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type ItemCollapsed = {
 | 
					 | 
				
			||||||
  id: string;
 | 
					 | 
				
			||||||
  collapsed: boolean;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const listItems: Array<ItemElement> = [
 | 
					 | 
				
			||||||
  { id: 'root', position: 4096, parent: null, collapsed: false },
 | 
					 | 
				
			||||||
  { id: 'entry-1', position: 4096, parent: 'root', collapsed: false },
 | 
					 | 
				
			||||||
  { id: 'entry-1_3', position: 4096 * 3, parent: 'entry-1', collapsed: false },
 | 
					 | 
				
			||||||
  { id: 'entry-1_3_1', position: 4096, parent: 'entry-1_3', collapsed: false },
 | 
					 | 
				
			||||||
  { id: 'entry-1_3_2', position: 4096 * 2, parent: 'entry-1_3', collapsed: false },
 | 
					 | 
				
			||||||
  { id: 'entry-1_3_3', position: 4096 * 3, parent: 'entry-1_3', collapsed: false },
 | 
					 | 
				
			||||||
  { id: 'entry-1_3_3_1', position: 4096 * 1, parent: 'entry-1_3_3', collapsed: false },
 | 
					 | 
				
			||||||
  { id: 'entry-1_3_3_1_1', position: 4096 * 1, parent: 'entry-1_3_3_1', collapsed: false },
 | 
					 | 
				
			||||||
  { id: 'entry-2', position: 4096 * 2, parent: 'root', collapsed: false },
 | 
					 | 
				
			||||||
  { id: 'entry-3', position: 4096 * 3, parent: 'root', collapsed: false },
 | 
					 | 
				
			||||||
  { id: 'entry-4', position: 4096 * 4, parent: 'root', collapsed: false },
 | 
					 | 
				
			||||||
  { id: 'entry-5', position: 4096 * 5, parent: 'root', collapsed: false },
 | 
					 | 
				
			||||||
];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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 { depth, ancestors } = findNodeDepth(outline.current.published, id);
 | 
					 | 
				
			||||||
      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 (idx !== -1) {
 | 
					 | 
				
			||||||
                draftItems[idx].parent = node.prev.parent;
 | 
					 | 
				
			||||||
                draftItems[idx].position = node.prev.position;
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
            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) {
 | 
					 | 
				
			||||||
                draftItems[idx].parent = node.next.parent;
 | 
					 | 
				
			||||||
                draftItems[idx].position = node.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);
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  }, []);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  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 {
 | 
					 | 
				
			||||||
                console.log(zone.above);
 | 
					 | 
				
			||||||
                console.log(zone.below);
 | 
					 | 
				
			||||||
                const correctNode = getCorrectNode(outline.current, zone.above ? zone.above.node : null, depth);
 | 
					 | 
				
			||||||
                console.log(correctNode);
 | 
					 | 
				
			||||||
                const listAbove = validateDepth(correctNode, depth);
 | 
					 | 
				
			||||||
                const listBelow = validateDepth(zone.below ? zone.below.node : null, depth);
 | 
					 | 
				
			||||||
                console.log(listAbove, listBelow);
 | 
					 | 
				
			||||||
                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>
 | 
					 | 
				
			||||||
            <PageContent>
 | 
					 | 
				
			||||||
              <RootWrapper ref={$content}>
 | 
					 | 
				
			||||||
                <Entry
 | 
					 | 
				
			||||||
                  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 } });
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                  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}
 | 
					 | 
				
			||||||
              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,
 | 
					 | 
				
			||||||
                                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;
 | 
					 | 
				
			||||||
@@ -1,22 +0,0 @@
 | 
				
			|||||||
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');
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
@@ -1,361 +0,0 @@
 | 
				
			|||||||
import _ from 'lodash';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function getCorrectNode(data: OutlineData, node: OutlineNode | null, depth: number) {
 | 
					 | 
				
			||||||
  if (node) {
 | 
					 | 
				
			||||||
    console.log(depth, node);
 | 
					 | 
				
			||||||
    if (depth === node.depth) {
 | 
					 | 
				
			||||||
      return node;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    const parent = node.ancestors[depth];
 | 
					 | 
				
			||||||
    console.log('parent', parent);
 | 
					 | 
				
			||||||
    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,
 | 
					 | 
				
			||||||
          };
 | 
					 | 
				
			||||||
          console.log('node above', nodeAbove);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        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;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  console.log('final node above', nodeAbove);
 | 
					 | 
				
			||||||
  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;
 | 
					 | 
				
			||||||
    // console.log(`mouseX=${mouseX} breakpoint=${breakpoint} x=${x} curDepth=${curDepth}`);
 | 
					 | 
				
			||||||
    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) {
 | 
					 | 
				
			||||||
      console.log(
 | 
					 | 
				
			||||||
        `[${id}] ${pos.y} <= ${target.bottom} = ${pos.y <= target.bottom} / ${pos.y} >= ${target.top} = ${pos.y >=
 | 
					 | 
				
			||||||
          target.top}`,
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      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) {
 | 
					 | 
				
			||||||
      console.log(
 | 
					 | 
				
			||||||
        `[${id}] ${pos.y} <= ${children.bottom} = ${pos.y <= children.bottom} / ${pos.y} >= ${children.top} = ${pos.y >=
 | 
					 | 
				
			||||||
          children.top}`,
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      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 {
 | 
					 | 
				
			||||||
      throw new Error('unable to find nextID');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  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;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -12,7 +12,7 @@ const FilterMember = styled(Member)`
 | 
				
			|||||||
  margin: 2px 0;
 | 
					  margin: 2px 0;
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    cursor: pointer;
 | 
					    cursor: pointer;
 | 
				
			||||||
    background: rgba(${props => props.theme.colors.primary});
 | 
					    background: ${props => props.theme.colors.primary};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -71,7 +71,7 @@ export const ActionItem = styled.li`
 | 
				
			|||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
  font-size: 14px;
 | 
					  font-size: 14px;
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    background: rgb(115, 103, 240);
 | 
					    background: ${props => props.theme.colors.primary};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -80,7 +80,7 @@ export const ActionTitle = styled.span`
 | 
				
			|||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ActionItemSeparator = styled.li`
 | 
					const ActionItemSeparator = styled.li`
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary}, 0.4);
 | 
					  color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.4)};
 | 
				
			||||||
  font-size: 12px;
 | 
					  font-size: 12px;
 | 
				
			||||||
  padding-left: 4px;
 | 
					  padding-left: 4px;
 | 
				
			||||||
  padding-right: 4px;
 | 
					  padding-right: 4px;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,7 +30,7 @@ export const ActionItem = styled.li`
 | 
				
			|||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
  font-size: 14px;
 | 
					  font-size: 14px;
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    background: rgb(115, 103, 240);
 | 
					    background: ${props => props.theme.colors.primary};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  &:hover ${ActionExtraMenuContainer} {
 | 
					  &:hover ${ActionExtraMenuContainer} {
 | 
				
			||||||
    visibility: visible;
 | 
					    visibility: visible;
 | 
				
			||||||
@@ -69,11 +69,11 @@ export const ActionExtraMenuItem = styled.li`
 | 
				
			|||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
  font-size: 14px;
 | 
					  font-size: 14px;
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    background: rgb(115, 103, 240);
 | 
					    background: rgb(${props => props.theme.colors.primary});
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
const ActionExtraMenuSeparator = styled.li`
 | 
					const ActionExtraMenuSeparator = styled.li`
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary}, 0.4);
 | 
					  color: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
  font-size: 12px;
 | 
					  font-size: 12px;
 | 
				
			||||||
  padding-left: 4px;
 | 
					  padding-left: 4px;
 | 
				
			||||||
  padding-right: 4px;
 | 
					  padding-right: 4px;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,7 @@
 | 
				
			|||||||
import React, { useState } from 'react';
 | 
					import React, { useState } from 'react';
 | 
				
			||||||
import styled from 'styled-components';
 | 
					import styled from 'styled-components';
 | 
				
			||||||
import { TaskSorting, TaskSortingType, TaskSortingDirection } from 'shared/utils/sorting';
 | 
					import { TaskSorting, TaskSortingType, TaskSortingDirection } from 'shared/utils/sorting';
 | 
				
			||||||
 | 
					import { mixin } from 'shared/utils/styles';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ActionsList = styled.ul`
 | 
					export const ActionsList = styled.ul`
 | 
				
			||||||
  margin: 0;
 | 
					  margin: 0;
 | 
				
			||||||
@@ -20,7 +21,7 @@ export const ActionItem = styled.li`
 | 
				
			|||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
  font-size: 14px;
 | 
					  font-size: 14px;
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    background: rgb(115, 103, 240);
 | 
					    background: ${props => props.theme.colors.primary};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -29,7 +30,7 @@ export const ActionTitle = styled.span`
 | 
				
			|||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ActionItemSeparator = styled.li`
 | 
					const ActionItemSeparator = styled.li`
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary}, 0.4);
 | 
					  color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.4)};
 | 
				
			||||||
  font-size: 12px;
 | 
					  font-size: 12px;
 | 
				
			||||||
  padding-left: 4px;
 | 
					  padding-left: 4px;
 | 
				
			||||||
  padding-right: 4px;
 | 
					  padding-right: 4px;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -136,14 +136,14 @@ const ProjectActionWrapper = styled.div<{ disabled?: boolean }>`
 | 
				
			|||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
  font-size: 15px;
 | 
					  font-size: 15px;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary});
 | 
					  color: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &:not(:last-of-type) {
 | 
					  &:not(:last-of-type) {
 | 
				
			||||||
    margin-right: 16px;
 | 
					    margin-right: 16px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    color: rgba(${props => props.theme.colors.text.secondary});
 | 
					    color: ${props => props.theme.colors.text.secondary};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  ${props =>
 | 
					  ${props =>
 | 
				
			||||||
    props.disabled &&
 | 
					    props.disabled &&
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -48,6 +48,7 @@ import { colourStyles } from 'shared/components/Select';
 | 
				
			|||||||
import Board, { BoardLoading } from './Board';
 | 
					import Board, { BoardLoading } from './Board';
 | 
				
			||||||
import Details from './Details';
 | 
					import Details from './Details';
 | 
				
			||||||
import LabelManagerEditor from './LabelManagerEditor';
 | 
					import LabelManagerEditor from './LabelManagerEditor';
 | 
				
			||||||
 | 
					import { mixin } from '../../shared/utils/styles';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const CARD_LABEL_VARIANT_STORAGE_KEY = 'card_label_variant';
 | 
					const CARD_LABEL_VARIANT_STORAGE_KEY = 'card_label_variant';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -71,7 +72,7 @@ const UserMember = styled(Member)`
 | 
				
			|||||||
  padding: 4px 0;
 | 
					  padding: 4px 0;
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    background: rgba(${props => props.theme.colors.bg.primary}, 0.4);
 | 
					    background: ${props => mixin.rgba(props.theme.colors.bg.primary, 0.4)};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  border-radius: 6px;
 | 
					  border-radius: 6px;
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,6 +20,8 @@ import Input from 'shared/components/Input';
 | 
				
			|||||||
import updateApolloCache from 'shared/utils/cache';
 | 
					import updateApolloCache from 'shared/utils/cache';
 | 
				
			||||||
import produce from 'immer';
 | 
					import produce from 'immer';
 | 
				
			||||||
import NOOP from 'shared/utils/noop';
 | 
					import NOOP from 'shared/utils/noop';
 | 
				
			||||||
 | 
					import theme from 'App/ThemeStyles';
 | 
				
			||||||
 | 
					import { mixin } from '../shared/utils/styles';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type CreateTeamData = { teamName: string };
 | 
					type CreateTeamData = { teamName: string };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -54,7 +56,7 @@ const CreateTeamForm: React.FC<CreateTeamFormProps> = ({ onCreateTeam }) => {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ProjectAddTile = styled.div`
 | 
					const ProjectAddTile = styled.div`
 | 
				
			||||||
  background-color: rgba(${props => props.theme.colors.bg.primary}, 0.4);
 | 
					  background-color: ${props => mixin.rgba(props.theme.colors.bg.primary, 0.4)};
 | 
				
			||||||
  background-size: cover;
 | 
					  background-size: cover;
 | 
				
			||||||
  background-position: 50%;
 | 
					  background-position: 50%;
 | 
				
			||||||
  color: #fff;
 | 
					  color: #fff;
 | 
				
			||||||
@@ -176,7 +178,7 @@ const SectionActionLink = styled(Link)`
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const ProjectSectionTitle = styled.h3`
 | 
					const ProjectSectionTitle = styled.h3`
 | 
				
			||||||
  font-size: 16px;
 | 
					  font-size: 16px;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary});
 | 
					  color: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ProjectsContainer = styled.div`
 | 
					const ProjectsContainer = styled.div`
 | 
				
			||||||
@@ -229,7 +231,7 @@ const Projects = () => {
 | 
				
			|||||||
    return <GlobalTopNavbar onSaveProjectName={NOOP} projectID={null} name={null} />;
 | 
					    return <GlobalTopNavbar onSaveProjectName={NOOP} projectID={null} name={null} />;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const colors = ['#e362e3', '#7a6ff0', '#37c5ab', '#aa62e3', '#e8384f'];
 | 
					  const colors = theme.colors.multiColors;
 | 
				
			||||||
  if (data && user) {
 | 
					  if (data && user) {
 | 
				
			||||||
    const { projects, teams, organizations } = data;
 | 
					    const { projects, teams, organizations } = data;
 | 
				
			||||||
    const organizationID = organizations[0].id ?? null;
 | 
					    const organizationID = organizations[0].id ?? null;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										13
									
								
								frontend/src/Register/Styles.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								frontend/src/Register/Styles.ts
									
									
									
									
									
										Normal 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%;
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
							
								
								
									
										62
									
								
								frontend/src/Register/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								frontend/src/Register/index.tsx
									
									
									
									
									
										Normal 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;
 | 
				
			||||||
@@ -21,6 +21,7 @@ import TaskAssignee from 'shared/components/TaskAssignee';
 | 
				
			|||||||
import Member from 'shared/components/Member';
 | 
					import Member from 'shared/components/Member';
 | 
				
			||||||
import ControlledInput from 'shared/components/ControlledInput';
 | 
					import ControlledInput from 'shared/components/ControlledInput';
 | 
				
			||||||
import NOOP from 'shared/utils/noop';
 | 
					import NOOP from 'shared/utils/noop';
 | 
				
			||||||
 | 
					import { mixin } from 'shared/utils/styles';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const MemberListWrapper = styled.div`
 | 
					const MemberListWrapper = styled.div`
 | 
				
			||||||
  flex: 1 1;
 | 
					  flex: 1 1;
 | 
				
			||||||
@@ -34,7 +35,7 @@ const UserMember = styled(Member)`
 | 
				
			|||||||
  padding: 4px 0;
 | 
					  padding: 4px 0;
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    background: rgba(${props => props.theme.colors.bg.primary}, 0.4);
 | 
					    background: ${props => mixin.rgba(props.theme.colors.bg.primary, 0.4)};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  border-radius: 6px;
 | 
					  border-radius: 6px;
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
@@ -119,12 +120,12 @@ export const MiniProfileActionItem = styled.span<{ disabled?: boolean }>`
 | 
				
			|||||||
      ? css`
 | 
					      ? css`
 | 
				
			||||||
          user-select: none;
 | 
					          user-select: none;
 | 
				
			||||||
          pointer-events: none;
 | 
					          pointer-events: none;
 | 
				
			||||||
          color: rgba(${props.theme.colors.text.primary}, 0.4);
 | 
					          color: ${mixin.rgba(props.theme.colors.text.primary, 0.4)};
 | 
				
			||||||
        `
 | 
					        `
 | 
				
			||||||
      : css`
 | 
					      : css`
 | 
				
			||||||
          cursor: pointer;
 | 
					          cursor: pointer;
 | 
				
			||||||
          &:hover {
 | 
					          &:hover {
 | 
				
			||||||
            background: rgb(115, 103, 240);
 | 
					            background: ${props.theme.colors.primary};
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        `}
 | 
					        `}
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
@@ -135,7 +136,7 @@ export const Content = styled.div`
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const CurrentPermission = styled.span`
 | 
					export const CurrentPermission = styled.span`
 | 
				
			||||||
  margin-left: 4px;
 | 
					  margin-left: 4px;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.secondary}, 0.4);
 | 
					  color: ${props => mixin.rgba(props.theme.colors.text.secondary, 0.4)};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const Separator = styled.div`
 | 
					export const Separator = styled.div`
 | 
				
			||||||
@@ -146,13 +147,13 @@ export const Separator = styled.div`
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const WarningText = styled.span`
 | 
					export const WarningText = styled.span`
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary}, 0.4);
 | 
					  color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.4)};
 | 
				
			||||||
  padding: 6px;
 | 
					  padding: 6px;
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const DeleteDescription = styled.div`
 | 
					export const DeleteDescription = styled.div`
 | 
				
			||||||
  font-size: 14px;
 | 
					  font-size: 14px;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary});
 | 
					  color: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const RemoveMemberButton = styled(Button)`
 | 
					export const RemoveMemberButton = styled(Button)`
 | 
				
			||||||
@@ -305,14 +306,14 @@ const MemberItemOption = styled(Button)`
 | 
				
			|||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const MemberList = styled.div`
 | 
					const MemberList = styled.div`
 | 
				
			||||||
  border-top: 1px solid rgba(${props => props.theme.colors.border});
 | 
					  border-top: 1px solid ${props => props.theme.colors.border};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const MemberListItem = styled.div`
 | 
					const MemberListItem = styled.div`
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  flex-flow: row wrap;
 | 
					  flex-flow: row wrap;
 | 
				
			||||||
  justify-content: space-between;
 | 
					  justify-content: space-between;
 | 
				
			||||||
  border-bottom: 1px solid rgba(${props => props.theme.colors.border});
 | 
					  border-bottom: 1px solid ${props => props.theme.colors.border};
 | 
				
			||||||
  min-height: 40px;
 | 
					  min-height: 40px;
 | 
				
			||||||
  padding: 12px 0 12px 40px;
 | 
					  padding: 12px 0 12px 40px;
 | 
				
			||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
@@ -336,11 +337,11 @@ const MemberProfile = styled(TaskAssignee)`
 | 
				
			|||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const MemberItemName = styled.p`
 | 
					const MemberItemName = styled.p`
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.secondary});
 | 
					  color: ${props => props.theme.colors.text.secondary};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const MemberItemUsername = styled.p`
 | 
					const MemberItemUsername = styled.p`
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary});
 | 
					  color: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const MemberListHeader = styled.div`
 | 
					const MemberListHeader = styled.div`
 | 
				
			||||||
@@ -349,12 +350,12 @@ const MemberListHeader = styled.div`
 | 
				
			|||||||
`;
 | 
					`;
 | 
				
			||||||
const ListTitle = styled.h3`
 | 
					const ListTitle = styled.h3`
 | 
				
			||||||
  font-size: 18px;
 | 
					  font-size: 18px;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.secondary});
 | 
					  color: ${props => props.theme.colors.text.secondary};
 | 
				
			||||||
  margin-bottom: 12px;
 | 
					  margin-bottom: 12px;
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
const ListDesc = styled.span`
 | 
					const ListDesc = styled.span`
 | 
				
			||||||
  font-size: 16px;
 | 
					  font-size: 16px;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary});
 | 
					  color: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
const FilterSearch = styled(Input)`
 | 
					const FilterSearch = styled(Input)`
 | 
				
			||||||
  margin: 0;
 | 
					  margin: 0;
 | 
				
			||||||
@@ -386,11 +387,11 @@ const FilterTabItem = styled.li`
 | 
				
			|||||||
  font-weight: 700;
 | 
					  font-weight: 700;
 | 
				
			||||||
  text-decoration: none;
 | 
					  text-decoration: none;
 | 
				
			||||||
  padding: 6px 8px;
 | 
					  padding: 6px 8px;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary});
 | 
					  color: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    border-radius: 6px;
 | 
					    border-radius: 6px;
 | 
				
			||||||
    background: rgba(${props => props.theme.colors.primary});
 | 
					    background: ${props => props.theme.colors.primary};
 | 
				
			||||||
    color: rgba(${props => props.theme.colors.text.secondary});
 | 
					    color: ${props => props.theme.colors.text.secondary};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,6 +8,7 @@ import {
 | 
				
			|||||||
} from 'shared/generated/graphql';
 | 
					} from 'shared/generated/graphql';
 | 
				
			||||||
import { Link } from 'react-router-dom';
 | 
					import { Link } from 'react-router-dom';
 | 
				
			||||||
import Input from 'shared/components/Input';
 | 
					import Input from 'shared/components/Input';
 | 
				
			||||||
 | 
					import theme from 'App/ThemeStyles';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const FilterSearch = styled(Input)`
 | 
					const FilterSearch = styled(Input)`
 | 
				
			||||||
  margin: 0;
 | 
					  margin: 0;
 | 
				
			||||||
@@ -34,11 +35,11 @@ const FilterTabItem = styled.li`
 | 
				
			|||||||
  font-weight: 700;
 | 
					  font-weight: 700;
 | 
				
			||||||
  text-decoration: none;
 | 
					  text-decoration: none;
 | 
				
			||||||
  padding: 6px 8px;
 | 
					  padding: 6px 8px;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary});
 | 
					  color: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    border-radius: 6px;
 | 
					    border-radius: 6px;
 | 
				
			||||||
    background: rgba(${props => props.theme.colors.primary});
 | 
					    background: ${props => props.theme.colors.primary};
 | 
				
			||||||
    color: rgba(${props => props.theme.colors.text.secondary});
 | 
					    color: ${props => props.theme.colors.text.secondary};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -55,7 +56,7 @@ const FilterTabTitle = styled.h2`
 | 
				
			|||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ProjectAddTile = styled.div`
 | 
					const ProjectAddTile = styled.div`
 | 
				
			||||||
  background-color: rgba(${props => props.theme.colors.bg.primary}, 0.4);
 | 
					  background-color: ${props => props.theme.colors.bg.primary};
 | 
				
			||||||
  background-size: cover;
 | 
					  background-size: cover;
 | 
				
			||||||
  background-position: 50%;
 | 
					  background-position: 50%;
 | 
				
			||||||
  color: #fff;
 | 
					  color: #fff;
 | 
				
			||||||
@@ -147,7 +148,7 @@ const ProjectListWrapper = styled.div`
 | 
				
			|||||||
  flex: 1 1;
 | 
					  flex: 1 1;
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const colors = ['#e362e3', '#7a6ff0', '#37c5ab', '#aa62e3', '#e8384f'];
 | 
					const colors = theme.colors.multiColors;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type TeamProjectsProps = {
 | 
					type TeamProjectsProps = {
 | 
				
			||||||
  teamID: string;
 | 
					  teamID: string;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,6 @@
 | 
				
			|||||||
import React from 'react';
 | 
					import React from 'react';
 | 
				
			||||||
import { action } from '@storybook/addon-actions';
 | 
					import { action } from '@storybook/addon-actions';
 | 
				
			||||||
 | 
					import theme from 'App/ThemeStyles';
 | 
				
			||||||
import AddList from '.';
 | 
					import AddList from '.';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
@@ -7,7 +8,7 @@ export default {
 | 
				
			|||||||
  title: 'AddList',
 | 
					  title: 'AddList',
 | 
				
			||||||
  parameters: {
 | 
					  parameters: {
 | 
				
			||||||
    backgrounds: [
 | 
					    backgrounds: [
 | 
				
			||||||
      { name: 'gray', value: '#262c49', default: true },
 | 
					      { name: 'gray', value: theme.colors.bg.secondary, default: true },
 | 
				
			||||||
      { name: 'white', value: '#ffffff' },
 | 
					      { name: 'white', value: '#ffffff' },
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -67,7 +67,7 @@ export const ListNameEditorWrapper = styled.div`
 | 
				
			|||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
export const ListNameEditor = styled(TextareaAutosize)`
 | 
					export const ListNameEditor = styled(TextareaAutosize)`
 | 
				
			||||||
  background-color: ${props => mixin.lighten('#262c49', 0.05)};
 | 
					  background-color: ${props => mixin.lighten(props.theme.colors.bg.secondary, 0.05)};
 | 
				
			||||||
  border: none;
 | 
					  border: none;
 | 
				
			||||||
  box-shadow: inset 0 0 0 2px #0079bf;
 | 
					  box-shadow: inset 0 0 0 2px #0079bf;
 | 
				
			||||||
  transition: margin 85ms ease-in, background 85ms ease-in;
 | 
					  transition: margin 85ms ease-in, background 85ms ease-in;
 | 
				
			||||||
@@ -91,7 +91,7 @@ export const ListNameEditor = styled(TextareaAutosize)`
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  color: #c2c6dc;
 | 
					  color: #c2c6dc;
 | 
				
			||||||
  l &:focus {
 | 
					  l &:focus {
 | 
				
			||||||
    background-color: ${props => mixin.lighten('#262c49', 0.05)};
 | 
					    background-color: ${props => mixin.lighten(props.theme.colors.bg.secondary, 0.05)};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,6 +8,7 @@ import { RoleCode, useUpdateUserRoleMutation } from 'shared/generated/graphql';
 | 
				
			|||||||
import Input from 'shared/components/Input';
 | 
					import Input from 'shared/components/Input';
 | 
				
			||||||
import Button from 'shared/components/Button';
 | 
					import Button from 'shared/components/Button';
 | 
				
			||||||
import NOOP from 'shared/utils/noop';
 | 
					import NOOP from 'shared/utils/noop';
 | 
				
			||||||
 | 
					import { mixin } from 'shared/utils/styles';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const RoleCheckmark = styled(Checkmark)`
 | 
					export const RoleCheckmark = styled(Checkmark)`
 | 
				
			||||||
  padding-left: 4px;
 | 
					  padding-left: 4px;
 | 
				
			||||||
@@ -58,12 +59,12 @@ export const MiniProfileActionItem = styled.span<{ disabled?: boolean }>`
 | 
				
			|||||||
      ? css`
 | 
					      ? css`
 | 
				
			||||||
          user-select: none;
 | 
					          user-select: none;
 | 
				
			||||||
          pointer-events: none;
 | 
					          pointer-events: none;
 | 
				
			||||||
          color: rgba(${props.theme.colors.text.primary}, 0.4);
 | 
					          color: ${mixin.rgba(props.theme.colors.text.primary, 0.4)};
 | 
				
			||||||
        `
 | 
					        `
 | 
				
			||||||
      : css`
 | 
					      : css`
 | 
				
			||||||
          cursor: pointer;
 | 
					          cursor: pointer;
 | 
				
			||||||
          &:hover {
 | 
					          &:hover {
 | 
				
			||||||
            background: rgb(115, 103, 240);
 | 
					            background: ${props.theme.colors.primary};
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        `}
 | 
					        `}
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
@@ -74,7 +75,7 @@ export const Content = styled.div`
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const CurrentPermission = styled.span`
 | 
					export const CurrentPermission = styled.span`
 | 
				
			||||||
  margin-left: 4px;
 | 
					  margin-left: 4px;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.secondary}, 0.4);
 | 
					  color: ${props => mixin.rgba(props.theme.colors.text.secondary, 0.4)};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const Separator = styled.div`
 | 
					export const Separator = styled.div`
 | 
				
			||||||
@@ -85,13 +86,13 @@ export const Separator = styled.div`
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const WarningText = styled.span`
 | 
					export const WarningText = styled.span`
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary}, 0.4);
 | 
					  color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.4)};
 | 
				
			||||||
  padding: 6px;
 | 
					  padding: 6px;
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const DeleteDescription = styled.div`
 | 
					export const DeleteDescription = styled.div`
 | 
				
			||||||
  font-size: 14px;
 | 
					  font-size: 14px;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary});
 | 
					  color: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const RemoveMemberButton = styled(Button)`
 | 
					export const RemoveMemberButton = styled(Button)`
 | 
				
			||||||
@@ -333,14 +334,14 @@ const MemberItemOption = styled(Button)`
 | 
				
			|||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const MemberList = styled.div`
 | 
					const MemberList = styled.div`
 | 
				
			||||||
  border-top: 1px solid rgba(${props => props.theme.colors.border});
 | 
					  border-top: 1px solid ${props => props.theme.colors.border};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const MemberListItem = styled.div`
 | 
					const MemberListItem = styled.div`
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  flex-flow: row wrap;
 | 
					  flex-flow: row wrap;
 | 
				
			||||||
  justify-content: space-between;
 | 
					  justify-content: space-between;
 | 
				
			||||||
  border-bottom: 1px solid rgba(${props => props.theme.colors.border});
 | 
					  border-bottom: 1px solid ${props => props.theme.colors.border};
 | 
				
			||||||
  min-height: 40px;
 | 
					  min-height: 40px;
 | 
				
			||||||
  padding: 12px 0 12px 40px;
 | 
					  padding: 12px 0 12px 40px;
 | 
				
			||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
@@ -364,11 +365,11 @@ const MemberProfile = styled(TaskAssignee)`
 | 
				
			|||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const MemberItemName = styled.p`
 | 
					const MemberItemName = styled.p`
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.secondary});
 | 
					  color: ${props => props.theme.colors.text.secondary};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const MemberItemUsername = styled.p`
 | 
					const MemberItemUsername = styled.p`
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary});
 | 
					  color: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const MemberListHeader = styled.div`
 | 
					const MemberListHeader = styled.div`
 | 
				
			||||||
@@ -377,12 +378,12 @@ const MemberListHeader = styled.div`
 | 
				
			|||||||
`;
 | 
					`;
 | 
				
			||||||
const ListTitle = styled.h3`
 | 
					const ListTitle = styled.h3`
 | 
				
			||||||
  font-size: 18px;
 | 
					  font-size: 18px;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.secondary});
 | 
					  color: ${props => props.theme.colors.text.secondary};
 | 
				
			||||||
  margin-bottom: 12px;
 | 
					  margin-bottom: 12px;
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
const ListDesc = styled.span`
 | 
					const ListDesc = styled.span`
 | 
				
			||||||
  font-size: 16px;
 | 
					  font-size: 16px;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary});
 | 
					  color: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
const FilterSearch = styled(Input)`
 | 
					const FilterSearch = styled(Input)`
 | 
				
			||||||
  margin: 0;
 | 
					  margin: 0;
 | 
				
			||||||
@@ -443,17 +444,17 @@ const TabNavItemButton = styled.button<{ active: boolean }>`
 | 
				
			|||||||
  width: 100%;
 | 
					  width: 100%;
 | 
				
			||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  color: ${props => (props.active ? 'rgba(115, 103, 240)' : '#c2c6dc')};
 | 
					  color: ${props => (props.active ? `${props.theme.colors.secondary}` : props.theme.colors.text.primary)};
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    color: rgba(115, 103, 240);
 | 
					    color: ${props => `${props.theme.colors.primary}`};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  &:hover svg {
 | 
					  &:hover svg {
 | 
				
			||||||
    fill: rgba(115, 103, 240);
 | 
					    fill: ${props => props.theme.colors.primary};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
const TabItemUser = styled(User)<{ active: boolean }>`
 | 
					const TabItemUser = styled(User)<{ active: boolean }>`
 | 
				
			||||||
fill: ${props => (props.active ? 'rgba(115, 103, 240)' : '#c2c6dc')}
 | 
					fill: ${props => (props.active ? `${props.theme.colors.primary}` : props.theme.colors.text.primary)}
 | 
				
			||||||
stroke: ${props => (props.active ? 'rgba(115, 103, 240)' : '#c2c6dc')}
 | 
					stroke: ${props => (props.active ? `${props.theme.colors.primary}` : props.theme.colors.text.primary)}
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const TabNavItemSpan = styled.span`
 | 
					const TabNavItemSpan = styled.span`
 | 
				
			||||||
@@ -470,8 +471,8 @@ const TabNavLine = styled.span<{ top: number }>`
 | 
				
			|||||||
  transform: scaleX(1);
 | 
					  transform: scaleX(1);
 | 
				
			||||||
  top: ${props => props.top}px;
 | 
					  top: ${props => props.top}px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  background: linear-gradient(30deg, rgba(115, 103, 240), rgba(115, 103, 240));
 | 
					  background: linear-gradient(30deg, ${props => props.theme.colors.primary}, ${props => props.theme.colors.primary});
 | 
				
			||||||
  box-shadow: 0 0 8px 0 rgba(115, 103, 240);
 | 
					  box-shadow: 0 0 8px 0 ${props => props.theme.colors.primary};
 | 
				
			||||||
  display: block;
 | 
					  display: block;
 | 
				
			||||||
  position: absolute;
 | 
					  position: absolute;
 | 
				
			||||||
  transition: all 0.2s ease;
 | 
					  transition: all 0.2s ease;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,6 @@
 | 
				
			|||||||
import React, { useRef } from 'react';
 | 
					import React, { useRef } from 'react';
 | 
				
			||||||
import styled, { css } from 'styled-components/macro';
 | 
					import styled, { css } from 'styled-components/macro';
 | 
				
			||||||
 | 
					import { mixin } from '../../utils/styles';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Text = styled.span<{ fontSize: string; justifyTextContent: string; hasIcon?: boolean }>`
 | 
					const Text = styled.span<{ fontSize: string; justifyTextContent: string; hasIcon?: boolean }>`
 | 
				
			||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
@@ -8,7 +9,7 @@ const Text = styled.span<{ fontSize: string; justifyTextContent: string; hasIcon
 | 
				
			|||||||
  justify-content: ${props => props.justifyTextContent};
 | 
					  justify-content: ${props => props.justifyTextContent};
 | 
				
			||||||
  transition: all 0.2s ease;
 | 
					  transition: all 0.2s ease;
 | 
				
			||||||
  font-size: ${props => props.fontSize};
 | 
					  font-size: ${props => props.fontSize};
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.secondary});
 | 
					  color: ${props => props.theme.colors.text.secondary};
 | 
				
			||||||
  ${props =>
 | 
					  ${props =>
 | 
				
			||||||
    props.hasIcon &&
 | 
					    props.hasIcon &&
 | 
				
			||||||
    css`
 | 
					    css`
 | 
				
			||||||
@@ -36,35 +37,36 @@ const Base = styled.button<{ color: string; disabled: boolean }>`
 | 
				
			|||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Filled = styled(Base)<{ hoverVariant: HoverVariant }>`
 | 
					const Filled = styled(Base)<{ hoverVariant: HoverVariant }>`
 | 
				
			||||||
  background: rgba(${props => props.theme.colors[props.color]});
 | 
					  background: ${props => props.theme.colors[props.color]};
 | 
				
			||||||
  ${props =>
 | 
					  ${props =>
 | 
				
			||||||
    props.hoverVariant === 'boxShadow' &&
 | 
					    props.hoverVariant === 'boxShadow' &&
 | 
				
			||||||
    css`
 | 
					    css`
 | 
				
			||||||
      &:hover {
 | 
					      &:hover {
 | 
				
			||||||
        box-shadow: 0 8px 25px -8px rgba(${props.theme.colors[props.color]});
 | 
					        box-shadow: 0 8px 25px -8px ${props.theme.colors[props.color]};
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    `}
 | 
					    `}
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Outline = styled(Base)<{ invert: boolean }>`
 | 
					const Outline = styled(Base)<{ invert: boolean }>`
 | 
				
			||||||
  border: 1px solid rgba(${props => props.theme.colors[props.color]});
 | 
					  border: 1px solid ${props => props.theme.colors[props.color]};
 | 
				
			||||||
  background: transparent;
 | 
					  background: transparent;
 | 
				
			||||||
  ${props =>
 | 
					  ${props =>
 | 
				
			||||||
    props.invert
 | 
					    props.invert
 | 
				
			||||||
      ? css`
 | 
					      ? css`
 | 
				
			||||||
          background: rgba(${props.theme.colors[props.color]});
 | 
					          background: ${props.theme.colors[props.color]});
 | 
				
			||||||
          & ${Text} {
 | 
					          & ${Text} {
 | 
				
			||||||
            color: rgba(${props.theme.colors.text.secondary});
 | 
					            color: ${props.theme.colors.text.secondary});
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          &:hover {
 | 
					          &:hover {
 | 
				
			||||||
            background: rgba(${props.theme.colors[props.color]}, 0.8);
 | 
					            background: ${mixin.rgba(props.theme.colors[props.color], 0.8)};
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        `
 | 
					        `
 | 
				
			||||||
      : css`
 | 
					      : css`
 | 
				
			||||||
          & ${Text} {
 | 
					          & ${Text} {
 | 
				
			||||||
            color: rgba(${props.theme.colors[props.color]});
 | 
					            color: ${props.theme.colors[props.color]});
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          &:hover {
 | 
					          &:hover {
 | 
				
			||||||
            background: rgba(${props.theme.colors[props.color]}, 0.08);
 | 
					            background: ${mixin.rgba(props.theme.colors[props.color], 0.08)};
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        `}
 | 
					        `}
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
@@ -72,7 +74,7 @@ const Outline = styled(Base)<{ invert: boolean }>`
 | 
				
			|||||||
const Flat = styled(Base)`
 | 
					const Flat = styled(Base)`
 | 
				
			||||||
  background: transparent;
 | 
					  background: transparent;
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    background: rgba(${props => props.theme.colors[props.color]}, 0.2);
 | 
					    background: ${props => mixin.rgba(props.theme.colors[props.color], 0.2)};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -85,7 +87,7 @@ const LineX = styled.span<{ color: string }>`
 | 
				
			|||||||
  bottom: -2px;
 | 
					  bottom: -2px;
 | 
				
			||||||
  left: 50%;
 | 
					  left: 50%;
 | 
				
			||||||
  transform: translate(-50%);
 | 
					  transform: translate(-50%);
 | 
				
			||||||
  background: rgba(${props => props.theme.colors[props.color]}, 1);
 | 
					  background: ${props => mixin.rgba(props.theme.colors[props.color], 1)};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const LineDown = styled(Base)`
 | 
					const LineDown = styled(Base)`
 | 
				
			||||||
@@ -94,7 +96,7 @@ const LineDown = styled(Base)`
 | 
				
			|||||||
  border-width: 0;
 | 
					  border-width: 0;
 | 
				
			||||||
  border-style: solid;
 | 
					  border-style: solid;
 | 
				
			||||||
  border-bottom-width: 2px;
 | 
					  border-bottom-width: 2px;
 | 
				
			||||||
  border-color: rgba(${props => props.theme.colors[props.color]}, 0.2);
 | 
					  border-color: ${props => mixin.rgba(props.theme.colors[props.color], 0.2)};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &:hover ${LineX} {
 | 
					  &:hover ${LineX} {
 | 
				
			||||||
    width: 100%;
 | 
					    width: 100%;
 | 
				
			||||||
@@ -107,8 +109,8 @@ const LineDown = styled(Base)`
 | 
				
			|||||||
const Gradient = styled(Base)`
 | 
					const Gradient = styled(Base)`
 | 
				
			||||||
  background: linear-gradient(
 | 
					  background: linear-gradient(
 | 
				
			||||||
    30deg,
 | 
					    30deg,
 | 
				
			||||||
    rgba(${props => props.theme.colors[props.color]}, 1),
 | 
					    ${props => mixin.rgba(props.theme.colors[props.color], 1)},
 | 
				
			||||||
    rgba(${props => props.theme.colors[props.color]}, 0.5)
 | 
					    ${props => mixin.rgba(props.theme.colors[props.color], 0.5)}
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
  text-shadow: 1px 2px 4px rgba(0, 0, 0, 0.3);
 | 
					  text-shadow: 1px 2px 4px rgba(0, 0, 0, 0.3);
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
@@ -117,7 +119,7 @@ const Gradient = styled(Base)`
 | 
				
			|||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Relief = styled(Base)`
 | 
					const Relief = styled(Base)`
 | 
				
			||||||
  background: rgba(${props => props.theme.colors[props.color]}, 1);
 | 
					  background: ${props => mixin.rgba(props.theme.colors[props.color], 1)};
 | 
				
			||||||
  -webkit-box-shadow: 0 -3px 0 0 rgba(0, 0, 0, 0.2) inset;
 | 
					  -webkit-box-shadow: 0 -3px 0 0 rgba(0, 0, 0, 0.2) inset;
 | 
				
			||||||
  box-shadow: inset 0 -3px 0 0 rgba(0, 0, 0, 0.2);
 | 
					  box-shadow: inset 0 -3px 0 0 rgba(0, 0, 0, 0.2);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,8 +5,8 @@ import { CheckCircle, CheckSquareOutline, Clock } from 'shared/icons';
 | 
				
			|||||||
import TaskAssignee from 'shared/components/TaskAssignee';
 | 
					import TaskAssignee from 'shared/components/TaskAssignee';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const CardMember = styled(TaskAssignee)<{ zIndex: number }>`
 | 
					export const CardMember = styled(TaskAssignee)<{ zIndex: number }>`
 | 
				
			||||||
  box-shadow: 0 0 0 2px rgba(${props => props.theme.colors.bg.secondary}),
 | 
					  box-shadow: 0 0 0 2px ${props => props.theme.colors.bg.secondary},
 | 
				
			||||||
    inset 0 0 0 1px rgba(${props => props.theme.colors.bg.secondary}, 0.07);
 | 
					    inset 0 0 0 1px ${props => mixin.rgba(props.theme.colors.bg.secondary, 0.07)};
 | 
				
			||||||
  z-index: ${props => props.zIndex};
 | 
					  z-index: ${props => props.zIndex};
 | 
				
			||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
@@ -14,8 +14,8 @@ export const ChecklistIcon = styled(CheckSquareOutline)<{ color: 'success' | 'no
 | 
				
			|||||||
  ${props =>
 | 
					  ${props =>
 | 
				
			||||||
    props.color === 'success' &&
 | 
					    props.color === 'success' &&
 | 
				
			||||||
    css`
 | 
					    css`
 | 
				
			||||||
      fill: rgba(${props.theme.colors.success});
 | 
					      fill: ${props.theme.colors.success};
 | 
				
			||||||
      stroke: rgba(${props.theme.colors.success});
 | 
					      stroke: ${props.theme.colors.success};
 | 
				
			||||||
    `}
 | 
					    `}
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
export const ClockIcon = styled(Clock)<{ color: string }>`
 | 
					export const ClockIcon = styled(Clock)<{ color: string }>`
 | 
				
			||||||
@@ -38,7 +38,7 @@ export const EditorTextarea = styled(TextareaAutosize)`
 | 
				
			|||||||
  padding: 0;
 | 
					  padding: 0;
 | 
				
			||||||
  font-size: 14px;
 | 
					  font-size: 14px;
 | 
				
			||||||
  line-height: 18px;
 | 
					  line-height: 18px;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary});
 | 
					  color: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
  &:focus {
 | 
					  &:focus {
 | 
				
			||||||
    border: none;
 | 
					    border: none;
 | 
				
			||||||
    outline: none;
 | 
					    outline: none;
 | 
				
			||||||
@@ -89,7 +89,7 @@ export const ListCardBadgeText = styled.span<{ color?: 'success' | 'normal' }>`
 | 
				
			|||||||
  padding: 0 4px 0 6px;
 | 
					  padding: 0 4px 0 6px;
 | 
				
			||||||
  vertical-align: top;
 | 
					  vertical-align: top;
 | 
				
			||||||
  white-space: nowrap;
 | 
					  white-space: nowrap;
 | 
				
			||||||
  ${props => props.color === 'success' && `color: rgba(${props.theme.colors.success});`}
 | 
					  ${props => props.color === 'success' && `color: ${props.theme.colors.success};`}
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ListCardContainer = styled.div<{ isActive: boolean; editable: boolean }>`
 | 
					export const ListCardContainer = styled.div<{ isActive: boolean; editable: boolean }>`
 | 
				
			||||||
@@ -101,7 +101,9 @@ export const ListCardContainer = styled.div<{ isActive: boolean; editable: boole
 | 
				
			|||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  background-color: ${props =>
 | 
					  background-color: ${props =>
 | 
				
			||||||
    props.isActive && !props.editable ? mixin.darken('#262c49', 0.1) : `rgba(${props.theme.colors.bg.secondary})`};
 | 
					    props.isActive && !props.editable
 | 
				
			||||||
 | 
					      ? mixin.darken(props.theme.colors.bg.secondary, 0.1)
 | 
				
			||||||
 | 
					      : `${props.theme.colors.bg.secondary}`};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ListCardInnerContainer = styled.div`
 | 
					export const ListCardInnerContainer = styled.div`
 | 
				
			||||||
@@ -221,7 +223,7 @@ export const ListCardOperation = styled.span`
 | 
				
			|||||||
  top: 2px;
 | 
					  top: 2px;
 | 
				
			||||||
  z-index: 100;
 | 
					  z-index: 100;
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    background-color: ${props => mixin.darken('#262c49', 0.25)};
 | 
					    background-color: ${props => mixin.darken(props.theme.colors.bg.secondary, 0.25)};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -233,7 +235,7 @@ export const CardTitle = styled.span`
 | 
				
			|||||||
  word-wrap: break-word;
 | 
					  word-wrap: break-word;
 | 
				
			||||||
  line-height: 18px;
 | 
					  line-height: 18px;
 | 
				
			||||||
  font-size: 14px;
 | 
					  font-size: 14px;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary});
 | 
					  color: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
@@ -246,7 +248,7 @@ export const CardMembers = styled.div`
 | 
				
			|||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const CompleteIcon = styled(CheckCircle)`
 | 
					export const CompleteIcon = styled(CheckCircle)`
 | 
				
			||||||
  fill: rgba(${props => props.theme.colors.success});
 | 
					  fill: ${props => props.theme.colors.success};
 | 
				
			||||||
  margin-right: 4px;
 | 
					  margin-right: 4px;
 | 
				
			||||||
  flex-shrink: 0;
 | 
					  flex-shrink: 0;
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,7 +22,7 @@ export default {
 | 
				
			|||||||
const Container = styled.div`
 | 
					const Container = styled.div`
 | 
				
			||||||
  width: 552px;
 | 
					  width: 552px;
 | 
				
			||||||
  margin: 25px;
 | 
					  margin: 25px;
 | 
				
			||||||
  border: 1px solid rgba(${props => props.theme.colors.bg.primary});
 | 
					  border: 1px solid ${props => props.theme.colors.bg.primary};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const defaultItems = [
 | 
					const defaultItems = [
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,6 +12,7 @@ import Button from 'shared/components/Button';
 | 
				
			|||||||
import TextareaAutosize from 'react-autosize-textarea';
 | 
					import TextareaAutosize from 'react-autosize-textarea';
 | 
				
			||||||
import Control from 'react-select/src/components/Control';
 | 
					import Control from 'react-select/src/components/Control';
 | 
				
			||||||
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
 | 
					import useOnOutsideClick from 'shared/hooks/onOutsideClick';
 | 
				
			||||||
 | 
					import { mixin } from 'shared/utils/styles';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Wrapper = styled.div`
 | 
					const Wrapper = styled.div`
 | 
				
			||||||
  margin-bottom: 24px;
 | 
					  margin-bottom: 24px;
 | 
				
			||||||
@@ -38,7 +39,7 @@ const WindowChecklistTitle = styled.div`
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const WindowTitleText = styled.h3`
 | 
					const WindowTitleText = styled.h3`
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary});
 | 
					  color: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
  margin: 6px 0;
 | 
					  margin: 6px 0;
 | 
				
			||||||
  display: inline-block;
 | 
					  display: inline-block;
 | 
				
			||||||
  width: auto;
 | 
					  width: auto;
 | 
				
			||||||
@@ -73,7 +74,7 @@ const ChecklistProgressPercent = styled.span`
 | 
				
			|||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ChecklistProgressBar = styled.div`
 | 
					const ChecklistProgressBar = styled.div`
 | 
				
			||||||
  background: rgba(${props => props.theme.colors.bg.primary});
 | 
					  background: ${props => props.theme.colors.bg.primary};
 | 
				
			||||||
  border-radius: 4px;
 | 
					  border-radius: 4px;
 | 
				
			||||||
  clear: both;
 | 
					  clear: both;
 | 
				
			||||||
  height: 8px;
 | 
					  height: 8px;
 | 
				
			||||||
@@ -83,7 +84,7 @@ const ChecklistProgressBar = styled.div`
 | 
				
			|||||||
`;
 | 
					`;
 | 
				
			||||||
const ChecklistProgressBarCurrent = styled.div<{ width: number }>`
 | 
					const ChecklistProgressBarCurrent = styled.div<{ width: number }>`
 | 
				
			||||||
  width: ${props => props.width}%;
 | 
					  width: ${props => props.width}%;
 | 
				
			||||||
  background: rgba(${props => (props.width === 100 ? props.theme.colors.success : props.theme.colors.primary)});
 | 
					  background: ${props => (props.width === 100 ? props.theme.colors.success : props.theme.colors.primary)};
 | 
				
			||||||
  bottom: 0;
 | 
					  bottom: 0;
 | 
				
			||||||
  left: 0;
 | 
					  left: 0;
 | 
				
			||||||
  position: absolute;
 | 
					  position: absolute;
 | 
				
			||||||
@@ -111,7 +112,7 @@ const ChecklistIcon = styled.div`
 | 
				
			|||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ChecklistItemCheckedIcon = styled(CheckSquare)`
 | 
					const ChecklistItemCheckedIcon = styled(CheckSquare)`
 | 
				
			||||||
  fill: rgba(${props => props.theme.colors.primary});
 | 
					  fill: ${props => props.theme.colors.primary};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ChecklistItemDetails = styled.div`
 | 
					const ChecklistItemDetails = styled.div`
 | 
				
			||||||
@@ -133,7 +134,7 @@ const ChecklistItemTextControls = styled.div`
 | 
				
			|||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ChecklistItemText = styled.span<{ complete: boolean }>`
 | 
					const ChecklistItemText = styled.span<{ complete: boolean }>`
 | 
				
			||||||
  color: ${props => (props.complete ? '#5e6c84' : `rgba(${props.theme.colors.text.primary})`)};
 | 
					  color: ${props => (props.complete ? '#5e6c84' : `${props.theme.colors.text.primary}`)};
 | 
				
			||||||
  ${props => props.complete && 'text-decoration: line-through;'}
 | 
					  ${props => props.complete && 'text-decoration: line-through;'}
 | 
				
			||||||
  line-height: 20px;
 | 
					  line-height: 20px;
 | 
				
			||||||
  font-size: 16px;
 | 
					  font-size: 16px;
 | 
				
			||||||
@@ -155,14 +156,14 @@ const ControlButton = styled.div`
 | 
				
			|||||||
  margin-left: 4px;
 | 
					  margin-left: 4px;
 | 
				
			||||||
  padding: 4px 6px;
 | 
					  padding: 4px 6px;
 | 
				
			||||||
  border-radius: 6px;
 | 
					  border-radius: 6px;
 | 
				
			||||||
  background-color: rgba(${props => props.theme.colors.bg.primary}, 0.8);
 | 
					  background-color: ${props => mixin.rgba(props.theme.colors.bg.primary, 0.8)};
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  width: 32px;
 | 
					  width: 32px;
 | 
				
			||||||
  height: 32px;
 | 
					  height: 32px;
 | 
				
			||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
  justify-content: center;
 | 
					  justify-content: center;
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    background-color: rgba(${props => props.theme.colors.primary}, 1);
 | 
					    background-color: ${props => mixin.rgba(props.theme.colors.primary, 1)};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -189,27 +190,27 @@ export const ChecklistNameEditor = styled(TextareaAutosize)`
 | 
				
			|||||||
  padding: 8px 12px;
 | 
					  padding: 8px 12px;
 | 
				
			||||||
  font-size: 16px;
 | 
					  font-size: 16px;
 | 
				
			||||||
  line-height: 20px;
 | 
					  line-height: 20px;
 | 
				
			||||||
  border: 1px solid rgba(${props => props.theme.colors.primary});
 | 
					  border: 1px solid ${props => props.theme.colors.primary};
 | 
				
			||||||
  border-radius: 3px;
 | 
					  border-radius: 3px;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary});
 | 
					  color: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  border-color: rgba(${props => props.theme.colors.border});
 | 
					  border-color: ${props => props.theme.colors.border};
 | 
				
			||||||
  background-color: rgba(${props => props.theme.colors.bg.primary}, 0.4);
 | 
					  background-color: ${props => mixin.rgba(props.theme.colors.bg.primary, 0.4)};
 | 
				
			||||||
  &:focus {
 | 
					  &:focus {
 | 
				
			||||||
    border-color: rgba(${props => props.theme.colors.primary});
 | 
					    border-color: ${props => props.theme.colors.primary};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const AssignUserButton = styled(AccountPlus)`
 | 
					const AssignUserButton = styled(AccountPlus)`
 | 
				
			||||||
  fill: rgba(${props => props.theme.colors.text.primary});
 | 
					  fill: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ClockButton = styled(Clock)`
 | 
					const ClockButton = styled(Clock)`
 | 
				
			||||||
  fill: rgba(${props => props.theme.colors.text.primary});
 | 
					  fill: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const TrashButton = styled(Trash)`
 | 
					const TrashButton = styled(Trash)`
 | 
				
			||||||
  fill: rgba(${props => props.theme.colors.text.primary});
 | 
					  fill: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ChecklistItemWrapper = styled.div<{ ref: any }>`
 | 
					const ChecklistItemWrapper = styled.div<{ ref: any }>`
 | 
				
			||||||
@@ -224,7 +225,7 @@ const ChecklistItemWrapper = styled.div<{ ref: any }>`
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    background-color: rgba(${props => props.theme.colors.bg.primary}, 0.4);
 | 
					    background-color: ${props => mixin.rgba(props.theme.colors.bg.primary, 0.4)};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  &:hover ${ControlButton} {
 | 
					  &:hover ${ControlButton} {
 | 
				
			||||||
    opacity: 1;
 | 
					    opacity: 1;
 | 
				
			||||||
@@ -246,10 +247,10 @@ const CancelButton = styled.div`
 | 
				
			|||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
  margin: 5px;
 | 
					  margin: 5px;
 | 
				
			||||||
  & svg {
 | 
					  & svg {
 | 
				
			||||||
    fill: rgba(${props => props.theme.colors.text.primary});
 | 
					    fill: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  &:hover svg {
 | 
					  &:hover svg {
 | 
				
			||||||
    fill: rgba(${props => props.theme.colors.text.secondary});
 | 
					    fill: ${props => props.theme.colors.text.secondary};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -265,7 +266,7 @@ const EditableDeleteButton = styled.button`
 | 
				
			|||||||
  border-radius: 3px;
 | 
					  border-radius: 3px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    background: rgba(${props => props.theme.colors.primary}, 0.8);
 | 
					    background: ${props => mixin.rgba(props.theme.colors.primary, 0.8)};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,7 @@ const LabelText = styled.span`
 | 
				
			|||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
  justify-content: center;
 | 
					  justify-content: center;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary});
 | 
					  color: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Container = styled.div<{ color?: string }>`
 | 
					const Container = styled.div<{ color?: string }>`
 | 
				
			||||||
@@ -24,11 +24,11 @@ const Container = styled.div<{ color?: string }>`
 | 
				
			|||||||
      ? css`
 | 
					      ? css`
 | 
				
			||||||
          background: ${props.color};
 | 
					          background: ${props.color};
 | 
				
			||||||
          & ${LabelText} {
 | 
					          & ${LabelText} {
 | 
				
			||||||
            color: rgba(${props.theme.colors.text.secondary});
 | 
					            color: ${props.theme.colors.text.secondary};
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        `
 | 
					        `
 | 
				
			||||||
      : css`
 | 
					      : css`
 | 
				
			||||||
          background: rgba(${props.theme.colors.bg.primary});
 | 
					          background: ${props.theme.colors.bg.primary};
 | 
				
			||||||
        `}
 | 
					        `}
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										103
									
								
								frontend/src/shared/components/Confirm/Styles.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								frontend/src/shared/components/Confirm/Styles.ts
									
									
									
									
									
										Normal 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);
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
							
								
								
									
										62
									
								
								frontend/src/shared/components/Confirm/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								frontend/src/shared/components/Confirm/index.tsx
									
									
									
									
									
										Normal 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;
 | 
				
			||||||
@@ -19,7 +19,7 @@ export default {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Wrapper = styled.div`
 | 
					const Wrapper = styled.div`
 | 
				
			||||||
  background: rgba(${props => props.theme.colors.bg.primary});
 | 
					  background: ${props => props.theme.colors.bg.primary};
 | 
				
			||||||
  padding: 45px;
 | 
					  padding: 45px;
 | 
				
			||||||
  margin: 25px;
 | 
					  margin: 25px;
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,6 @@
 | 
				
			|||||||
import React, { useState, useEffect, useRef } from 'react';
 | 
					import React, { useState, useEffect, useRef } from 'react';
 | 
				
			||||||
import styled, { css } from 'styled-components/macro';
 | 
					import styled, { css } from 'styled-components/macro';
 | 
				
			||||||
 | 
					import theme from '../../../App/ThemeStyles';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const InputWrapper = styled.div<{ width: string }>`
 | 
					const InputWrapper = styled.div<{ width: string }>`
 | 
				
			||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
@@ -57,14 +58,14 @@ const InputInput = styled.input<{
 | 
				
			|||||||
    background: ${props => props.focusBg};
 | 
					    background: ${props => props.focusBg};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  &:focus ~ ${InputLabel} {
 | 
					  &:focus ~ ${InputLabel} {
 | 
				
			||||||
    color: rgba(115, 103, 240);
 | 
					    color: ${props => props.theme.colors.primary};
 | 
				
			||||||
    transform: translate(-3px, -90%);
 | 
					    transform: translate(-3px, -90%);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  ${props =>
 | 
					  ${props =>
 | 
				
			||||||
    props.hasValue &&
 | 
					    props.hasValue &&
 | 
				
			||||||
    css`
 | 
					    css`
 | 
				
			||||||
      & ~ ${InputLabel} {
 | 
					      & ~ ${InputLabel} {
 | 
				
			||||||
        color: rgba(115, 103, 240);
 | 
					        color: ${props.theme.colors.primary};
 | 
				
			||||||
        transform: translate(-3px, -90%);
 | 
					        transform: translate(-3px, -90%);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    `}
 | 
					    `}
 | 
				
			||||||
@@ -115,8 +116,8 @@ const ControlledInput = ({
 | 
				
			|||||||
}: ControlledInputProps) => {
 | 
					}: ControlledInputProps) => {
 | 
				
			||||||
  const $input = useRef<HTMLInputElement>(null);
 | 
					  const $input = useRef<HTMLInputElement>(null);
 | 
				
			||||||
  const [hasValue, setHasValue] = useState(false);
 | 
					  const [hasValue, setHasValue] = useState(false);
 | 
				
			||||||
  const borderColor = variant === 'normal' ? 'rgba(0, 0, 0, 0.2)' : '#414561';
 | 
					  const borderColor = variant === 'normal' ? 'rgba(0, 0, 0, 0.2)' : theme.colors.alternate;
 | 
				
			||||||
  const focusBg = variant === 'normal' ? 'rgba(38, 44, 73, )' : 'rgba(16, 22, 58, 1)';
 | 
					  const focusBg = variant === 'normal' ? theme.colors.bg.secondary : theme.colors.bg.primary;
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    if (autoFocus && $input && $input.current) {
 | 
					    if (autoFocus && $input && $input.current) {
 | 
				
			||||||
      $input.current.focus();
 | 
					      $input.current.focus();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@ import React, { createRef, useState } from 'react';
 | 
				
			|||||||
import styled from 'styled-components';
 | 
					import styled from 'styled-components';
 | 
				
			||||||
import { action } from '@storybook/addon-actions';
 | 
					import { action } from '@storybook/addon-actions';
 | 
				
			||||||
import DropdownMenu from '.';
 | 
					import DropdownMenu from '.';
 | 
				
			||||||
 | 
					import theme from '../../../App/ThemeStyles';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  component: DropdownMenu,
 | 
					  component: DropdownMenu,
 | 
				
			||||||
@@ -10,7 +11,7 @@ export default {
 | 
				
			|||||||
    backgrounds: [
 | 
					    backgrounds: [
 | 
				
			||||||
      { name: 'white', value: '#ffffff' },
 | 
					      { name: 'white', value: '#ffffff' },
 | 
				
			||||||
      { name: 'gray', value: '#f8f8f8' },
 | 
					      { name: 'gray', value: '#f8f8f8' },
 | 
				
			||||||
      { name: 'darkBlue', value: '#262c49', default: true },
 | 
					      { name: 'darkBlue', value: theme.colors.bg.secondary, default: true },
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -59,7 +59,7 @@ export const ActionItem = styled.li`
 | 
				
			|||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
  font-size: 14px;
 | 
					  font-size: 14px;
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    background: rgb(115, 103, 240);
 | 
					    background: ${props => props.theme.colors.primary};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,23 +19,23 @@ display: flex
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  & .react-datepicker-time__header {
 | 
					  & .react-datepicker-time__header {
 | 
				
			||||||
    color: rgba(${props => props.theme.colors.text.primary});
 | 
					    color: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  & .react-datepicker__time-list-item {
 | 
					  & .react-datepicker__time-list-item {
 | 
				
			||||||
    color: rgba(${props => props.theme.colors.text.primary});
 | 
					    color: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  & .react-datepicker__time-container .react-datepicker__time
 | 
					  & .react-datepicker__time-container .react-datepicker__time
 | 
				
			||||||
  .react-datepicker__time-box ul.react-datepicker__time-list
 | 
					  .react-datepicker__time-box ul.react-datepicker__time-list
 | 
				
			||||||
  li.react-datepicker__time-list-item:hover {
 | 
					  li.react-datepicker__time-list-item:hover {
 | 
				
			||||||
    color: rgba(${props => props.theme.colors.text.secondary});
 | 
					    color: ${props => props.theme.colors.text.secondary};
 | 
				
			||||||
    background: rgba(${props => props.theme.colors.bg.secondary});
 | 
					    background: ${props => props.theme.colors.bg.secondary};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  & .react-datepicker__time-container .react-datepicker__time {
 | 
					  & .react-datepicker__time-container .react-datepicker__time {
 | 
				
			||||||
    background: rgba(${props => props.theme.colors.bg.primary});
 | 
					    background: ${props => props.theme.colors.bg.primary};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  & .react-datepicker--time-only {
 | 
					  & .react-datepicker--time-only {
 | 
				
			||||||
    background: rgba(${props => props.theme.colors.bg.primary});
 | 
					    background: ${props => props.theme.colors.bg.primary};
 | 
				
			||||||
    border: 1px solid rgba(${props => props.theme.colors.border});
 | 
					    border: 1px solid ${props => props.theme.colors.border};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  & .react-datepicker * {
 | 
					  & .react-datepicker * {
 | 
				
			||||||
@@ -75,12 +75,12 @@ display: flex
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
  & .react-datepicker__day--selected {
 | 
					  & .react-datepicker__day--selected {
 | 
				
			||||||
    border-radius: 50%;
 | 
					    border-radius: 50%;
 | 
				
			||||||
    background: rgba(115, 103, 240);
 | 
					    background: ${props => props.theme.colors.primary};
 | 
				
			||||||
    color: #fff;
 | 
					    color: #fff;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  & .react-datepicker__day--selected:hover {
 | 
					  & .react-datepicker__day--selected:hover {
 | 
				
			||||||
    border-radius: 50%;
 | 
					    border-radius: 50%;
 | 
				
			||||||
    background: rgba(115, 103, 240);
 | 
					    background: ${props => props.theme.colors.primary};
 | 
				
			||||||
    color: #fff;
 | 
					    color: #fff;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  & .react-datepicker__header {
 | 
					  & .react-datepicker__header {
 | 
				
			||||||
@@ -88,7 +88,7 @@ display: flex
 | 
				
			|||||||
    border: none;
 | 
					    border: none;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  & .react-datepicker__header--time {
 | 
					  & .react-datepicker__header--time {
 | 
				
			||||||
    border-bottom: 1px solid rgba(${props => props.theme.colors.border});
 | 
					    border-bottom: 1px solid ${props => props.theme.colors.border};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -43,7 +43,7 @@ const HeaderSelectLabel = styled.div`
 | 
				
			|||||||
  color: #c2c6dc;
 | 
					  color: #c2c6dc;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    background: rgba(115, 103, 240);
 | 
					    background: ${props => props.theme.colors.primary};
 | 
				
			||||||
    color: #c2c6dc;
 | 
					    color: #c2c6dc;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
@@ -60,8 +60,8 @@ const HeaderSelect = styled.select`
 | 
				
			|||||||
  appearance: none;
 | 
					  appearance: none;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    background: #262c49;
 | 
					    background: ${props => props.theme.colors.bg.secondary};
 | 
				
			||||||
    border: 1px solid rgba(115, 103, 240);
 | 
					    border: 1px solid ${props => props.theme.colors.primary};
 | 
				
			||||||
    outline: none !important;
 | 
					    outline: none !important;
 | 
				
			||||||
    box-shadow: none;
 | 
					    box-shadow: none;
 | 
				
			||||||
    color: #c2c6dc;
 | 
					    color: #c2c6dc;
 | 
				
			||||||
@@ -93,7 +93,7 @@ const HeaderButton = styled.button`
 | 
				
			|||||||
  border: none;
 | 
					  border: none;
 | 
				
			||||||
  border-radius: 3px;
 | 
					  border-radius: 3px;
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    background: rgba(115, 103, 240);
 | 
					    background: ${props => props.theme.colors.primary};
 | 
				
			||||||
    color: #fff;
 | 
					    color: #fff;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,7 @@
 | 
				
			|||||||
import React from 'react';
 | 
					import React from 'react';
 | 
				
			||||||
import styled, { keyframes } from 'styled-components/macro';
 | 
					import styled, { keyframes } from 'styled-components/macro';
 | 
				
			||||||
import { mixin } from 'shared/utils/styles';
 | 
					import { mixin } from 'shared/utils/styles';
 | 
				
			||||||
 | 
					import theme from '../../../App/ThemeStyles';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const BoardContainer = styled.div`
 | 
					export const BoardContainer = styled.div`
 | 
				
			||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
@@ -34,9 +35,9 @@ export const Container = styled.div`
 | 
				
			|||||||
  white-space: nowrap;
 | 
					  white-space: nowrap;
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const defaultBaseColor = '#10163a';
 | 
					export const defaultBaseColor = theme.colors.bg.primary;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const defaultHighlightColor = mixin.lighten('#10163a', 0.25);
 | 
					export const defaultHighlightColor = mixin.lighten(theme.colors.bg.primary, 0.25);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const skeletonKeyframes = keyframes`
 | 
					export const skeletonKeyframes = keyframes`
 | 
				
			||||||
  0% {
 | 
					  0% {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,7 +19,7 @@ export default {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Wrapper = styled.div`
 | 
					const Wrapper = styled.div`
 | 
				
			||||||
  background: rgba(${props => props.theme.colors.bg.primary});
 | 
					  background: ${props => props.theme.colors.bg.primary};
 | 
				
			||||||
  padding: 45px;
 | 
					  padding: 45px;
 | 
				
			||||||
  margin: 25px;
 | 
					  margin: 25px;
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,6 @@
 | 
				
			|||||||
import React, { useState, useEffect, useRef } from 'react';
 | 
					import React, { useState, useEffect, useRef } from 'react';
 | 
				
			||||||
import styled, { css } from 'styled-components/macro';
 | 
					import styled, { css } from 'styled-components/macro';
 | 
				
			||||||
 | 
					import theme from '../../../App/ThemeStyles';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const InputWrapper = styled.div<{ width: string }>`
 | 
					const InputWrapper = styled.div<{ width: string }>`
 | 
				
			||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
@@ -53,18 +54,18 @@ const InputInput = styled.input<{
 | 
				
			|||||||
  transition: all 0.3s ease;
 | 
					  transition: all 0.3s ease;
 | 
				
			||||||
  &:focus {
 | 
					  &:focus {
 | 
				
			||||||
    box-shadow: 0 3px 10px 0 rgba(0, 0, 0, 0.15);
 | 
					    box-shadow: 0 3px 10px 0 rgba(0, 0, 0, 0.15);
 | 
				
			||||||
    border: 1px solid rgba(115, 103, 240);
 | 
					    border: 1px solid ${props => props.theme.colors.primary};
 | 
				
			||||||
    background: ${props => props.focusBg};
 | 
					    background: ${props => props.focusBg};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  &:focus ~ ${InputLabel} {
 | 
					  &:focus ~ ${InputLabel} {
 | 
				
			||||||
    color: rgba(115, 103, 240);
 | 
					    color: ${props => props.theme.colors.primary};
 | 
				
			||||||
    transform: translate(-3px, -90%);
 | 
					    transform: translate(-3px, -90%);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  ${props =>
 | 
					  ${props =>
 | 
				
			||||||
    props.hasValue &&
 | 
					    props.hasValue &&
 | 
				
			||||||
    css`
 | 
					    css`
 | 
				
			||||||
      & ~ ${InputLabel} {
 | 
					      & ~ ${InputLabel} {
 | 
				
			||||||
        color: rgba(115, 103, 240);
 | 
					        color: ${props.theme.colors.primary};
 | 
				
			||||||
        transform: translate(-3px, -90%);
 | 
					        transform: translate(-3px, -90%);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    `}
 | 
					    `}
 | 
				
			||||||
@@ -138,8 +139,8 @@ const Input = React.forwardRef(
 | 
				
			|||||||
    $ref: any,
 | 
					    $ref: any,
 | 
				
			||||||
  ) => {
 | 
					  ) => {
 | 
				
			||||||
    const [hasValue, setHasValue] = useState(defaultValue !== '');
 | 
					    const [hasValue, setHasValue] = useState(defaultValue !== '');
 | 
				
			||||||
    const borderColor = variant === 'normal' ? 'rgba(0, 0, 0, 0.2)' : '#414561';
 | 
					    const borderColor = variant === 'normal' ? 'rgba(0,0,0,0.2)' : theme.colors.alternate;
 | 
				
			||||||
    const focusBg = variant === 'normal' ? 'rgba(38, 44, 73, )' : 'rgba(16, 22, 58, 1)';
 | 
					    const focusBg = variant === 'normal' ? theme.colors.bg.secondary : theme.colors.bg.primary;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Merge forwarded ref and internal ref in order to be able to access the ref in the useEffect
 | 
					    // Merge forwarded ref and internal ref in order to be able to access the ref in the useEffect
 | 
				
			||||||
    // The forwarded ref is not accessible by itself, which is what the innerRef & combined ref is for
 | 
					    // The forwarded ref is not accessible by itself, which is what the innerRef & combined ref is for
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,5 @@
 | 
				
			|||||||
import styled, { css } from 'styled-components';
 | 
					import styled, { css } from 'styled-components';
 | 
				
			||||||
import TextareaAutosize from 'react-autosize-textarea';
 | 
					import TextareaAutosize from 'react-autosize-textarea';
 | 
				
			||||||
import { mixin } from 'shared/utils/styles';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const Container = styled.div`
 | 
					export const Container = styled.div`
 | 
				
			||||||
  width: 272px;
 | 
					  width: 272px;
 | 
				
			||||||
@@ -34,7 +33,7 @@ export const AddCardButton = styled.a`
 | 
				
			|||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    color: #c2c6dc;
 | 
					    color: #c2c6dc;
 | 
				
			||||||
    text-decoration: none;
 | 
					    text-decoration: none;
 | 
				
			||||||
    background: rgb(115, 103, 240);
 | 
					    background: ${props => props.theme.colors.primary};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
export const Wrapper = styled.div`
 | 
					export const Wrapper = styled.div`
 | 
				
			||||||
@@ -96,7 +95,7 @@ export const Header = styled.div<{ isEditing: boolean }>`
 | 
				
			|||||||
    props.isEditing &&
 | 
					    props.isEditing &&
 | 
				
			||||||
    css`
 | 
					    css`
 | 
				
			||||||
      & ${HeaderName} {
 | 
					      & ${HeaderName} {
 | 
				
			||||||
        box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px;
 | 
					        box-shadow: ${props.theme.colors.primary} 0px 0px 0px 1px;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    `}
 | 
					    `}
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,7 +21,7 @@ export const ListActionItem = styled.span`
 | 
				
			|||||||
  margin: 0 -12px;
 | 
					  margin: 0 -12px;
 | 
				
			||||||
  text-decoration: none;
 | 
					  text-decoration: none;
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    background: rgb(115, 103, 240);
 | 
					    background: ${props => props.theme.colors.primary};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,6 @@
 | 
				
			|||||||
import React, { useState } from 'react';
 | 
					import React, { useState } from 'react';
 | 
				
			||||||
import { action } from '@storybook/addon-actions';
 | 
					import { action } from '@storybook/addon-actions';
 | 
				
			||||||
 | 
					import theme from 'App/ThemeStyles';
 | 
				
			||||||
import Lists from '.';
 | 
					import Lists from '.';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
@@ -7,7 +8,7 @@ export default {
 | 
				
			|||||||
  title: 'Lists',
 | 
					  title: 'Lists',
 | 
				
			||||||
  parameters: {
 | 
					  parameters: {
 | 
				
			||||||
    backgrounds: [
 | 
					    backgrounds: [
 | 
				
			||||||
      { name: 'gray', value: '#262c49', default: true },
 | 
					      { name: 'gray', value: theme.colors.bg.secondary, default: true },
 | 
				
			||||||
      { name: 'white', value: '#ffffff' },
 | 
					      { name: 'white', value: '#ffffff' },
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,10 +22,10 @@ export const LoadingSpinnerWrapper = styled.div<{ color: string; size: string; b
 | 
				
			|||||||
    width: ${props => props.size};
 | 
					    width: ${props => props.size};
 | 
				
			||||||
    height: ${props => props.size};
 | 
					    height: ${props => props.size};
 | 
				
			||||||
    margin: ${props => props.thickness};
 | 
					    margin: ${props => props.thickness};
 | 
				
			||||||
    border: ${props => props.thickness} solid rgba(${props => props.theme.colors[props.color]});
 | 
					    border: ${props => props.thickness} solid ${props => props.theme.colors[props.color]};
 | 
				
			||||||
    border-radius: 50%;
 | 
					    border-radius: 50%;
 | 
				
			||||||
    animation: 1.2s ${LoadingSpinnerKeyframes} cubic-bezier(0.5, 0, 0.5, 1) infinite;
 | 
					    animation: 1.2s ${LoadingSpinnerKeyframes} cubic-bezier(0.5, 0, 0.5, 1) infinite;
 | 
				
			||||||
    border-color: rgba(${props => props.theme.colors[props.color]}) transparent transparent transparent;
 | 
					    border-color: ${props => props.theme.colors[props.color]} transparent transparent transparent;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  & > div:nth-child(1) {
 | 
					  & > div:nth-child(1) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
import React from 'react';
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { LoadingSpinnerWrapper} from './Styles';
 | 
					import { LoadingSpinnerWrapper } from './Styles';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type LoadingSpinnerProps = {
 | 
					type LoadingSpinnerProps = {
 | 
				
			||||||
  color?: 'primary' | 'danger' | 'success' | 'warning' | 'dark';
 | 
					  color?: 'primary' | 'danger' | 'success' | 'warning' | 'dark';
 | 
				
			||||||
@@ -30,11 +30,11 @@ const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({
 | 
				
			|||||||
  borderSize = '80px',
 | 
					  borderSize = '80px',
 | 
				
			||||||
}) => {
 | 
					}) => {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
      <LoadingSpinnerWrapper color={color} size={size} thickness={thickness} borderSize={borderSize}>
 | 
					    <LoadingSpinnerWrapper color={color} size={size} thickness={thickness} borderSize={borderSize}>
 | 
				
			||||||
        <div />
 | 
					      <div />
 | 
				
			||||||
        <div />
 | 
					      <div />
 | 
				
			||||||
        <div />
 | 
					      <div />
 | 
				
			||||||
      </LoadingSpinnerWrapper>
 | 
					    </LoadingSpinnerWrapper>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,6 @@
 | 
				
			|||||||
import styled from 'styled-components';
 | 
					import styled from 'styled-components';
 | 
				
			||||||
import Button from 'shared/components/Button';
 | 
					import Button from 'shared/components/Button';
 | 
				
			||||||
 | 
					import { mixin } from 'shared/utils/styles';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const Wrapper = styled.div`
 | 
					export const Wrapper = styled.div`
 | 
				
			||||||
  background: #eff2f7;
 | 
					  background: #eff2f7;
 | 
				
			||||||
@@ -68,7 +69,7 @@ export const FormIcon = styled.div`
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const FormError = styled.span`
 | 
					export const FormError = styled.span`
 | 
				
			||||||
  font-size: 0.875rem;
 | 
					  font-size: 0.875rem;
 | 
				
			||||||
  color: rgb(234, 84, 85);
 | 
					  color: ${props => props.theme.colors.danger};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const LoginButton = styled(Button)``;
 | 
					export const LoginButton = styled(Button)``;
 | 
				
			||||||
@@ -99,5 +100,5 @@ export const LogoWrapper = styled.div`
 | 
				
			|||||||
  padding-bottom: 16px;
 | 
					  padding-bottom: 16px;
 | 
				
			||||||
  margin-bottom: 24px;
 | 
					  margin-bottom: 24px;
 | 
				
			||||||
  color: rgb(222, 235, 255);
 | 
					  color: rgb(222, 235, 255);
 | 
				
			||||||
  border-bottom: 1px solid rgba(65, 69, 97, 0.65);
 | 
					  border-bottom: 1px solid ${props => mixin.rgba(props.theme.colors.alternate, 0.65)};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,14 +20,14 @@ export const MemberManagerSearch = styled(TextareaAutosize)`
 | 
				
			|||||||
  font-size: 14px;
 | 
					  font-size: 14px;
 | 
				
			||||||
  font-weight: 400;
 | 
					  font-weight: 400;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  background: #262c49;
 | 
					  background: ${props => props.theme.colors.bgColor.secondary};
 | 
				
			||||||
  outline: none;
 | 
					  outline: none;
 | 
				
			||||||
  color: #c2c6dc;
 | 
					  color: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
  border-color: #414561;
 | 
					  border-color: ${props => props.theme.colors.border};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &:focus {
 | 
					  &:focus {
 | 
				
			||||||
    box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px;
 | 
					    box-shadow: ${props => props.theme.colors.primary} 0px 0px 0px 1px;
 | 
				
			||||||
    background: ${mixin.darken('#262c49', 0.15)};
 | 
					    background: ${props => mixin.darken(props.theme.colors.bgColor.secondary, 0.15)};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -66,8 +66,8 @@ export const BoardMemberListItemContent = styled(Member)`
 | 
				
			|||||||
  color: #c2c6dc;
 | 
					  color: #c2c6dc;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    background-color: rgba(${props => props.theme.colors.primary});
 | 
					    background-color: ${props => props.theme.colors.primary};
 | 
				
			||||||
    color: rgba(${props => props.theme.colors.text.secondary});
 | 
					    color: ${props => props.theme.colors.text.secondary};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -80,7 +80,7 @@ export const ProfileIcon = styled.div`
 | 
				
			|||||||
  justify-content: center;
 | 
					  justify-content: center;
 | 
				
			||||||
  color: #c2c6dc;
 | 
					  color: #c2c6dc;
 | 
				
			||||||
  font-weight: 700;
 | 
					  font-weight: 700;
 | 
				
			||||||
  background: rgb(115, 103, 240);
 | 
					  background: ${props => props.theme.colors.primary};
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
  margin-right: 6px;
 | 
					  margin-right: 6px;
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,7 @@
 | 
				
			|||||||
import styled, { css } from 'styled-components';
 | 
					import styled, { css } from 'styled-components';
 | 
				
			||||||
import Button from 'shared/components/Button';
 | 
					import Button from 'shared/components/Button';
 | 
				
			||||||
import { Checkmark } from 'shared/icons';
 | 
					import { Checkmark } from 'shared/icons';
 | 
				
			||||||
 | 
					import { mixin } from 'shared/utils/styles';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const RoleCheckmark = styled(Checkmark)`
 | 
					export const RoleCheckmark = styled(Checkmark)`
 | 
				
			||||||
  padding-left: 4px;
 | 
					  padding-left: 4px;
 | 
				
			||||||
@@ -80,36 +81,36 @@ export const MiniProfileActionItem = styled.span<{ disabled?: boolean }>`
 | 
				
			|||||||
      ? css`
 | 
					      ? css`
 | 
				
			||||||
          user-select: none;
 | 
					          user-select: none;
 | 
				
			||||||
          pointer-events: none;
 | 
					          pointer-events: none;
 | 
				
			||||||
          color: rgba(${props.theme.colors.text.primary}, 0.4);
 | 
					          color: ${mixin.rgba(props.theme.colors.text.primary, 0.4)};
 | 
				
			||||||
        `
 | 
					        `
 | 
				
			||||||
      : css`
 | 
					      : css`
 | 
				
			||||||
          cursor: pointer;
 | 
					          cursor: pointer;
 | 
				
			||||||
          &:hover {
 | 
					          &:hover {
 | 
				
			||||||
            background: rgb(115, 103, 240);
 | 
					            background: ${props.theme.colors.primary};
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        `}
 | 
					        `}
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const CurrentPermission = styled.span`
 | 
					export const CurrentPermission = styled.span`
 | 
				
			||||||
  margin-left: 4px;
 | 
					  margin-left: 4px;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.secondary}, 0.4);
 | 
					  color: ${props => mixin.rgba(props.theme.colors.text.secondary, 0.4)};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const Separator = styled.div`
 | 
					export const Separator = styled.div`
 | 
				
			||||||
  height: 1px;
 | 
					  height: 1px;
 | 
				
			||||||
  border-top: 1px solid #414561;
 | 
					  border-top: 1px solid ${props => props.theme.colors.alternate};
 | 
				
			||||||
  margin: 0.25rem !important;
 | 
					  margin: 0.25rem !important;
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const WarningText = styled.span`
 | 
					export const WarningText = styled.span`
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary}, 0.4);
 | 
					  color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.4)};
 | 
				
			||||||
  padding: 6px;
 | 
					  padding: 6px;
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const DeleteDescription = styled.div`
 | 
					export const DeleteDescription = styled.div`
 | 
				
			||||||
  font-size: 14px;
 | 
					  font-size: 14px;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary});
 | 
					  color: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const RemoveMemberButton = styled(Button)`
 | 
					export const RemoveMemberButton = styled(Button)`
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,9 +30,9 @@ const CloseIcon = styled(Cross)`
 | 
				
			|||||||
  top: 16px;
 | 
					  top: 16px;
 | 
				
			||||||
  right: -32px;
 | 
					  right: -32px;
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
  fill: rgba(${props => props.theme.colors.text.primary});
 | 
					  fill: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    fill: rgba(${props => props.theme.colors.text.secondary});
 | 
					    fill: ${props => props.theme.colors.text.secondary};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,5 @@
 | 
				
			|||||||
import styled, { css } from 'styled-components';
 | 
					import styled, { css } from 'styled-components';
 | 
				
			||||||
 | 
					import { mixin } from 'shared/utils/styles';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const Logo = styled.div``;
 | 
					export const Logo = styled.div``;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -9,7 +10,7 @@ export const LogoTitle = styled.div`
 | 
				
			|||||||
  font-size: 24px;
 | 
					  font-size: 24px;
 | 
				
			||||||
  font-weight: 600;
 | 
					  font-weight: 600;
 | 
				
			||||||
  transition: visibility, opacity, transform 0.25s ease;
 | 
					  transition: visibility, opacity, transform 0.25s ease;
 | 
				
			||||||
  color: #7367f0;
 | 
					  color: #22ff00;
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
export const ActionContainer = styled.div`
 | 
					export const ActionContainer = styled.div`
 | 
				
			||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
@@ -46,8 +47,8 @@ export const ActionButtonWrapper = styled.div<{ active?: boolean }>`
 | 
				
			|||||||
  ${props =>
 | 
					  ${props =>
 | 
				
			||||||
    props.active &&
 | 
					    props.active &&
 | 
				
			||||||
    css`
 | 
					    css`
 | 
				
			||||||
      background: rgb(115, 103, 240);
 | 
					      background: ${props.theme.colors.primary};
 | 
				
			||||||
      box-shadow: 0 0 10px 1px rgba(115, 103, 240, 0.7);
 | 
					      box-shadow: 0 0 10px 1px ${mixin.rgba(props.theme.colors.primary, 0.7)};
 | 
				
			||||||
    `}
 | 
					    `}
 | 
				
			||||||
  border-radius: 6px;
 | 
					  border-radius: 6px;
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
@@ -73,7 +74,7 @@ export const LogoWrapper = styled.div`
 | 
				
			|||||||
  color: rgb(222, 235, 255);
 | 
					  color: rgb(222, 235, 255);
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
  transition: color 0.1s ease 0s, border 0.1s ease 0s;
 | 
					  transition: color 0.1s ease 0s, border 0.1s ease 0s;
 | 
				
			||||||
  border-bottom: 1px solid rgba(65, 69, 97, 0.65);
 | 
					  border-bottom: 1px solid ${props => mixin.rgba(props.theme.colors.alternate, 0.65)};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const Container = styled.aside`
 | 
					export const Container = styled.aside`
 | 
				
			||||||
@@ -87,12 +88,12 @@ export const Container = styled.aside`
 | 
				
			|||||||
  transform: translateZ(0px);
 | 
					  transform: translateZ(0px);
 | 
				
			||||||
  background: #10163a;
 | 
					  background: #10163a;
 | 
				
			||||||
  transition: all 0.1s ease 0s;
 | 
					  transition: all 0.1s ease 0s;
 | 
				
			||||||
  border-right: 1px solid rgba(65, 69, 97, 0.65);
 | 
					  border-right: 1px solid ${props => mixin.rgba(props.theme.colors.alternate, 0.65)};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    width: 260px;
 | 
					    width: 260px;
 | 
				
			||||||
    box-shadow: rgba(0, 0, 0, 0.6) 0px 0px 50px 0px;
 | 
					    box-shadow: rgba(0, 0, 0, 0.6) 0px 0px 50px 0px;
 | 
				
			||||||
    border-right: 1px solid rgba(65, 69, 97, 0);
 | 
					    border-right: 1px solid ${props => mixin.rgba(props.theme.colors.alternate, 0)};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  &:hover ${LogoTitle} {
 | 
					  &:hover ${LogoTitle} {
 | 
				
			||||||
    bottom: -12px;
 | 
					    bottom: -12px;
 | 
				
			||||||
@@ -106,6 +107,6 @@ export const Container = styled.aside`
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &:hover ${LogoWrapper} {
 | 
					  &:hover ${LogoWrapper} {
 | 
				
			||||||
    border-bottom: 1px solid rgba(65, 69, 97, 0);
 | 
					    border-bottom: 1px solid ${props => mixin.rgba(props.theme.colors.alternate, 0)};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,16 +3,17 @@ import styled from 'styled-components';
 | 
				
			|||||||
import { mixin } from 'shared/utils/styles';
 | 
					import { mixin } from 'shared/utils/styles';
 | 
				
			||||||
import Select from 'react-select';
 | 
					import Select from 'react-select';
 | 
				
			||||||
import { ArrowLeft, Cross } from 'shared/icons';
 | 
					import { ArrowLeft, Cross } from 'shared/icons';
 | 
				
			||||||
 | 
					import theme from '../../../App/ThemeStyles';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getBackgroundColor(isDisabled: boolean, isSelected: boolean, isFocused: boolean) {
 | 
					function getBackgroundColor(isDisabled: boolean, isSelected: boolean, isFocused: boolean) {
 | 
				
			||||||
  if (isDisabled) {
 | 
					  if (isDisabled) {
 | 
				
			||||||
    return null;
 | 
					    return null;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  if (isSelected) {
 | 
					  if (isSelected) {
 | 
				
			||||||
    return mixin.darken('#262c49', 0.25);
 | 
					    return mixin.darken(theme.colors.bg.secondary, 0.25);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  if (isFocused) {
 | 
					  if (isFocused) {
 | 
				
			||||||
    return mixin.darken('#262c49', 0.15);
 | 
					    return mixin.darken(theme.colors.bg.secondary, 0.15);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  return null;
 | 
					  return null;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -97,8 +98,8 @@ const ProjectName = styled.input`
 | 
				
			|||||||
  font-weight: 400;
 | 
					  font-weight: 400;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &:focus {
 | 
					  &:focus {
 | 
				
			||||||
    background: ${mixin.darken('#262c49', 0.15)};
 | 
					    background: ${props => mixin.darken(props.theme.colors.bg.secondary, 0.15)};
 | 
				
			||||||
    box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px;
 | 
					    box-shadow: ${props => props.theme.colors.primary} 0px 0px 0px 1px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
const ProjectNameLabel = styled.label`
 | 
					const ProjectNameLabel = styled.label`
 | 
				
			||||||
@@ -126,35 +127,35 @@ const colourStyles = {
 | 
				
			|||||||
  control: (styles: any, data: any) => {
 | 
					  control: (styles: any, data: any) => {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      ...styles,
 | 
					      ...styles,
 | 
				
			||||||
      backgroundColor: data.isMenuOpen ? mixin.darken('#262c49', 0.15) : '#262c49',
 | 
					      backgroundColor: data.isMenuOpen ? mixin.darken(theme.colors.bg.secondary, 0.15) : theme.colors.bg.secondary,
 | 
				
			||||||
      boxShadow: data.menuIsOpen ? 'rgb(115, 103, 240) 0px 0px 0px 1px' : 'none',
 | 
					      boxShadow: data.menuIsOpen ? `${theme.colors.primary} 0px 0px 0px 1px` : 'none',
 | 
				
			||||||
      borderRadius: '3px',
 | 
					      borderRadius: '3px',
 | 
				
			||||||
      borderWidth: '1px',
 | 
					      borderWidth: '1px',
 | 
				
			||||||
      borderStyle: 'solid',
 | 
					      borderStyle: 'solid',
 | 
				
			||||||
      borderImage: 'initial',
 | 
					      borderImage: 'initial',
 | 
				
			||||||
      borderColor: '#414561',
 | 
					      borderColor: theme.colors.alternate,
 | 
				
			||||||
      ':hover': {
 | 
					      ':hover': {
 | 
				
			||||||
        boxShadow: 'rgb(115, 103, 240) 0px 0px 0px 1px',
 | 
					        boxShadow: `${theme.colors.primary} 0px 0px 0px 1px`,
 | 
				
			||||||
        borderRadius: '3px',
 | 
					        borderRadius: '3px',
 | 
				
			||||||
        borderWidth: '1px',
 | 
					        borderWidth: '1px',
 | 
				
			||||||
        borderStyle: 'solid',
 | 
					        borderStyle: 'solid',
 | 
				
			||||||
        borderImage: 'initial',
 | 
					        borderImage: 'initial',
 | 
				
			||||||
        borderColor: '#414561',
 | 
					        borderColor: theme.colors.alternate,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      ':active': {
 | 
					      ':active': {
 | 
				
			||||||
        boxShadow: 'rgb(115, 103, 240) 0px 0px 0px 1px',
 | 
					        boxShadow: `${theme.colors.primary} 0px 0px 0px 1px`,
 | 
				
			||||||
        borderRadius: '3px',
 | 
					        borderRadius: '3px',
 | 
				
			||||||
        borderWidth: '1px',
 | 
					        borderWidth: '1px',
 | 
				
			||||||
        borderStyle: 'solid',
 | 
					        borderStyle: 'solid',
 | 
				
			||||||
        borderImage: 'initial',
 | 
					        borderImage: 'initial',
 | 
				
			||||||
        borderColor: 'rgb(115, 103, 240)',
 | 
					        borderColor: `${theme.colors.primary}`,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  menu: (styles: any) => {
 | 
					  menu: (styles: any) => {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      ...styles,
 | 
					      ...styles,
 | 
				
			||||||
      backgroundColor: mixin.darken('#262c49', 0.15),
 | 
					      backgroundColor: mixin.darken(theme.colors.bg.secondary, 0.15),
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  dropdownIndicator: (styles: any) => ({ ...styles, color: '#c2c6dc', ':hover': { color: '#c2c6dc' } }),
 | 
					  dropdownIndicator: (styles: any) => ({ ...styles, color: '#c2c6dc', ':hover': { color: '#c2c6dc' } }),
 | 
				
			||||||
@@ -167,11 +168,11 @@ const colourStyles = {
 | 
				
			|||||||
      cursor: isDisabled ? 'not-allowed' : 'default',
 | 
					      cursor: isDisabled ? 'not-allowed' : 'default',
 | 
				
			||||||
      ':active': {
 | 
					      ':active': {
 | 
				
			||||||
        ...styles[':active'],
 | 
					        ...styles[':active'],
 | 
				
			||||||
        backgroundColor: !isDisabled && (isSelected ? mixin.darken('#262c49', 0.25) : '#fff'),
 | 
					        backgroundColor: !isDisabled && (isSelected ? mixin.darken(theme.colors.bg.secondary, 0.25) : '#fff'),
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      ':hover': {
 | 
					      ':hover': {
 | 
				
			||||||
        ...styles[':hover'],
 | 
					        ...styles[':hover'],
 | 
				
			||||||
        backgroundColor: !isDisabled && (isSelected ? 'rgb(115, 103, 240)' : 'rgb(115, 103, 240)'),
 | 
					        backgroundColor: !isDisabled && (isSelected ? theme.colors.primary : theme.colors.primary),
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
@@ -209,8 +210,8 @@ const CreateButton = styled.button`
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    color: #fff;
 | 
					    color: #fff;
 | 
				
			||||||
    background: rgb(115, 103, 240);
 | 
					    background: ${props => props.theme.colors.primary};
 | 
				
			||||||
    border-color: rgb(115, 103, 240);
 | 
					    border-color: ${props => props.theme.colors.primary};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
type NewProjectProps = {
 | 
					type NewProjectProps = {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -37,7 +37,7 @@ const ItemTextContainer = styled.div`
 | 
				
			|||||||
const ItemTextTitle = styled.span`
 | 
					const ItemTextTitle = styled.span`
 | 
				
			||||||
  font-weight: 500;
 | 
					  font-weight: 500;
 | 
				
			||||||
  display: block;
 | 
					  display: block;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.primary});
 | 
					  color: ${props => props.theme.colors.primary};
 | 
				
			||||||
  font-size: 14px;
 | 
					  font-size: 14px;
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
const ItemTextDesc = styled.span`
 | 
					const ItemTextDesc = styled.span`
 | 
				
			||||||
@@ -76,21 +76,21 @@ const NotificationHeader = styled.div`
 | 
				
			|||||||
  text-align: center;
 | 
					  text-align: center;
 | 
				
			||||||
  border-top-left-radius: 6px;
 | 
					  border-top-left-radius: 6px;
 | 
				
			||||||
  border-top-right-radius: 6px;
 | 
					  border-top-right-radius: 6px;
 | 
				
			||||||
  background: rgba(${props => props.theme.colors.primary});
 | 
					  background: ${props => props.theme.colors.primary};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const NotificationHeaderTitle = styled.span`
 | 
					const NotificationHeaderTitle = styled.span`
 | 
				
			||||||
  font-size: 14px;
 | 
					  font-size: 14px;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.secondary});
 | 
					  color: ${props => props.theme.colors.text.secondary};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const NotificationFooter = styled.div`
 | 
					const NotificationFooter = styled.div`
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
  padding: 0.5rem;
 | 
					  padding: 0.5rem;
 | 
				
			||||||
  text-align: center;
 | 
					  text-align: center;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.primary});
 | 
					  color: ${props => props.theme.colors.primary};
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    background: #10163a;
 | 
					    background: ${props => props.theme.colors.bg.primary};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  border-bottom-left-radius: 6px;
 | 
					  border-bottom-left-radius: 6px;
 | 
				
			||||||
  border-bottom-right-radius: 6px;
 | 
					  border-bottom-right-radius: 6px;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,7 +4,7 @@ import styled from 'styled-components';
 | 
				
			|||||||
import { SaveButton, DeleteButton, LabelBox, EditLabelForm, FieldLabel, FieldName } from './Styles';
 | 
					import { SaveButton, DeleteButton, LabelBox, EditLabelForm, FieldLabel, FieldName } from './Styles';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const WhiteCheckmark = styled(Checkmark)`
 | 
					const WhiteCheckmark = styled(Checkmark)`
 | 
				
			||||||
  fill: rgba(${props => props.theme.colors.text.secondary});
 | 
					  fill: ${props => props.theme.colors.text.secondary};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
type Props = {
 | 
					type Props = {
 | 
				
			||||||
  labelColors: Array<LabelColor>;
 | 
					  labelColors: Array<LabelColor>;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,7 @@
 | 
				
			|||||||
import styled, { css } from 'styled-components';
 | 
					import styled, { css } from 'styled-components';
 | 
				
			||||||
import { mixin } from 'shared/utils/styles';
 | 
					import { mixin } from 'shared/utils/styles';
 | 
				
			||||||
import ControlledInput from 'shared/components/ControlledInput';
 | 
					import ControlledInput from 'shared/components/ControlledInput';
 | 
				
			||||||
 | 
					import theme from 'App/ThemeStyles';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const Container = styled.div<{
 | 
					export const Container = styled.div<{
 | 
				
			||||||
  invertY: boolean;
 | 
					  invertY: boolean;
 | 
				
			||||||
@@ -176,7 +177,7 @@ export const LabelIcon = styled.div`
 | 
				
			|||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    background: rgb(115, 103, 240);
 | 
					    background: ${props => props.theme.colors.primary};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -233,8 +234,8 @@ export const FieldName = styled.input`
 | 
				
			|||||||
  font-weight: 400;
 | 
					  font-weight: 400;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &:focus {
 | 
					  &:focus {
 | 
				
			||||||
    box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px;
 | 
					    box-shadow: ${props => props.theme.colors.primary} 0px 0px 0px 1px;
 | 
				
			||||||
    background: ${mixin.darken('#262c49', 0.15)};
 | 
					    background: ${mixin.darken(theme.colors.bg.secondary, 0.15)};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -258,7 +259,7 @@ export const LabelBox = styled.span<{ color: string }>`
 | 
				
			|||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const SaveButton = styled.input`
 | 
					export const SaveButton = styled.input`
 | 
				
			||||||
  background: rgb(115, 103, 240);
 | 
					  background: ${props => props.theme.colors.primary};
 | 
				
			||||||
  box-shadow: none;
 | 
					  box-shadow: none;
 | 
				
			||||||
  border: none;
 | 
					  border: none;
 | 
				
			||||||
  color: #fff;
 | 
					  color: #fff;
 | 
				
			||||||
@@ -296,7 +297,7 @@ export const DeleteButton = styled.input`
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    color: #fff;
 | 
					    color: #fff;
 | 
				
			||||||
    background: rgb(115, 103, 240);
 | 
					    background: ${props => props.theme.colors.primary};
 | 
				
			||||||
    border-color: transparent;
 | 
					    border-color: transparent;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
@@ -317,7 +318,7 @@ export const CreateLabelButton = styled.button`
 | 
				
			|||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    background: rgb(115, 103, 240);
 | 
					    background: ${props => props.theme.colors.primary};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,7 @@ import useOnOutsideClick from 'shared/hooks/onOutsideClick';
 | 
				
			|||||||
import { createPortal } from 'react-dom';
 | 
					import { createPortal } from 'react-dom';
 | 
				
			||||||
import NOOP from 'shared/utils/noop';
 | 
					import NOOP from 'shared/utils/noop';
 | 
				
			||||||
import produce from 'immer';
 | 
					import produce from 'immer';
 | 
				
			||||||
 | 
					import theme from 'App/ThemeStyles';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  Container,
 | 
					  Container,
 | 
				
			||||||
  ContainerDiamond,
 | 
					  ContainerDiamond,
 | 
				
			||||||
@@ -18,7 +19,7 @@ import {
 | 
				
			|||||||
function getPopupOptions(options?: PopupOptions) {
 | 
					function getPopupOptions(options?: PopupOptions) {
 | 
				
			||||||
  const popupOptions = {
 | 
					  const popupOptions = {
 | 
				
			||||||
    borders: true,
 | 
					    borders: true,
 | 
				
			||||||
    diamondColor: '#262c49',
 | 
					    diamondColor: theme.colors.bg.secondary,
 | 
				
			||||||
    targetPadding: '10px',
 | 
					    targetPadding: '10px',
 | 
				
			||||||
    showDiamond: true,
 | 
					    showDiamond: true,
 | 
				
			||||||
    width: 316,
 | 
					    width: 316,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,7 +24,7 @@ export const ListActionItem = styled.span`
 | 
				
			|||||||
  margin: 0 -12px;
 | 
					  margin: 0 -12px;
 | 
				
			||||||
  text-decoration: none;
 | 
					  text-decoration: none;
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    background: rgb(115, 103, 240);
 | 
					    background: ${props => props.theme.colors.primary};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,4 @@
 | 
				
			|||||||
import styled, { keyframes, css } from 'styled-components';
 | 
					import styled, { keyframes, css } from 'styled-components';
 | 
				
			||||||
import TextareaAutosize from 'react-autosize-textarea';
 | 
					 | 
				
			||||||
import { mixin } from 'shared/utils/styles';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const Wrapper = styled.div<{ open: boolean }>`
 | 
					export const Wrapper = styled.div<{ open: boolean }>`
 | 
				
			||||||
  background: rgba(0, 0, 0, 0.55);
 | 
					  background: rgba(0, 0, 0, 0.55);
 | 
				
			||||||
@@ -30,7 +28,7 @@ export const Container = styled.div<{ fixed: boolean; width: number; top: number
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const SaveButton = styled.button`
 | 
					export const SaveButton = styled.button`
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
  background: rgb(115, 103, 240);
 | 
					  background: ${props => props.theme.colors.primary};
 | 
				
			||||||
  box-shadow: none;
 | 
					  box-shadow: none;
 | 
				
			||||||
  border: none;
 | 
					  border: none;
 | 
				
			||||||
  color: #fff;
 | 
					  color: #fff;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,6 @@
 | 
				
			|||||||
import styled from 'styled-components';
 | 
					import styled from 'styled-components';
 | 
				
			||||||
import Button from 'shared/components/Button';
 | 
					import Button from 'shared/components/Button';
 | 
				
			||||||
 | 
					import { mixin } from 'shared/utils/styles';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const Wrapper = styled.div`
 | 
					export const Wrapper = styled.div`
 | 
				
			||||||
  background: #eff2f7;
 | 
					  background: #eff2f7;
 | 
				
			||||||
@@ -68,7 +69,7 @@ export const FormIcon = styled.div`
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const FormError = styled.span`
 | 
					export const FormError = styled.span`
 | 
				
			||||||
  font-size: 0.875rem;
 | 
					  font-size: 0.875rem;
 | 
				
			||||||
  color: rgb(234, 84, 85);
 | 
					  color: ${props => props.theme.colors.danger};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const LoginButton = styled(Button)``;
 | 
					export const LoginButton = styled(Button)``;
 | 
				
			||||||
@@ -99,5 +100,5 @@ export const LogoWrapper = styled.div`
 | 
				
			|||||||
  padding-bottom: 16px;
 | 
					  padding-bottom: 16px;
 | 
				
			||||||
  margin-bottom: 24px;
 | 
					  margin-bottom: 24px;
 | 
				
			||||||
  color: rgb(222, 235, 255);
 | 
					  color: rgb(222, 235, 255);
 | 
				
			||||||
  border-bottom: 1px solid rgba(65, 69, 97, 0.65);
 | 
					  border-bottom: 1px solid ${props => mixin.rgba(props.theme.colors.alternate, 0.65)};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,7 +24,7 @@ import {
 | 
				
			|||||||
const EMAIL_PATTERN = /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/i;
 | 
					const EMAIL_PATTERN = /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/i;
 | 
				
			||||||
const INITIALS_PATTERN = /[a-zA-Z]{2,3}/i;
 | 
					const INITIALS_PATTERN = /[a-zA-Z]{2,3}/i;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Register = ({ onSubmit }: RegisterProps) => {
 | 
					const Register = ({ onSubmit, registered = false }: RegisterProps) => {
 | 
				
			||||||
  const [isComplete, setComplete] = useState(true);
 | 
					  const [isComplete, setComplete] = useState(true);
 | 
				
			||||||
  const { register, handleSubmit, errors, setError } = useForm<RegisterFormData>();
 | 
					  const { register, handleSubmit, errors, setError } = useForm<RegisterFormData>();
 | 
				
			||||||
  const loginSubmit = (data: RegisterFormData) => {
 | 
					  const loginSubmit = (data: RegisterFormData) => {
 | 
				
			||||||
@@ -43,103 +43,112 @@ const Register = ({ onSubmit }: RegisterProps) => {
 | 
				
			|||||||
              <Taskcafe width={42} height={42} />
 | 
					              <Taskcafe width={42} height={42} />
 | 
				
			||||||
              <LogoTitle>Taskcafé</LogoTitle>
 | 
					              <LogoTitle>Taskcafé</LogoTitle>
 | 
				
			||||||
            </LogoWrapper>
 | 
					            </LogoWrapper>
 | 
				
			||||||
            <Title>Register</Title>
 | 
					            {registered ? (
 | 
				
			||||||
            <SubTitle>Please create the system admin user</SubTitle>
 | 
					              <>
 | 
				
			||||||
            <Form onSubmit={handleSubmit(loginSubmit)}>
 | 
					                <Title>Thanks for registering</Title>
 | 
				
			||||||
              <FormLabel htmlFor="fullname">
 | 
					                <SubTitle>Please check your inbox for a confirmation email.</SubTitle>
 | 
				
			||||||
                Full name
 | 
					              </>
 | 
				
			||||||
                <FormTextInput
 | 
					            ) : (
 | 
				
			||||||
                  type="text"
 | 
					              <>
 | 
				
			||||||
                  id="fullname"
 | 
					                <Title>Register</Title>
 | 
				
			||||||
                  name="fullname"
 | 
					                <SubTitle>Please create your user</SubTitle>
 | 
				
			||||||
                  ref={register({ required: 'Full name is required' })}
 | 
					                <Form onSubmit={handleSubmit(loginSubmit)}>
 | 
				
			||||||
                />
 | 
					                  <FormLabel htmlFor="fullname">
 | 
				
			||||||
                <FormIcon>
 | 
					                    Full name
 | 
				
			||||||
                  <User width={20} height={20} />
 | 
					                    <FormTextInput
 | 
				
			||||||
                </FormIcon>
 | 
					                      type="text"
 | 
				
			||||||
              </FormLabel>
 | 
					                      id="fullname"
 | 
				
			||||||
              {errors.username && <FormError>{errors.username.message}</FormError>}
 | 
					                      name="fullname"
 | 
				
			||||||
              <FormLabel htmlFor="username">
 | 
					                      ref={register({ required: 'Full name is required' })}
 | 
				
			||||||
                Username
 | 
					                    />
 | 
				
			||||||
                <FormTextInput
 | 
					                    <FormIcon>
 | 
				
			||||||
                  type="text"
 | 
					                      <User width={20} height={20} />
 | 
				
			||||||
                  id="username"
 | 
					                    </FormIcon>
 | 
				
			||||||
                  name="username"
 | 
					                  </FormLabel>
 | 
				
			||||||
                  ref={register({ required: 'Username is required' })}
 | 
					                  {errors.username && <FormError>{errors.username.message}</FormError>}
 | 
				
			||||||
                />
 | 
					                  <FormLabel htmlFor="username">
 | 
				
			||||||
                <FormIcon>
 | 
					                    Username
 | 
				
			||||||
                  <User width={20} height={20} />
 | 
					                    <FormTextInput
 | 
				
			||||||
                </FormIcon>
 | 
					                      type="text"
 | 
				
			||||||
              </FormLabel>
 | 
					                      id="username"
 | 
				
			||||||
              {errors.username && <FormError>{errors.username.message}</FormError>}
 | 
					                      name="username"
 | 
				
			||||||
              <FormLabel htmlFor="email">
 | 
					                      ref={register({ required: 'Username is required' })}
 | 
				
			||||||
                Email
 | 
					                    />
 | 
				
			||||||
                <FormTextInput
 | 
					                    <FormIcon>
 | 
				
			||||||
                  type="text"
 | 
					                      <User width={20} height={20} />
 | 
				
			||||||
                  id="email"
 | 
					                    </FormIcon>
 | 
				
			||||||
                  name="email"
 | 
					                  </FormLabel>
 | 
				
			||||||
                  ref={register({
 | 
					                  {errors.username && <FormError>{errors.username.message}</FormError>}
 | 
				
			||||||
                    required: 'Email is required',
 | 
					                  <FormLabel htmlFor="email">
 | 
				
			||||||
                    pattern: { value: EMAIL_PATTERN, message: 'Must be a valid email' },
 | 
					                    Email
 | 
				
			||||||
                  })}
 | 
					                    <FormTextInput
 | 
				
			||||||
                />
 | 
					                      type="text"
 | 
				
			||||||
                <FormIcon>
 | 
					                      id="email"
 | 
				
			||||||
                  <User width={20} height={20} />
 | 
					                      name="email"
 | 
				
			||||||
                </FormIcon>
 | 
					                      ref={register({
 | 
				
			||||||
              </FormLabel>
 | 
					                        required: 'Email is required',
 | 
				
			||||||
              {errors.email && <FormError>{errors.email.message}</FormError>}
 | 
					                        pattern: { value: EMAIL_PATTERN, message: 'Must be a valid email' },
 | 
				
			||||||
              <FormLabel htmlFor="initials">
 | 
					                      })}
 | 
				
			||||||
                Initials
 | 
					                    />
 | 
				
			||||||
                <FormTextInput
 | 
					                    <FormIcon>
 | 
				
			||||||
                  type="text"
 | 
					                      <User width={20} height={20} />
 | 
				
			||||||
                  id="initials"
 | 
					                    </FormIcon>
 | 
				
			||||||
                  name="initials"
 | 
					                  </FormLabel>
 | 
				
			||||||
                  ref={register({
 | 
					                  {errors.email && <FormError>{errors.email.message}</FormError>}
 | 
				
			||||||
                    required: 'Initials is required',
 | 
					                  <FormLabel htmlFor="initials">
 | 
				
			||||||
                    pattern: {
 | 
					                    Initials
 | 
				
			||||||
                      value: INITIALS_PATTERN,
 | 
					                    <FormTextInput
 | 
				
			||||||
                      message: 'Initials must be between 2 to 3 characters.',
 | 
					                      type="text"
 | 
				
			||||||
                    },
 | 
					                      id="initials"
 | 
				
			||||||
                  })}
 | 
					                      name="initials"
 | 
				
			||||||
                />
 | 
					                      ref={register({
 | 
				
			||||||
                <FormIcon>
 | 
					                        required: 'Initials is required',
 | 
				
			||||||
                  <User width={20} height={20} />
 | 
					                        pattern: {
 | 
				
			||||||
                </FormIcon>
 | 
					                          value: INITIALS_PATTERN,
 | 
				
			||||||
              </FormLabel>
 | 
					                          message: 'Initials must be between 2 to 3 characters.',
 | 
				
			||||||
              {errors.initials && <FormError>{errors.initials.message}</FormError>}
 | 
					                        },
 | 
				
			||||||
              <FormLabel htmlFor="password">
 | 
					                      })}
 | 
				
			||||||
                Password
 | 
					                    />
 | 
				
			||||||
                <FormTextInput
 | 
					                    <FormIcon>
 | 
				
			||||||
                  type="password"
 | 
					                      <User width={20} height={20} />
 | 
				
			||||||
                  id="password"
 | 
					                    </FormIcon>
 | 
				
			||||||
                  name="password"
 | 
					                  </FormLabel>
 | 
				
			||||||
                  ref={register({ required: 'Password is required' })}
 | 
					                  {errors.initials && <FormError>{errors.initials.message}</FormError>}
 | 
				
			||||||
                />
 | 
					                  <FormLabel htmlFor="password">
 | 
				
			||||||
                <FormIcon>
 | 
					                    Password
 | 
				
			||||||
                  <Lock width={20} height={20} />
 | 
					                    <FormTextInput
 | 
				
			||||||
                </FormIcon>
 | 
					                      type="password"
 | 
				
			||||||
              </FormLabel>
 | 
					                      id="password"
 | 
				
			||||||
              {errors.password && <FormError>{errors.password.message}</FormError>}
 | 
					                      name="password"
 | 
				
			||||||
              <FormLabel htmlFor="password_confirm">
 | 
					                      ref={register({ required: 'Password is required' })}
 | 
				
			||||||
                Password (Confirm)
 | 
					                    />
 | 
				
			||||||
                <FormTextInput
 | 
					                    <FormIcon>
 | 
				
			||||||
                  type="password"
 | 
					                      <Lock width={20} height={20} />
 | 
				
			||||||
                  id="password_confirm"
 | 
					                    </FormIcon>
 | 
				
			||||||
                  name="password_confirm"
 | 
					                  </FormLabel>
 | 
				
			||||||
                  ref={register({ required: 'Password (confirm) is required' })}
 | 
					                  {errors.password && <FormError>{errors.password.message}</FormError>}
 | 
				
			||||||
                />
 | 
					                  <FormLabel htmlFor="password_confirm">
 | 
				
			||||||
                <FormIcon>
 | 
					                    Password (Confirm)
 | 
				
			||||||
                  <Lock width={20} height={20} />
 | 
					                    <FormTextInput
 | 
				
			||||||
                </FormIcon>
 | 
					                      type="password"
 | 
				
			||||||
              </FormLabel>
 | 
					                      id="password_confirm"
 | 
				
			||||||
              {errors.password_confirm && <FormError>{errors.password_confirm.message}</FormError>}
 | 
					                      name="password_confirm"
 | 
				
			||||||
 | 
					                      ref={register({ required: 'Password (confirm) is required' })}
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                    <FormIcon>
 | 
				
			||||||
 | 
					                      <Lock width={20} height={20} />
 | 
				
			||||||
 | 
					                    </FormIcon>
 | 
				
			||||||
 | 
					                  </FormLabel>
 | 
				
			||||||
 | 
					                  {errors.password_confirm && <FormError>{errors.password_confirm.message}</FormError>}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              <ActionButtons>
 | 
					                  <ActionButtons>
 | 
				
			||||||
                <RegisterButton type="submit" disabled={!isComplete}>
 | 
					                    <RegisterButton type="submit" disabled={!isComplete}>
 | 
				
			||||||
                  Register
 | 
					                      Register
 | 
				
			||||||
                </RegisterButton>
 | 
					                    </RegisterButton>
 | 
				
			||||||
              </ActionButtons>
 | 
					                  </ActionButtons>
 | 
				
			||||||
            </Form>
 | 
					                </Form>
 | 
				
			||||||
 | 
					              </>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
          </LoginFormContainer>
 | 
					          </LoginFormContainer>
 | 
				
			||||||
        </LoginFormWrapper>
 | 
					        </LoginFormWrapper>
 | 
				
			||||||
      </Column>
 | 
					      </Column>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,16 +2,17 @@ import React from 'react';
 | 
				
			|||||||
import Select from 'react-select';
 | 
					import Select from 'react-select';
 | 
				
			||||||
import styled from 'styled-components';
 | 
					import styled from 'styled-components';
 | 
				
			||||||
import { mixin } from 'shared/utils/styles';
 | 
					import { mixin } from 'shared/utils/styles';
 | 
				
			||||||
 | 
					import theme from 'App/ThemeStyles';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getBackgroundColor(isDisabled: boolean, isSelected: boolean, isFocused: boolean) {
 | 
					function getBackgroundColor(isDisabled: boolean, isSelected: boolean, isFocused: boolean) {
 | 
				
			||||||
  if (isDisabled) {
 | 
					  if (isDisabled) {
 | 
				
			||||||
    return null;
 | 
					    return null;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  if (isSelected) {
 | 
					  if (isSelected) {
 | 
				
			||||||
    return mixin.darken('#262c49', 0.25);
 | 
					    return mixin.darken(theme.colors.bg.secondary, 0.25);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  if (isFocused) {
 | 
					  if (isFocused) {
 | 
				
			||||||
    return mixin.darken('#262c49', 0.15);
 | 
					    return mixin.darken(theme.colors.bg.secondary, 0.15);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  return null;
 | 
					  return null;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -20,35 +21,35 @@ export const colourStyles = {
 | 
				
			|||||||
  control: (styles: any, data: any) => {
 | 
					  control: (styles: any, data: any) => {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      ...styles,
 | 
					      ...styles,
 | 
				
			||||||
      backgroundColor: data.isMenuOpen ? mixin.darken('#262c49', 0.15) : '#262c49',
 | 
					      backgroundColor: data.isMenuOpen ? mixin.darken(theme.colors.bg.secondary, 0.15) : theme.colors.bg.secondary,
 | 
				
			||||||
      boxShadow: data.menuIsOpen ? 'rgb(115, 103, 240) 0px 0px 0px 1px' : 'none',
 | 
					      boxShadow: data.menuIsOpen ? `${theme.colors.primary} 0px 0px 0px 1px` : 'none',
 | 
				
			||||||
      borderRadius: '3px',
 | 
					      borderRadius: '3px',
 | 
				
			||||||
      borderWidth: '1px',
 | 
					      borderWidth: '1px',
 | 
				
			||||||
      borderStyle: 'solid',
 | 
					      borderStyle: 'solid',
 | 
				
			||||||
      borderImage: 'initial',
 | 
					      borderImage: 'initial',
 | 
				
			||||||
      borderColor: '#414561',
 | 
					      borderColor: theme.colors.alternate,
 | 
				
			||||||
      ':hover': {
 | 
					      ':hover': {
 | 
				
			||||||
        boxShadow: 'rgb(115, 103, 240) 0px 0px 0px 1px',
 | 
					        boxShadow: `${theme.colors.primary} 0px 0px 0px 1px`,
 | 
				
			||||||
        borderRadius: '3px',
 | 
					        borderRadius: '3px',
 | 
				
			||||||
        borderWidth: '1px',
 | 
					        borderWidth: '1px',
 | 
				
			||||||
        borderStyle: 'solid',
 | 
					        borderStyle: 'solid',
 | 
				
			||||||
        borderImage: 'initial',
 | 
					        borderImage: 'initial',
 | 
				
			||||||
        borderColor: '#414561',
 | 
					        borderColor: theme.colors.alternate,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      ':active': {
 | 
					      ':active': {
 | 
				
			||||||
        boxShadow: 'rgb(115, 103, 240) 0px 0px 0px 1px',
 | 
					        boxShadow: `${theme.colors.primary} 0px 0px 0px 1px`,
 | 
				
			||||||
        borderRadius: '3px',
 | 
					        borderRadius: '3px',
 | 
				
			||||||
        borderWidth: '1px',
 | 
					        borderWidth: '1px',
 | 
				
			||||||
        borderStyle: 'solid',
 | 
					        borderStyle: 'solid',
 | 
				
			||||||
        borderImage: 'initial',
 | 
					        borderImage: 'initial',
 | 
				
			||||||
        borderColor: 'rgb(115, 103, 240)',
 | 
					        borderColor: `${theme.colors.primary}`,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  menu: (styles: any) => {
 | 
					  menu: (styles: any) => {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      ...styles,
 | 
					      ...styles,
 | 
				
			||||||
      backgroundColor: mixin.darken('#262c49', 0.15),
 | 
					      backgroundColor: mixin.darken(theme.colors.bg.secondary, 0.15),
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  dropdownIndicator: (styles: any) => ({ ...styles, color: '#c2c6dc', ':hover': { color: '#c2c6dc' } }),
 | 
					  dropdownIndicator: (styles: any) => ({ ...styles, color: '#c2c6dc', ':hover': { color: '#c2c6dc' } }),
 | 
				
			||||||
@@ -61,11 +62,11 @@ export const colourStyles = {
 | 
				
			|||||||
      cursor: isDisabled ? 'not-allowed' : 'default',
 | 
					      cursor: isDisabled ? 'not-allowed' : 'default',
 | 
				
			||||||
      ':active': {
 | 
					      ':active': {
 | 
				
			||||||
        ...styles[':active'],
 | 
					        ...styles[':active'],
 | 
				
			||||||
        backgroundColor: !isDisabled && (isSelected ? mixin.darken('#262c49', 0.25) : '#fff'),
 | 
					        backgroundColor: !isDisabled && (isSelected ? mixin.darken(theme.colors.bg.secondary, 0.25) : '#fff'),
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      ':hover': {
 | 
					      ':hover': {
 | 
				
			||||||
        ...styles[':hover'],
 | 
					        ...styles[':hover'],
 | 
				
			||||||
        backgroundColor: !isDisabled && (isSelected ? 'rgb(115, 103, 240)' : 'rgb(115, 103, 240)'),
 | 
					        backgroundColor: !isDisabled && (isSelected ? theme.colors.primary : theme.colors.primary),
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
@@ -86,7 +87,7 @@ export const colourStyles = {
 | 
				
			|||||||
const InputLabel = styled.span<{ width: string }>`
 | 
					const InputLabel = styled.span<{ width: string }>`
 | 
				
			||||||
width: ${props => props.width};
 | 
					width: ${props => props.width};
 | 
				
			||||||
padding-left: 0.7rem;
 | 
					padding-left: 0.7rem;
 | 
				
			||||||
color: rgba(115, 103, 240);
 | 
					color: ${props => props.theme.colors.primary};
 | 
				
			||||||
left: 0;
 | 
					left: 0;
 | 
				
			||||||
top: 0;
 | 
					top: 0;
 | 
				
			||||||
transition: all 0.2s ease;
 | 
					transition: all 0.2s ease;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,7 +17,7 @@ const UserInfoInput = styled(Input)`
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const FormError = styled.span`
 | 
					const FormError = styled.span`
 | 
				
			||||||
  font-size: 12px;
 | 
					  font-size: 12px;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.warning});
 | 
					  color: ${props => props.theme.colors.warning};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ProfileContainer = styled.div`
 | 
					const ProfileContainer = styled.div`
 | 
				
			||||||
@@ -152,12 +152,12 @@ const TabNavItemButton = styled.button<{ active: boolean }>`
 | 
				
			|||||||
  width: 100%;
 | 
					  width: 100%;
 | 
				
			||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  color: ${props => (props.active ? 'rgba(115, 103, 240)' : '#c2c6dc')};
 | 
					  color: ${props => (props.active ? `${props.theme.colors.primary}` : '#c2c6dc')};
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    color: rgba(115, 103, 240);
 | 
					    color: ${props => props.theme.colors.primary};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  &:hover svg {
 | 
					  &:hover svg {
 | 
				
			||||||
    fill: rgba(115, 103, 240);
 | 
					    fill: ${props => props.theme.colors.primary};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -175,8 +175,8 @@ const TabNavLine = styled.span<{ top: number }>`
 | 
				
			|||||||
  transform: scaleX(1);
 | 
					  transform: scaleX(1);
 | 
				
			||||||
  top: ${props => props.top}px;
 | 
					  top: ${props => props.top}px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  background: linear-gradient(30deg, rgba(115, 103, 240), rgba(115, 103, 240));
 | 
					  background: linear-gradient(30deg, ${props => props.theme.colors.primary}, ${props => props.theme.colors.primary});
 | 
				
			||||||
  box-shadow: 0 0 8px 0 rgba(115, 103, 240);
 | 
					  box-shadow: 0 0 8px 0 ${props => props.theme.colors.primary};
 | 
				
			||||||
  display: block;
 | 
					  display: block;
 | 
				
			||||||
  position: absolute;
 | 
					  position: absolute;
 | 
				
			||||||
  transition: all 0.2s ease;
 | 
					  transition: all 0.2s ease;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -36,7 +36,7 @@ export const Wrapper = styled.div<{
 | 
				
			|||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
  justify-content: center;
 | 
					  justify-content: center;
 | 
				
			||||||
  color: rgba(${props => (props.backgroundURL ? props.theme.colors.text.primary : '0,0,0')});
 | 
					  color: ${props => (props.backgroundURL ? props.theme.colors.text.primary : 'rgb(0,0,0)')};
 | 
				
			||||||
  background: ${props => (props.backgroundURL ? `url(${props.backgroundURL})` : props.bgColor)};
 | 
					  background: ${props => (props.backgroundURL ? `url(${props.backgroundURL})` : props.bgColor)};
 | 
				
			||||||
  background-position: center;
 | 
					  background-position: center;
 | 
				
			||||||
  background-size: contain;
 | 
					  background-size: contain;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,8 +3,6 @@ import TextareaAutosize from 'react-autosize-textarea';
 | 
				
			|||||||
import { mixin } from 'shared/utils/styles';
 | 
					import { mixin } from 'shared/utils/styles';
 | 
				
			||||||
import Button from 'shared/components/Button';
 | 
					import Button from 'shared/components/Button';
 | 
				
			||||||
import TaskAssignee from 'shared/components/TaskAssignee';
 | 
					import TaskAssignee from 'shared/components/TaskAssignee';
 | 
				
			||||||
import { User, Trash, Paperclip } from 'shared/icons';
 | 
					 | 
				
			||||||
import Member from 'shared/components/Member';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const Container = styled.div`
 | 
					export const Container = styled.div`
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
@@ -33,35 +31,35 @@ export const MarkCompleteButton = styled.button<{ invert: boolean }>`
 | 
				
			|||||||
  ${props =>
 | 
					  ${props =>
 | 
				
			||||||
    props.invert
 | 
					    props.invert
 | 
				
			||||||
      ? css`
 | 
					      ? css`
 | 
				
			||||||
          background: rgba(${props.theme.colors.success});
 | 
					          background: ${props.theme.colors.success};
 | 
				
			||||||
          & svg {
 | 
					          & svg {
 | 
				
			||||||
            fill: rgba(${props.theme.colors.text.secondary});
 | 
					            fill: ${props.theme.colors.text.secondary};
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          & span {
 | 
					          & span {
 | 
				
			||||||
            color: rgba(${props.theme.colors.text.secondary});
 | 
					            color: ${props.theme.colors.text.secondary};
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          &:hover {
 | 
					          &:hover {
 | 
				
			||||||
            background: rgba(${props.theme.colors.success}, 0.8);
 | 
					            background: ${mixin.rgba(props.theme.colors.success, 0.8)};
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        `
 | 
					        `
 | 
				
			||||||
      : css`
 | 
					      : css`
 | 
				
			||||||
          background: none;
 | 
					          background: none;
 | 
				
			||||||
          border: 1px solid rgba(${props.theme.colors.text.secondary});
 | 
					          border: 1px solid ${props.theme.colors.text.secondary};
 | 
				
			||||||
          & svg {
 | 
					          & svg {
 | 
				
			||||||
            fill: rgba(${props.theme.colors.text.secondary});
 | 
					            fill: ${props.theme.colors.text.secondary};
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          & span {
 | 
					          & span {
 | 
				
			||||||
            color: rgba(${props.theme.colors.text.secondary});
 | 
					            color: ${props.theme.colors.text.secondary};
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          &:hover {
 | 
					          &:hover {
 | 
				
			||||||
            background: rgba(${props.theme.colors.success}, 0.08);
 | 
					            background: ${mixin.rgba(props.theme.colors.success, 0.08)};
 | 
				
			||||||
            border: 1px solid rgba(${props.theme.colors.success});
 | 
					            border: 1px solid ${props.theme.colors.success};
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          &:hover svg {
 | 
					          &:hover svg {
 | 
				
			||||||
            fill: rgba(${props.theme.colors.success});
 | 
					            fill: ${props.theme.colors.success};
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          &:hover span {
 | 
					          &:hover span {
 | 
				
			||||||
            color: rgba(${props.theme.colors.success});
 | 
					            color: ${props.theme.colors.success};
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        `}
 | 
					        `}
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
@@ -85,7 +83,7 @@ export const SidebarTitle = styled.div`
 | 
				
			|||||||
  font-size: 12px;
 | 
					  font-size: 12px;
 | 
				
			||||||
  min-height: 24px;
 | 
					  min-height: 24px;
 | 
				
			||||||
  margin-left: 8px;
 | 
					  margin-left: 8px;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary}, 0.75);
 | 
					  color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.75)};
 | 
				
			||||||
  padding-top: 4px;
 | 
					  padding-top: 4px;
 | 
				
			||||||
  letter-spacing: 0.5px;
 | 
					  letter-spacing: 0.5px;
 | 
				
			||||||
  text-transform: uppercase;
 | 
					  text-transform: uppercase;
 | 
				
			||||||
@@ -93,7 +91,7 @@ export const SidebarTitle = styled.div`
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const SidebarButton = styled.div`
 | 
					export const SidebarButton = styled.div`
 | 
				
			||||||
  font-size: 14px;
 | 
					  font-size: 14px;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary});
 | 
					  color: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
  min-height: 32px;
 | 
					  min-height: 32px;
 | 
				
			||||||
  width: 100%;
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -168,7 +166,7 @@ export const TaskDetailsTitle = styled(TextareaAutosize)`
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &:focus {
 | 
					  &:focus {
 | 
				
			||||||
    border-color: rgba(${props => props.theme.colors.primary});
 | 
					    border-color: ${props => props.theme.colors.primary};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -176,7 +174,7 @@ export const DueDateTitle = styled.div`
 | 
				
			|||||||
  font-size: 12px;
 | 
					  font-size: 12px;
 | 
				
			||||||
  min-height: 24px;
 | 
					  min-height: 24px;
 | 
				
			||||||
  margin-left: 8px;
 | 
					  margin-left: 8px;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary}, 0.75);
 | 
					  color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.75)};
 | 
				
			||||||
  padding-top: 8px;
 | 
					  padding-top: 8px;
 | 
				
			||||||
  letter-spacing: 0.5px;
 | 
					  letter-spacing: 0.5px;
 | 
				
			||||||
  text-transform: uppercase;
 | 
					  text-transform: uppercase;
 | 
				
			||||||
@@ -187,7 +185,7 @@ export const AssignedUsersSection = styled.div`
 | 
				
			|||||||
  padding-right: 32px;
 | 
					  padding-right: 32px;
 | 
				
			||||||
  padding-top: 24px;
 | 
					  padding-top: 24px;
 | 
				
			||||||
  padding-bottom: 24px;
 | 
					  padding-bottom: 24px;
 | 
				
			||||||
  border-bottom: 1px solid #414561;
 | 
					  border-bottom: 1px solid ${props => props.theme.colors.alternate};
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  flex-direction: column;
 | 
					  flex-direction: column;
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
@@ -205,10 +203,10 @@ export const AssignUserIcon = styled.div`
 | 
				
			|||||||
  justify-content: center;
 | 
					  justify-content: center;
 | 
				
			||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    border: 1px solid rgba(${props => props.theme.colors.text.secondary}, 0.75);
 | 
					    border: 1px solid ${props => mixin.rgba(props.theme.colors.text.secondary, 0.75)};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  &:hover svg {
 | 
					  &:hover svg {
 | 
				
			||||||
    fill: rgba(${props => props.theme.colors.text.secondary}, 0.75);
 | 
					    fill: ${props => mixin.rgba(props.theme.colors.text.secondary, 0.75)};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -223,17 +221,17 @@ export const AssignUsersButton = styled.div`
 | 
				
			|||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
  border: 1px solid transparent;
 | 
					  border: 1px solid transparent;
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    border: 1px solid ${mixin.darken('#414561', 0.15)};
 | 
					    border: 1px solid ${props => mixin.darken(props.theme.colors.alternate, 0.15)};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  &:hover ${AssignUserIcon} {
 | 
					  &:hover ${AssignUserIcon} {
 | 
				
			||||||
    border: 1px solid #414561;
 | 
					    border: 1px solid ${props => props.theme.colors.alternate};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const AssignUserLabel = styled.span`
 | 
					export const AssignUserLabel = styled.span`
 | 
				
			||||||
  flex: 1 1 auto;
 | 
					  flex: 1 1 auto;
 | 
				
			||||||
  line-height: 15px;
 | 
					  line-height: 15px;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary}, 0.75);
 | 
					  color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.75)};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ExtraActionsSection = styled.div`
 | 
					export const ExtraActionsSection = styled.div`
 | 
				
			||||||
@@ -245,7 +243,7 @@ export const ExtraActionsSection = styled.div`
 | 
				
			|||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ActionButtonsTitle = styled.h3`
 | 
					export const ActionButtonsTitle = styled.h3`
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary});
 | 
					  color: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
  font-size: 12px;
 | 
					  font-size: 12px;
 | 
				
			||||||
  font-weight: 500;
 | 
					  font-weight: 500;
 | 
				
			||||||
  letter-spacing: 0.04em;
 | 
					  letter-spacing: 0.04em;
 | 
				
			||||||
@@ -255,7 +253,7 @@ export const ActionButton = styled(Button)`
 | 
				
			|||||||
  margin-top: 8px;
 | 
					  margin-top: 8px;
 | 
				
			||||||
  margin-left: -10px;
 | 
					  margin-left: -10px;
 | 
				
			||||||
  padding: 8px 16px;
 | 
					  padding: 8px 16px;
 | 
				
			||||||
  background: rgba(${props => props.theme.colors.bg.primary}, 0.5);
 | 
					  background: ${props => mixin.rgba(props.theme.colors.bg.primary, 0.5)};
 | 
				
			||||||
  text-align: left;
 | 
					  text-align: left;
 | 
				
			||||||
  transition: transform 0.2s ease;
 | 
					  transition: transform 0.2s ease;
 | 
				
			||||||
  & span {
 | 
					  & span {
 | 
				
			||||||
@@ -264,7 +262,7 @@ export const ActionButton = styled(Button)`
 | 
				
			|||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    box-shadow: none;
 | 
					    box-shadow: none;
 | 
				
			||||||
    transform: translateX(4px);
 | 
					    transform: translateX(4px);
 | 
				
			||||||
    background: rgba(${props => props.theme.colors.bg.primary}, 0.75);
 | 
					    background: ${props => mixin.rgba(props.theme.colors.bg.primary, 0.75)};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -283,10 +281,10 @@ export const HeaderActionIcon = styled.div`
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
  svg {
 | 
					  svg {
 | 
				
			||||||
    fill: rgba(${props => props.theme.colors.text.primary}, 0.75);
 | 
					    fill: ${props => mixin.rgba(props.theme.colors.text.primary, 0.75)};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  &:hover svg {
 | 
					  &:hover svg {
 | 
				
			||||||
    fill: rgba(${props => props.theme.colors.primary});
 | 
					    fill: ${props => mixin.rgba(props.theme.colors.primary, 0.75)});
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -343,7 +341,7 @@ export const MetaDetail = styled.div`
 | 
				
			|||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const MetaDetailTitle = styled.h3`
 | 
					export const MetaDetailTitle = styled.h3`
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary});
 | 
					  color: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
  font-size: 12px;
 | 
					  font-size: 12px;
 | 
				
			||||||
  font-weight: 500;
 | 
					  font-weight: 500;
 | 
				
			||||||
  letter-spacing: 0.04em;
 | 
					  letter-spacing: 0.04em;
 | 
				
			||||||
@@ -362,7 +360,7 @@ export const MetaDetailContent = styled.div`
 | 
				
			|||||||
`;
 | 
					`;
 | 
				
			||||||
export const TaskDetailsAddLabel = styled.div`
 | 
					export const TaskDetailsAddLabel = styled.div`
 | 
				
			||||||
  border-radius: 3px;
 | 
					  border-radius: 3px;
 | 
				
			||||||
  background: ${mixin.darken('#262c49', 0.15)};
 | 
					  background: ${props => mixin.darken(props.theme.colors.bg.secondary, 0.15)};
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    opacity: 0.8;
 | 
					    opacity: 0.8;
 | 
				
			||||||
@@ -377,7 +375,7 @@ export const TaskDetailsAddLabelIcon = styled.div`
 | 
				
			|||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
  justify-content: center;
 | 
					  justify-content: center;
 | 
				
			||||||
  border-radius: 3px;
 | 
					  border-radius: 3px;
 | 
				
			||||||
  background: ${mixin.darken('#262c49', 0.15)};
 | 
					  background: ${props => mixin.darken(props.theme.colors.bg.secondary, 0.15)};
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    opacity: 0.8;
 | 
					    opacity: 0.8;
 | 
				
			||||||
@@ -452,11 +450,11 @@ export const TabBarSection = styled.div`
 | 
				
			|||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const TabBarItem = styled.div`
 | 
					export const TabBarItem = styled.div`
 | 
				
			||||||
  box-shadow: inset 0 -2px rgba(216, 93, 216);
 | 
					  box-shadow: inset 0 -2px ${props => props.theme.colors.primary};
 | 
				
			||||||
  padding: 12px 7px 14px 7px;
 | 
					  padding: 12px 7px 14px 7px;
 | 
				
			||||||
  margin-bottom: -1px;
 | 
					  margin-bottom: -1px;
 | 
				
			||||||
  margin-right: 36px;
 | 
					  margin-right: 36px;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary});
 | 
					  color: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const CommentContainer = styled.div`
 | 
					export const CommentContainer = styled.div`
 | 
				
			||||||
@@ -491,7 +489,7 @@ export const CommentTextArea = styled(TextareaAutosize)`
 | 
				
			|||||||
  line-height: 28px;
 | 
					  line-height: 28px;
 | 
				
			||||||
  padding: 4px 6px;
 | 
					  padding: 4px 6px;
 | 
				
			||||||
  border-radius: 6px;
 | 
					  border-radius: 6px;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary});
 | 
					  color: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
  background: #1f243e;
 | 
					  background: #1f243e;
 | 
				
			||||||
  border: none;
 | 
					  border: none;
 | 
				
			||||||
  transition: max-height 200ms, height 200ms, min-height 200ms;
 | 
					  transition: max-height 200ms, height 200ms, min-height 200ms;
 | 
				
			||||||
@@ -556,13 +554,13 @@ export const ActivityItemHeaderTitle = styled.div`
 | 
				
			|||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ActivityItemHeaderTitleName = styled.span`
 | 
					export const ActivityItemHeaderTitleName = styled.span`
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary});
 | 
					  color: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
  font-weight: 500;
 | 
					  font-weight: 500;
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ActivityItemTimestamp = styled.span<{ margin: number }>`
 | 
					export const ActivityItemTimestamp = styled.span<{ margin: number }>`
 | 
				
			||||||
  font-size: 12px;
 | 
					  font-size: 12px;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary}, 0.65);
 | 
					  color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.65)};
 | 
				
			||||||
  margin-left: ${props => props.margin}px;
 | 
					  margin-left: ${props => props.margin}px;
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -575,15 +573,15 @@ export const ActivityItemComment = styled.div`
 | 
				
			|||||||
  border-radius: 3px;
 | 
					  border-radius: 3px;
 | 
				
			||||||
  ${mixin.boxShadowCard}
 | 
					  ${mixin.boxShadowCard}
 | 
				
			||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary});
 | 
					  color: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
  padding: 8px 12px;
 | 
					  padding: 8px 12px;
 | 
				
			||||||
  margin: 4px 0;
 | 
					  margin: 4px 0;
 | 
				
			||||||
  background-color: ${mixin.darken('#262c49', 0.1)};
 | 
					  background-color: ${props => mixin.darken(props.theme.colors.alternate, 0.1)};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ActivityItemLog = styled.span`
 | 
					export const ActivityItemLog = styled.span`
 | 
				
			||||||
  margin-left: 2px;
 | 
					  margin-left: 2px;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary});
 | 
					  color: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ViewRawButton = styled.button`
 | 
					export const ViewRawButton = styled.button`
 | 
				
			||||||
@@ -594,9 +592,9 @@ export const ViewRawButton = styled.button`
 | 
				
			|||||||
  right: 4px;
 | 
					  right: 4px;
 | 
				
			||||||
  bottom: -24px;
 | 
					  bottom: -24px;
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary}, 0.25);
 | 
					  color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.25)};
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    color: rgba(${props => props.theme.colors.text.primary});
 | 
					    color: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,7 +24,7 @@ const Textarea = styled(TextareaAutosize)`
 | 
				
			|||||||
  font-size: 20px;
 | 
					  font-size: 20px;
 | 
				
			||||||
  padding: 3px 10px 3px 8px;
 | 
					  padding: 3px 10px 3px 8px;
 | 
				
			||||||
  &:focus {
 | 
					  &:focus {
 | 
				
			||||||
    box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px;
 | 
					    box-shadow: ${props => props.theme.colors.primary} 0px 0px 0px 1px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,7 +11,8 @@ export const ProjectMember = styled(TaskAssignee)<{ zIndex: number }>`
 | 
				
			|||||||
  z-index: ${props => props.zIndex};
 | 
					  z-index: ${props => props.zIndex};
 | 
				
			||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  box-shadow: 0 0 0 2px rgba(16, 22, 58), inset 0 0 0 1px rgba(16, 22, 58, 0.07);
 | 
					  box-shadow: 0 0 0 2px ${props => props.theme.colors.bg.primary},
 | 
				
			||||||
 | 
					    inset 0 0 0 1px ${props => mixin.rgba(props.theme.colors.bg.primary, 0.07)};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const NavbarWrapper = styled.div`
 | 
					export const NavbarWrapper = styled.div`
 | 
				
			||||||
@@ -28,9 +29,9 @@ export const NavbarHeader = styled.header`
 | 
				
			|||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
  justify-content: space-between;
 | 
					  justify-content: space-between;
 | 
				
			||||||
  background: rgb(16, 22, 58);
 | 
					  background: ${props => props.theme.colors.bg.primary};
 | 
				
			||||||
  box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.05);
 | 
					  box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.05);
 | 
				
			||||||
  border-bottom: 1px solid rgba(65, 69, 97, 0.65);
 | 
					  border-bottom: 1px solid ${props => mixin.rgba(props.theme.colors.alternate, 0.65)};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
export const Breadcrumbs = styled.div`
 | 
					export const Breadcrumbs = styled.div`
 | 
				
			||||||
  color: rgb(94, 108, 132);
 | 
					  color: rgb(94, 108, 132);
 | 
				
			||||||
@@ -124,7 +125,7 @@ export const ProjectTabs = styled.div`
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const ProjectTab = styled(NavLink)`
 | 
					export const ProjectTab = styled(NavLink)`
 | 
				
			||||||
  font-size: 80%;
 | 
					  font-size: 80%;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary});
 | 
					  color: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
  font-size: 15px;
 | 
					  font-size: 15px;
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
@@ -141,22 +142,22 @@ export const ProjectTab = styled(NavLink)`
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    box-shadow: inset 0 -2px rgba(${props => props.theme.colors.text.secondary});
 | 
					    box-shadow: inset 0 -2px ${props => props.theme.colors.text.secondary};
 | 
				
			||||||
    color: rgba(${props => props.theme.colors.text.secondary});
 | 
					    color: ${props => props.theme.colors.text.secondary};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &.active {
 | 
					  &.active {
 | 
				
			||||||
    box-shadow: inset 0 -2px rgba(${props => props.theme.colors.secondary});
 | 
					    box-shadow: inset 0 -2px ${props => props.theme.colors.secondary};
 | 
				
			||||||
    color: rgba(${props => props.theme.colors.secondary});
 | 
					    color: ${props => props.theme.colors.secondary};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  &.active:hover {
 | 
					  &.active:hover {
 | 
				
			||||||
    box-shadow: inset 0 -2px rgba(${props => props.theme.colors.secondary});
 | 
					    box-shadow: inset 0 -2px ${props => props.theme.colors.secondary};
 | 
				
			||||||
    color: rgba(${props => props.theme.colors.secondary});
 | 
					    color: ${props => props.theme.colors.secondary};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ProjectName = styled.h1`
 | 
					export const ProjectName = styled.h1`
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary});
 | 
					  color: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
  font-weight: 600;
 | 
					  font-weight: 600;
 | 
				
			||||||
  font-size: 20px;
 | 
					  font-size: 20px;
 | 
				
			||||||
  padding: 3px 10px 3px 8px;
 | 
					  padding: 3px 10px 3px 8px;
 | 
				
			||||||
@@ -185,7 +186,7 @@ export const ProjectNameTextarea = styled(TextareaAutosize)`
 | 
				
			|||||||
  font-size: 20px;
 | 
					  font-size: 20px;
 | 
				
			||||||
  padding: 3px 10px 3px 8px;
 | 
					  padding: 3px 10px 3px 8px;
 | 
				
			||||||
  &:focus {
 | 
					  &:focus {
 | 
				
			||||||
    box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px;
 | 
					    box-shadow: ${props => props.theme.colors.primary} 0px 0px 0px 1px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -203,7 +204,7 @@ export const ProjectSwitcher = styled.button`
 | 
				
			|||||||
  color: #c2c6dc;
 | 
					  color: #c2c6dc;
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    background: rgb(115, 103, 240);
 | 
					    background: ${props => props.theme.colors.primary};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -227,7 +228,7 @@ export const ProjectSettingsButton = styled.button`
 | 
				
			|||||||
  justify-content: center;
 | 
					  justify-content: center;
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
    background: rgb(115, 103, 240);
 | 
					    background: ${props => props.theme.colors.primary};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -243,7 +244,7 @@ export const ProjectFinder = styled(Button)`
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const NavSeparator = styled.div`
 | 
					export const NavSeparator = styled.div`
 | 
				
			||||||
  width: 1px;
 | 
					  width: 1px;
 | 
				
			||||||
  background: rgba(${props => props.theme.colors.border});
 | 
					  background: ${props => props.theme.colors.border};
 | 
				
			||||||
  height: 34px;
 | 
					  height: 34px;
 | 
				
			||||||
  margin: 0 20px;
 | 
					  margin: 0 20px;
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
@@ -260,11 +261,11 @@ export const LogoContainer = styled(Link)`
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const TaskcafeTitle = styled.h2`
 | 
					export const TaskcafeTitle = styled.h2`
 | 
				
			||||||
  margin-left: 5px;
 | 
					  margin-left: 5px;
 | 
				
			||||||
  color: rgba(${props => props.theme.colors.text.primary});
 | 
					  color: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
  font-size: 20px;
 | 
					  font-size: 20px;
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const TaskcafeLogo = styled(Taskcafe)`
 | 
					export const TaskcafeLogo = styled(Taskcafe)`
 | 
				
			||||||
  fill: rgba(${props => props.theme.colors.text.primary});
 | 
					  fill: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
  stroke: rgba(${props => props.theme.colors.text.primary});
 | 
					  stroke: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,8 +2,8 @@ import React, { useState } from 'react';
 | 
				
			|||||||
import NormalizeStyles from 'App/NormalizeStyles';
 | 
					import NormalizeStyles from 'App/NormalizeStyles';
 | 
				
			||||||
import BaseStyles from 'App/BaseStyles';
 | 
					import BaseStyles from 'App/BaseStyles';
 | 
				
			||||||
import { action } from '@storybook/addon-actions';
 | 
					import { action } from '@storybook/addon-actions';
 | 
				
			||||||
import DropdownMenu from 'shared/components/DropdownMenu';
 | 
					 | 
				
			||||||
import TopNavbar from '.';
 | 
					import TopNavbar from '.';
 | 
				
			||||||
 | 
					import theme from '../../../App/ThemeStyles';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  component: TopNavbar,
 | 
					  component: TopNavbar,
 | 
				
			||||||
@@ -15,7 +15,7 @@ export default {
 | 
				
			|||||||
    backgrounds: [
 | 
					    backgrounds: [
 | 
				
			||||||
      { name: 'white', value: '#ffffff' },
 | 
					      { name: 'white', value: '#ffffff' },
 | 
				
			||||||
      { name: 'gray', value: '#f8f8f8' },
 | 
					      { name: 'gray', value: '#f8f8f8' },
 | 
				
			||||||
      { name: 'darkBlue', value: '#262c49', default: true },
 | 
					      { name: 'darkBlue', value: theme.colors.bg.secondary, default: true },
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,7 @@ import ProfileIcon from 'shared/components/ProfileIcon';
 | 
				
			|||||||
import { usePopup } from 'shared/components/PopupMenu';
 | 
					import { usePopup } from 'shared/components/PopupMenu';
 | 
				
			||||||
import { RoleCode } from 'shared/generated/graphql';
 | 
					import { RoleCode } from 'shared/generated/graphql';
 | 
				
			||||||
import NOOP from 'shared/utils/noop';
 | 
					import NOOP from 'shared/utils/noop';
 | 
				
			||||||
 | 
					import { useHistory } from 'react-router';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  TaskcafeLogo,
 | 
					  TaskcafeLogo,
 | 
				
			||||||
  TaskcafeTitle,
 | 
					  TaskcafeTitle,
 | 
				
			||||||
@@ -30,7 +31,6 @@ import {
 | 
				
			|||||||
  ProjectMember,
 | 
					  ProjectMember,
 | 
				
			||||||
  ProjectMembers,
 | 
					  ProjectMembers,
 | 
				
			||||||
} from './Styles';
 | 
					} from './Styles';
 | 
				
			||||||
import { useHistory } from 'react-router';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
type IconContainerProps = {
 | 
					type IconContainerProps = {
 | 
				
			||||||
  disabled?: boolean;
 | 
					  disabled?: boolean;
 | 
				
			||||||
@@ -309,7 +309,7 @@ const NavBar: React.FC<NavBarProps> = ({
 | 
				
			|||||||
          <IconContainer disabled onClick={NOOP}>
 | 
					          <IconContainer disabled onClick={NOOP}>
 | 
				
			||||||
            <CheckCircle width={20} height={20} />
 | 
					            <CheckCircle width={20} height={20} />
 | 
				
			||||||
          </IconContainer>
 | 
					          </IconContainer>
 | 
				
			||||||
          <IconContainer onClick={() => history.push('/outline')}>
 | 
					          <IconContainer disabled onClick={NOOP}>
 | 
				
			||||||
            <ListUnordered width={20} height={20} />
 | 
					            <ListUnordered width={20} height={20} />
 | 
				
			||||||
          </IconContainer>
 | 
					          </IconContainer>
 | 
				
			||||||
          <IconContainer disabled onClick={onNotificationClick}>
 | 
					          <IconContainer disabled onClick={onNotificationClick}>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,8 +17,8 @@ type Props = {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Svg = styled.svg`
 | 
					const Svg = styled.svg`
 | 
				
			||||||
  fill: rgba(${props => props.theme.colors.text.primary});
 | 
					  fill: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
  stroke: rgba(${props => props.theme.colors.text.primary});
 | 
					  stroke: ${props => props.theme.colors.text.primary};
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Icon: React.FC<Props> = ({ width, height, viewBox, className, onClick, children }) => {
 | 
					const Icon: React.FC<Props> = ({ width, height, viewBox, className, onClick, children }) => {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1
									
								
								frontend/src/styled.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								frontend/src/styled.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -10,6 +10,7 @@ declare module 'styled-components' {
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
    colors: {
 | 
					    colors: {
 | 
				
			||||||
      [key: string]: any;
 | 
					      [key: string]: any;
 | 
				
			||||||
 | 
					      multiColors: string[];
 | 
				
			||||||
      primary: string;
 | 
					      primary: string;
 | 
				
			||||||
      secondary: string;
 | 
					      secondary: string;
 | 
				
			||||||
      success: string;
 | 
					      success: string;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										9
									
								
								frontend/src/taskcafe.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								frontend/src/taskcafe.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -61,7 +61,7 @@ type User = TaskUser & {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
type RefreshTokenResponse = {
 | 
					type RefreshTokenResponse = {
 | 
				
			||||||
  accessToken: string;
 | 
					  accessToken: string;
 | 
				
			||||||
  isInstalled: boolean;
 | 
					  setup?: null | { confirmToken: string };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type LoginFormData = {
 | 
					type LoginFormData = {
 | 
				
			||||||
@@ -91,7 +91,14 @@ type ErrorOption =
 | 
				
			|||||||
      type: string;
 | 
					      type: string;
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type SetFailedFn = () => void;
 | 
				
			||||||
 | 
					type ConfirmProps = {
 | 
				
			||||||
 | 
					  hasConfirmToken: boolean;
 | 
				
			||||||
 | 
					  onConfirmUser: (setFailed: SetFailedFn) => void;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type RegisterProps = {
 | 
					type RegisterProps = {
 | 
				
			||||||
 | 
					  registered?: boolean;
 | 
				
			||||||
  onSubmit: (
 | 
					  onSubmit: (
 | 
				
			||||||
    data: RegisterFormData,
 | 
					    data: RegisterFormData,
 | 
				
			||||||
    setComplete: (val: boolean) => void,
 | 
					    setComplete: (val: boolean) => void,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3112,6 +3112,13 @@
 | 
				
			|||||||
  resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8"
 | 
					  resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8"
 | 
				
			||||||
  integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==
 | 
					  integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@types/query-string@^6.3.0":
 | 
				
			||||||
 | 
					  version "6.3.0"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@types/query-string/-/query-string-6.3.0.tgz#b6fa172a01405abcaedac681118e78429d62ea39"
 | 
				
			||||||
 | 
					  integrity sha512-yuIv/WRffRzL7cBW+sla4HwBZrEXRNf1MKQ5SklPEadth+BKbDxiVG8A3iISN5B3yC4EeSCzMZP8llHTcUhOzQ==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    query-string "*"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"@types/reach__router@^1.2.3":
 | 
					"@types/reach__router@^1.2.3":
 | 
				
			||||||
  version "1.3.0"
 | 
					  version "1.3.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/@types/reach__router/-/reach__router-1.3.0.tgz#4c05a947ccecca05c72bb335a0f7bb43fec12446"
 | 
					  resolved "https://registry.yarnpkg.com/@types/reach__router/-/reach__router-1.3.0.tgz#4c05a947ccecca05c72bb335a0f7bb43fec12446"
 | 
				
			||||||
@@ -13356,6 +13363,15 @@ qs@~6.5.2:
 | 
				
			|||||||
  resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
 | 
					  resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
 | 
				
			||||||
  integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
 | 
					  integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					query-string@*, query-string@^6.13.7:
 | 
				
			||||||
 | 
					  version "6.13.7"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.13.7.tgz#af53802ff6ed56f3345f92d40a056f93681026ee"
 | 
				
			||||||
 | 
					  integrity sha512-CsGs8ZYb39zu0WLkeOhe0NMePqgYdAuCqxOYKDR5LVCytDZYMGx3Bb+xypvQvPHVPijRXB0HZNFllCzHRe4gEA==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    decode-uri-component "^0.2.0"
 | 
				
			||||||
 | 
					    split-on-first "^1.0.0"
 | 
				
			||||||
 | 
					    strict-uri-encode "^2.0.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
query-string@^4.1.0:
 | 
					query-string@^4.1.0:
 | 
				
			||||||
  version "4.3.4"
 | 
					  version "4.3.4"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb"
 | 
					  resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb"
 | 
				
			||||||
@@ -15254,6 +15270,11 @@ speedometer@~1.0.0:
 | 
				
			|||||||
  resolved "https://registry.yarnpkg.com/speedometer/-/speedometer-1.0.0.tgz#cd671cb06752c22bca3370e2f334440be4fc62e2"
 | 
					  resolved "https://registry.yarnpkg.com/speedometer/-/speedometer-1.0.0.tgz#cd671cb06752c22bca3370e2f334440be4fc62e2"
 | 
				
			||||||
  integrity sha1-zWccsGdSwivKM3Di8zREC+T8YuI=
 | 
					  integrity sha1-zWccsGdSwivKM3Di8zREC+T8YuI=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					split-on-first@^1.0.0:
 | 
				
			||||||
 | 
					  version "1.1.0"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f"
 | 
				
			||||||
 | 
					  integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
split-string@^3.0.1, split-string@^3.0.2:
 | 
					split-string@^3.0.1, split-string@^3.0.2:
 | 
				
			||||||
  version "3.1.0"
 | 
					  version "3.1.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
 | 
					  resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
 | 
				
			||||||
@@ -15407,6 +15428,11 @@ strict-uri-encode@^1.0.0:
 | 
				
			|||||||
  resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
 | 
					  resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
 | 
				
			||||||
  integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=
 | 
					  integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					strict-uri-encode@^2.0.0:
 | 
				
			||||||
 | 
					  version "2.0.0"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546"
 | 
				
			||||||
 | 
					  integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
string-length@^2.0.0:
 | 
					string-length@^2.0.0:
 | 
				
			||||||
  version "2.0.0"
 | 
					  version "2.0.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"
 | 
					  resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							@@ -13,6 +13,7 @@ require (
 | 
				
			|||||||
	github.com/lib/pq v1.3.0
 | 
						github.com/lib/pq v1.3.0
 | 
				
			||||||
	github.com/lithammer/fuzzysearch v1.1.0
 | 
						github.com/lithammer/fuzzysearch v1.1.0
 | 
				
			||||||
	github.com/magefile/mage v1.9.0
 | 
						github.com/magefile/mage v1.9.0
 | 
				
			||||||
 | 
						github.com/matcornic/hermes/v2 v2.1.0
 | 
				
			||||||
	github.com/pelletier/go-toml v1.8.0 // indirect
 | 
						github.com/pelletier/go-toml v1.8.0 // indirect
 | 
				
			||||||
	github.com/pkg/errors v0.9.1
 | 
						github.com/pkg/errors v0.9.1
 | 
				
			||||||
	github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0
 | 
						github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0
 | 
				
			||||||
@@ -22,4 +23,5 @@ require (
 | 
				
			|||||||
	github.com/spf13/viper v1.4.0
 | 
						github.com/spf13/viper v1.4.0
 | 
				
			||||||
	github.com/vektah/gqlparser/v2 v2.0.1
 | 
						github.com/vektah/gqlparser/v2 v2.0.1
 | 
				
			||||||
	golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899
 | 
						golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899
 | 
				
			||||||
 | 
						gopkg.in/mail.v2 v2.3.1
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										38
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										38
									
								
								go.sum
									
									
									
									
									
								
							@@ -50,11 +50,17 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
 | 
				
			|||||||
github.com/ClickHouse/clickhouse-go v1.3.12/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI=
 | 
					github.com/ClickHouse/clickhouse-go v1.3.12/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI=
 | 
				
			||||||
github.com/DataDog/sketches-go v0.0.0-20190923095040-43f19ad77ff7 h1:qELHH0AWCvf98Yf+CNIJx9vOZOfHFDDzgDRYsnNk/vs=
 | 
					github.com/DataDog/sketches-go v0.0.0-20190923095040-43f19ad77ff7 h1:qELHH0AWCvf98Yf+CNIJx9vOZOfHFDDzgDRYsnNk/vs=
 | 
				
			||||||
github.com/DataDog/sketches-go v0.0.0-20190923095040-43f19ad77ff7/go.mod h1:Q5DbzQ+3AkgGwymQO7aZFNP7ns2lZKGtvRBzRXfdi60=
 | 
					github.com/DataDog/sketches-go v0.0.0-20190923095040-43f19ad77ff7/go.mod h1:Q5DbzQ+3AkgGwymQO7aZFNP7ns2lZKGtvRBzRXfdi60=
 | 
				
			||||||
 | 
					github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc=
 | 
				
			||||||
 | 
					github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
 | 
				
			||||||
 | 
					github.com/Masterminds/sprig v2.16.0+incompatible h1:QZbMUPxRQ50EKAq3LFMnxddMu88/EUUG3qmxwtDmPsY=
 | 
				
			||||||
 | 
					github.com/Masterminds/sprig v2.16.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
 | 
				
			||||||
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
 | 
					github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
 | 
				
			||||||
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
 | 
					github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
 | 
				
			||||||
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
 | 
					github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
 | 
				
			||||||
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
 | 
					github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
 | 
				
			||||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 | 
					github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 | 
				
			||||||
 | 
					github.com/PuerkitoBio/goquery v1.5.0 h1:uGvmFXOA73IKluu/F84Xd1tt/z07GYm8X49XKHP7EJk=
 | 
				
			||||||
 | 
					github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg=
 | 
				
			||||||
github.com/RichardKnop/logging v0.0.0-20190827224416-1a693bdd4fae h1:DcFpTQBYQ9Ct2d6sC7ol0/ynxc2pO1cpGUM+f4t5adg=
 | 
					github.com/RichardKnop/logging v0.0.0-20190827224416-1a693bdd4fae h1:DcFpTQBYQ9Ct2d6sC7ol0/ynxc2pO1cpGUM+f4t5adg=
 | 
				
			||||||
github.com/RichardKnop/logging v0.0.0-20190827224416-1a693bdd4fae/go.mod h1:rJJ84PyA/Wlmw1hO+xTzV2wsSUon6J5ktg0g8BF2PuU=
 | 
					github.com/RichardKnop/logging v0.0.0-20190827224416-1a693bdd4fae/go.mod h1:rJJ84PyA/Wlmw1hO+xTzV2wsSUon6J5ktg0g8BF2PuU=
 | 
				
			||||||
github.com/RichardKnop/machinery v1.9.1 h1:Q4WInk0OWGMbXDH3Q8dm8uadN5Wcyquc+7IcM4p9ECs=
 | 
					github.com/RichardKnop/machinery v1.9.1 h1:Q4WInk0OWGMbXDH3Q8dm8uadN5Wcyquc+7IcM4p9ECs=
 | 
				
			||||||
@@ -70,6 +76,10 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
 | 
				
			|||||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 | 
					github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 | 
				
			||||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
 | 
					github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
 | 
				
			||||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
 | 
					github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
 | 
				
			||||||
 | 
					github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o=
 | 
				
			||||||
 | 
					github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
 | 
				
			||||||
 | 
					github.com/aokoli/goutils v1.0.1 h1:7fpzNGoJ3VA8qcrm++XEE1QUe0mIwNeLa02Nwq7RDkg=
 | 
				
			||||||
 | 
					github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ=
 | 
				
			||||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
 | 
					github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
 | 
				
			||||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
 | 
					github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
 | 
				
			||||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
 | 
					github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
 | 
				
			||||||
@@ -151,6 +161,7 @@ github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxm
 | 
				
			|||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 | 
					github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 | 
				
			||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 | 
					github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 | 
				
			||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 | 
					github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 | 
				
			||||||
 | 
					github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df/go.mod h1:GJr+FCSXshIwgHBtLglIg9M2l2kQSi6QjVAngtzI08Y=
 | 
				
			||||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 | 
					github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 | 
				
			||||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
 | 
					github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
 | 
				
			||||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 | 
					github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 | 
				
			||||||
@@ -258,6 +269,7 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf
 | 
				
			|||||||
github.com/google/pprof v0.0.0-20200507031123-427632fa3b1c/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 | 
					github.com/google/pprof v0.0.0-20200507031123-427632fa3b1c/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 | 
				
			||||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 | 
					github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 | 
				
			||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 | 
					github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 | 
				
			||||||
 | 
					github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 | 
				
			||||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
 | 
					github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
 | 
				
			||||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 | 
					github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 | 
				
			||||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 | 
					github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 | 
				
			||||||
@@ -265,6 +277,8 @@ github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+
 | 
				
			|||||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
 | 
					github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
 | 
				
			||||||
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
 | 
					github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
 | 
				
			||||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
 | 
					github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
 | 
				
			||||||
 | 
					github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
 | 
				
			||||||
 | 
					github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
 | 
				
			||||||
github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
 | 
					github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
 | 
				
			||||||
github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
 | 
					github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
 | 
				
			||||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
 | 
					github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
 | 
				
			||||||
@@ -291,7 +305,11 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
 | 
				
			|||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 | 
					github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 | 
				
			||||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
 | 
					github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
 | 
				
			||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 | 
					github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 | 
				
			||||||
 | 
					github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0=
 | 
				
			||||||
 | 
					github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
 | 
				
			||||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 | 
					github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 | 
				
			||||||
 | 
					github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
 | 
				
			||||||
 | 
					github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
 | 
				
			||||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 | 
					github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 | 
				
			||||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 | 
					github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 | 
				
			||||||
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
 | 
					github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
 | 
				
			||||||
@@ -318,6 +336,8 @@ github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9
 | 
				
			|||||||
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
 | 
					github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
 | 
				
			||||||
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 | 
					github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 | 
				
			||||||
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 | 
					github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 | 
				
			||||||
 | 
					github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0 h1:xqgexXAGQgY3HAjNPSaCqn5Aahbo5TKsmhp8VRfr1iQ=
 | 
				
			||||||
 | 
					github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
 | 
				
			||||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
 | 
					github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
 | 
				
			||||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
 | 
					github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
 | 
				
			||||||
github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc=
 | 
					github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc=
 | 
				
			||||||
@@ -368,6 +388,9 @@ github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czP
 | 
				
			|||||||
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
 | 
					github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
 | 
				
			||||||
github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI=
 | 
					github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI=
 | 
				
			||||||
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
 | 
					github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
 | 
				
			||||||
 | 
					github.com/matcornic/hermes v1.2.0 h1:AuqZpYcTOtTB7cahdevLfnhIpfzmpqw5Czv8vpdnFDU=
 | 
				
			||||||
 | 
					github.com/matcornic/hermes/v2 v2.1.0 h1:9TDYFBPFv6mcXanaDmRDEp/RTWj0dTTi+LpFnnnfNWc=
 | 
				
			||||||
 | 
					github.com/matcornic/hermes/v2 v2.1.0/go.mod h1:2+ziJeoyRfaLiATIL8VZ7f9hpzH4oDHqTmn0bhrsgVI=
 | 
				
			||||||
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007 h1:reVOUXwnhsYv/8UqjvhrMOu5CNT9UapHFLbQ2JcXsmg=
 | 
					github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007 h1:reVOUXwnhsYv/8UqjvhrMOu5CNT9UapHFLbQ2JcXsmg=
 | 
				
			||||||
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
 | 
					github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
 | 
				
			||||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
 | 
					github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
 | 
				
			||||||
@@ -376,6 +399,8 @@ github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
 | 
				
			|||||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 | 
					github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 | 
				
			||||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 | 
					github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 | 
				
			||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
 | 
					github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
 | 
				
			||||||
 | 
					github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
 | 
				
			||||||
 | 
					github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
 | 
				
			||||||
github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4=
 | 
					github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4=
 | 
				
			||||||
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
 | 
					github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
 | 
				
			||||||
github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
 | 
					github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
 | 
				
			||||||
@@ -394,6 +419,8 @@ github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8/go.mod h1:86w
 | 
				
			|||||||
github.com/neo4j-drivers/gobolt v1.7.4/go.mod h1:O9AUbip4Dgre+CD3p40dnMD4a4r52QBIfblg5k7CTbE=
 | 
					github.com/neo4j-drivers/gobolt v1.7.4/go.mod h1:O9AUbip4Dgre+CD3p40dnMD4a4r52QBIfblg5k7CTbE=
 | 
				
			||||||
github.com/neo4j/neo4j-go-driver v1.7.4/go.mod h1:aPO0vVr+WnhEJne+FgFjfsjzAnssPFLucHgGZ76Zb/U=
 | 
					github.com/neo4j/neo4j-go-driver v1.7.4/go.mod h1:aPO0vVr+WnhEJne+FgFjfsjzAnssPFLucHgGZ76Zb/U=
 | 
				
			||||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
 | 
					github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
 | 
				
			||||||
 | 
					github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88=
 | 
				
			||||||
 | 
					github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
 | 
				
			||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 | 
					github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 | 
				
			||||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 | 
					github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 | 
				
			||||||
github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo=
 | 
					github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo=
 | 
				
			||||||
@@ -482,6 +509,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 | 
				
			|||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 | 
					github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 | 
				
			||||||
github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
 | 
					github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
 | 
				
			||||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
 | 
					github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
 | 
				
			||||||
 | 
					github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
 | 
				
			||||||
 | 
					github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
 | 
				
			||||||
github.com/streadway/amqp v1.0.0 h1:kuuDrUJFZL1QYL9hUNuCxNObNzB0bV/ZG5jV3RWAQgo=
 | 
					github.com/streadway/amqp v1.0.0 h1:kuuDrUJFZL1QYL9hUNuCxNObNzB0bV/ZG5jV3RWAQgo=
 | 
				
			||||||
github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
 | 
					github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
 | 
				
			||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 | 
					github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 | 
				
			||||||
@@ -505,6 +534,10 @@ github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGr
 | 
				
			|||||||
github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
 | 
					github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
 | 
				
			||||||
github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
 | 
					github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
 | 
				
			||||||
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
 | 
					github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
 | 
				
			||||||
 | 
					github.com/vanng822/css v0.0.0-20190504095207-a21e860bcd04 h1:L0rPdfzq43+NV8rfIx2kA4iSSLRj2jN5ijYHoeXRwvQ=
 | 
				
			||||||
 | 
					github.com/vanng822/css v0.0.0-20190504095207-a21e860bcd04/go.mod h1:tcnB1voG49QhCrwq1W0w5hhGasvOg+VQp9i9H1rCM1w=
 | 
				
			||||||
 | 
					github.com/vanng822/go-premailer v0.0.0-20191214114701-be27abe028fe h1:9YnI5plmy+ad6BM+JCLJb2ZV7/TNiE5l7SNKfumYKgc=
 | 
				
			||||||
 | 
					github.com/vanng822/go-premailer v0.0.0-20191214114701-be27abe028fe/go.mod h1:JTFJA/t820uFDoyPpErFQ3rb3amdZoPtxcKervG0OE4=
 | 
				
			||||||
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e h1:+w0Zm/9gaWpEAyDlU1eKOuk5twTjAjuevXqcJJw8hrg=
 | 
					github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e h1:+w0Zm/9gaWpEAyDlU1eKOuk5twTjAjuevXqcJJw8hrg=
 | 
				
			||||||
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
 | 
					github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
 | 
				
			||||||
github.com/vektah/gqlparser/v2 v2.0.1 h1:xgl5abVnsd4hkN9rk65OJID9bfcLSMuTaTcZj777q1o=
 | 
					github.com/vektah/gqlparser/v2 v2.0.1 h1:xgl5abVnsd4hkN9rk65OJID9bfcLSMuTaTcZj777q1o=
 | 
				
			||||||
@@ -543,6 +576,7 @@ go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/
 | 
				
			|||||||
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
 | 
					go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
 | 
				
			||||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
 | 
					go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
 | 
				
			||||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 | 
					golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 | 
				
			||||||
 | 
					golang.org/x/crypto v0.0.0-20181029175232-7e6ffbd03851/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 | 
				
			||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 | 
					golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 | 
				
			||||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 | 
					golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 | 
				
			||||||
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
 | 
					golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
 | 
				
			||||||
@@ -594,6 +628,7 @@ golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
 | 
				
			|||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 | 
					golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 | 
				
			||||||
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
 | 
					golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
 | 
				
			||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 | 
					golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 | 
				
			||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 | 
					golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 | 
				
			||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 | 
					golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 | 
				
			||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 | 
					golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 | 
				
			||||||
@@ -657,6 +692,7 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h
 | 
				
			|||||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
					golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
				
			||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
					golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
				
			||||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
					golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20190225065934-cc5685c2db12/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
				
			||||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
					golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
					golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
					golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
@@ -865,6 +901,8 @@ gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
 | 
				
			|||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 | 
					gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 | 
				
			||||||
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
 | 
					gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
 | 
				
			||||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 | 
					gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 | 
				
			||||||
 | 
					gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
 | 
				
			||||||
 | 
					gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
 | 
				
			||||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
 | 
					gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
 | 
				
			||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
 | 
					gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
 | 
				
			||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
 | 
					gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -34,11 +34,12 @@ func newMigrateCmd() *cobra.Command {
 | 
				
			|||||||
		Short: "Run the database schema migrations",
 | 
							Short: "Run the database schema migrations",
 | 
				
			||||||
		Long:  "Run the database schema migrations",
 | 
							Long:  "Run the database schema migrations",
 | 
				
			||||||
		RunE: func(cmd *cobra.Command, args []string) error {
 | 
							RunE: func(cmd *cobra.Command, args []string) error {
 | 
				
			||||||
			connection := fmt.Sprintf("user=%s password=%s host=%s dbname=%s sslmode=disable",
 | 
								connection := fmt.Sprintf("user=%s password=%s host=%s dbname=%s port=%s sslmode=disable",
 | 
				
			||||||
				viper.GetString("database.user"),
 | 
									viper.GetString("database.user"),
 | 
				
			||||||
				viper.GetString("database.password"),
 | 
									viper.GetString("database.password"),
 | 
				
			||||||
				viper.GetString("database.host"),
 | 
									viper.GetString("database.host"),
 | 
				
			||||||
				viper.GetString("database.name"),
 | 
									viper.GetString("database.name"),
 | 
				
			||||||
 | 
									viper.GetString("database.port"),
 | 
				
			||||||
			)
 | 
								)
 | 
				
			||||||
			db, err := sqlx.Connect("postgres", connection)
 | 
								db, err := sqlx.Connect("postgres", connection)
 | 
				
			||||||
			if err != nil {
 | 
								if err != nil {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -170,6 +170,12 @@ type UserAccount struct {
 | 
				
			|||||||
	ProfileAvatarUrl sql.NullString `json:"profile_avatar_url"`
 | 
						ProfileAvatarUrl sql.NullString `json:"profile_avatar_url"`
 | 
				
			||||||
	RoleCode         string         `json:"role_code"`
 | 
						RoleCode         string         `json:"role_code"`
 | 
				
			||||||
	Bio              string         `json:"bio"`
 | 
						Bio              string         `json:"bio"`
 | 
				
			||||||
 | 
						Active           bool           `json:"active"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type UserAccountConfirmToken struct {
 | 
				
			||||||
 | 
						ConfirmTokenID uuid.UUID `json:"confirm_token_id"`
 | 
				
			||||||
 | 
						Email          string    `json:"email"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type UserAccountInvited struct {
 | 
					type UserAccountInvited struct {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,7 @@ import (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Querier interface {
 | 
					type Querier interface {
 | 
				
			||||||
 | 
						CreateConfirmToken(ctx context.Context, email string) (UserAccountConfirmToken, error)
 | 
				
			||||||
	CreateInvitedProjectMember(ctx context.Context, arg CreateInvitedProjectMemberParams) (ProjectMemberInvited, error)
 | 
						CreateInvitedProjectMember(ctx context.Context, arg CreateInvitedProjectMemberParams) (ProjectMemberInvited, error)
 | 
				
			||||||
	CreateInvitedUser(ctx context.Context, email string) (UserAccountInvited, error)
 | 
						CreateInvitedUser(ctx context.Context, email string) (UserAccountInvited, error)
 | 
				
			||||||
	CreateLabelColor(ctx context.Context, arg CreateLabelColorParams) (LabelColor, error)
 | 
						CreateLabelColor(ctx context.Context, arg CreateLabelColorParams) (LabelColor, error)
 | 
				
			||||||
@@ -32,12 +33,14 @@ type Querier interface {
 | 
				
			|||||||
	CreateTeamMember(ctx context.Context, arg CreateTeamMemberParams) (TeamMember, error)
 | 
						CreateTeamMember(ctx context.Context, arg CreateTeamMemberParams) (TeamMember, error)
 | 
				
			||||||
	CreateTeamProject(ctx context.Context, arg CreateTeamProjectParams) (Project, error)
 | 
						CreateTeamProject(ctx context.Context, arg CreateTeamProjectParams) (Project, error)
 | 
				
			||||||
	CreateUserAccount(ctx context.Context, arg CreateUserAccountParams) (UserAccount, error)
 | 
						CreateUserAccount(ctx context.Context, arg CreateUserAccountParams) (UserAccount, error)
 | 
				
			||||||
 | 
						DeleteConfirmTokenForEmail(ctx context.Context, email string) error
 | 
				
			||||||
	DeleteExpiredTokens(ctx context.Context) error
 | 
						DeleteExpiredTokens(ctx context.Context) error
 | 
				
			||||||
	DeleteInvitedProjectMemberByID(ctx context.Context, projectMemberInvitedID uuid.UUID) error
 | 
						DeleteInvitedProjectMemberByID(ctx context.Context, projectMemberInvitedID uuid.UUID) error
 | 
				
			||||||
	DeleteInvitedUserAccount(ctx context.Context, userAccountInvitedID uuid.UUID) (UserAccountInvited, error)
 | 
						DeleteInvitedUserAccount(ctx context.Context, userAccountInvitedID uuid.UUID) (UserAccountInvited, error)
 | 
				
			||||||
	DeleteProjectByID(ctx context.Context, projectID uuid.UUID) error
 | 
						DeleteProjectByID(ctx context.Context, projectID uuid.UUID) error
 | 
				
			||||||
	DeleteProjectLabelByID(ctx context.Context, projectLabelID uuid.UUID) error
 | 
						DeleteProjectLabelByID(ctx context.Context, projectLabelID uuid.UUID) error
 | 
				
			||||||
	DeleteProjectMember(ctx context.Context, arg DeleteProjectMemberParams) error
 | 
						DeleteProjectMember(ctx context.Context, arg DeleteProjectMemberParams) error
 | 
				
			||||||
 | 
						DeleteProjectMemberInvitedForEmail(ctx context.Context, email string) error
 | 
				
			||||||
	DeleteRefreshTokenByID(ctx context.Context, tokenID uuid.UUID) error
 | 
						DeleteRefreshTokenByID(ctx context.Context, tokenID uuid.UUID) error
 | 
				
			||||||
	DeleteRefreshTokenByUserID(ctx context.Context, userID uuid.UUID) error
 | 
						DeleteRefreshTokenByUserID(ctx context.Context, userID uuid.UUID) error
 | 
				
			||||||
	DeleteTaskAssignedByID(ctx context.Context, arg DeleteTaskAssignedByIDParams) (TaskAssigned, error)
 | 
						DeleteTaskAssignedByID(ctx context.Context, arg DeleteTaskAssignedByIDParams) (TaskAssigned, error)
 | 
				
			||||||
@@ -51,6 +54,7 @@ type Querier interface {
 | 
				
			|||||||
	DeleteTeamByID(ctx context.Context, teamID uuid.UUID) error
 | 
						DeleteTeamByID(ctx context.Context, teamID uuid.UUID) error
 | 
				
			||||||
	DeleteTeamMember(ctx context.Context, arg DeleteTeamMemberParams) error
 | 
						DeleteTeamMember(ctx context.Context, arg DeleteTeamMemberParams) error
 | 
				
			||||||
	DeleteUserAccountByID(ctx context.Context, userID uuid.UUID) error
 | 
						DeleteUserAccountByID(ctx context.Context, userID uuid.UUID) error
 | 
				
			||||||
 | 
						DeleteUserAccountInvitedForEmail(ctx context.Context, email string) error
 | 
				
			||||||
	GetAllNotificationsForUserID(ctx context.Context, notifierID uuid.UUID) ([]Notification, error)
 | 
						GetAllNotificationsForUserID(ctx context.Context, notifierID uuid.UUID) ([]Notification, error)
 | 
				
			||||||
	GetAllOrganizations(ctx context.Context) ([]Organization, error)
 | 
						GetAllOrganizations(ctx context.Context) ([]Organization, error)
 | 
				
			||||||
	GetAllProjectsForTeam(ctx context.Context, teamID uuid.UUID) ([]Project, error)
 | 
						GetAllProjectsForTeam(ctx context.Context, teamID uuid.UUID) ([]Project, error)
 | 
				
			||||||
@@ -61,6 +65,8 @@ type Querier interface {
 | 
				
			|||||||
	GetAllUserAccounts(ctx context.Context) ([]UserAccount, error)
 | 
						GetAllUserAccounts(ctx context.Context) ([]UserAccount, error)
 | 
				
			||||||
	GetAllVisibleProjectsForUserID(ctx context.Context, userID uuid.UUID) ([]Project, error)
 | 
						GetAllVisibleProjectsForUserID(ctx context.Context, userID uuid.UUID) ([]Project, error)
 | 
				
			||||||
	GetAssignedMembersForTask(ctx context.Context, taskID uuid.UUID) ([]TaskAssigned, error)
 | 
						GetAssignedMembersForTask(ctx context.Context, taskID uuid.UUID) ([]TaskAssigned, error)
 | 
				
			||||||
 | 
						GetConfirmTokenByEmail(ctx context.Context, email string) (UserAccountConfirmToken, error)
 | 
				
			||||||
 | 
						GetConfirmTokenByID(ctx context.Context, confirmTokenID uuid.UUID) (UserAccountConfirmToken, error)
 | 
				
			||||||
	GetEntityForNotificationID(ctx context.Context, notificationID uuid.UUID) (GetEntityForNotificationIDRow, error)
 | 
						GetEntityForNotificationID(ctx context.Context, notificationID uuid.UUID) (GetEntityForNotificationIDRow, error)
 | 
				
			||||||
	GetEntityIDForNotificationID(ctx context.Context, notificationID uuid.UUID) (uuid.UUID, error)
 | 
						GetEntityIDForNotificationID(ctx context.Context, notificationID uuid.UUID) (uuid.UUID, error)
 | 
				
			||||||
	GetInvitedMembersForProjectID(ctx context.Context, projectID uuid.UUID) ([]GetInvitedMembersForProjectIDRow, error)
 | 
						GetInvitedMembersForProjectID(ctx context.Context, projectID uuid.UUID) ([]GetInvitedMembersForProjectIDRow, error)
 | 
				
			||||||
@@ -83,6 +89,7 @@ type Querier interface {
 | 
				
			|||||||
	GetProjectMemberInvitedIDByEmail(ctx context.Context, email string) (GetProjectMemberInvitedIDByEmailRow, error)
 | 
						GetProjectMemberInvitedIDByEmail(ctx context.Context, email string) (GetProjectMemberInvitedIDByEmailRow, error)
 | 
				
			||||||
	GetProjectMembersForProjectID(ctx context.Context, projectID uuid.UUID) ([]ProjectMember, error)
 | 
						GetProjectMembersForProjectID(ctx context.Context, projectID uuid.UUID) ([]ProjectMember, error)
 | 
				
			||||||
	GetProjectRolesForUserID(ctx context.Context, userID uuid.UUID) ([]GetProjectRolesForUserIDRow, error)
 | 
						GetProjectRolesForUserID(ctx context.Context, userID uuid.UUID) ([]GetProjectRolesForUserIDRow, error)
 | 
				
			||||||
 | 
						GetProjectsForInvitedMember(ctx context.Context, email string) ([]uuid.UUID, error)
 | 
				
			||||||
	GetRefreshTokenByID(ctx context.Context, tokenID uuid.UUID) (RefreshToken, error)
 | 
						GetRefreshTokenByID(ctx context.Context, tokenID uuid.UUID) (RefreshToken, error)
 | 
				
			||||||
	GetRoleForProjectMemberByUserID(ctx context.Context, arg GetRoleForProjectMemberByUserIDParams) (Role, error)
 | 
						GetRoleForProjectMemberByUserID(ctx context.Context, arg GetRoleForProjectMemberByUserIDParams) (Role, error)
 | 
				
			||||||
	GetRoleForTeamMember(ctx context.Context, arg GetRoleForTeamMemberParams) (Role, error)
 | 
						GetRoleForTeamMember(ctx context.Context, arg GetRoleForTeamMemberParams) (Role, error)
 | 
				
			||||||
@@ -106,12 +113,17 @@ type Querier interface {
 | 
				
			|||||||
	GetTeamRolesForUserID(ctx context.Context, userID uuid.UUID) ([]GetTeamRolesForUserIDRow, error)
 | 
						GetTeamRolesForUserID(ctx context.Context, userID uuid.UUID) ([]GetTeamRolesForUserIDRow, error)
 | 
				
			||||||
	GetTeamsForOrganization(ctx context.Context, organizationID uuid.UUID) ([]Team, error)
 | 
						GetTeamsForOrganization(ctx context.Context, organizationID uuid.UUID) ([]Team, error)
 | 
				
			||||||
	GetTeamsForUserIDWhereAdmin(ctx context.Context, userID uuid.UUID) ([]Team, error)
 | 
						GetTeamsForUserIDWhereAdmin(ctx context.Context, userID uuid.UUID) ([]Team, error)
 | 
				
			||||||
 | 
						GetUserAccountByEmail(ctx context.Context, email string) (UserAccount, error)
 | 
				
			||||||
	GetUserAccountByID(ctx context.Context, userID uuid.UUID) (UserAccount, error)
 | 
						GetUserAccountByID(ctx context.Context, userID uuid.UUID) (UserAccount, error)
 | 
				
			||||||
	GetUserAccountByUsername(ctx context.Context, username string) (UserAccount, error)
 | 
						GetUserAccountByUsername(ctx context.Context, username string) (UserAccount, error)
 | 
				
			||||||
	GetUserRolesForProject(ctx context.Context, arg GetUserRolesForProjectParams) (GetUserRolesForProjectRow, error)
 | 
						GetUserRolesForProject(ctx context.Context, arg GetUserRolesForProjectParams) (GetUserRolesForProjectRow, error)
 | 
				
			||||||
 | 
						HasActiveUser(ctx context.Context) (bool, error)
 | 
				
			||||||
 | 
						HasAnyUser(ctx context.Context) (bool, error)
 | 
				
			||||||
 | 
						SetFirstUserActive(ctx context.Context) (UserAccount, error)
 | 
				
			||||||
	SetTaskChecklistItemComplete(ctx context.Context, arg SetTaskChecklistItemCompleteParams) (TaskChecklistItem, error)
 | 
						SetTaskChecklistItemComplete(ctx context.Context, arg SetTaskChecklistItemCompleteParams) (TaskChecklistItem, error)
 | 
				
			||||||
	SetTaskComplete(ctx context.Context, arg SetTaskCompleteParams) (Task, error)
 | 
						SetTaskComplete(ctx context.Context, arg SetTaskCompleteParams) (Task, error)
 | 
				
			||||||
	SetTaskGroupName(ctx context.Context, arg SetTaskGroupNameParams) (TaskGroup, error)
 | 
						SetTaskGroupName(ctx context.Context, arg SetTaskGroupNameParams) (TaskGroup, error)
 | 
				
			||||||
 | 
						SetUserActiveByEmail(ctx context.Context, email string) (UserAccount, error)
 | 
				
			||||||
	SetUserPassword(ctx context.Context, arg SetUserPasswordParams) (UserAccount, error)
 | 
						SetUserPassword(ctx context.Context, arg SetUserPasswordParams) (UserAccount, error)
 | 
				
			||||||
	UpdateProjectLabel(ctx context.Context, arg UpdateProjectLabelParams) (ProjectLabel, error)
 | 
						UpdateProjectLabel(ctx context.Context, arg UpdateProjectLabelParams) (ProjectLabel, error)
 | 
				
			||||||
	UpdateProjectLabelColor(ctx context.Context, arg UpdateProjectLabelColorParams) (ProjectLabel, error)
 | 
						UpdateProjectLabelColor(ctx context.Context, arg UpdateProjectLabelColorParams) (ProjectLabel, error)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,9 +7,12 @@ SELECT * FROM user_account WHERE username != 'system';
 | 
				
			|||||||
-- name: GetUserAccountByUsername :one
 | 
					-- name: GetUserAccountByUsername :one
 | 
				
			||||||
SELECT * FROM user_account WHERE username = $1;
 | 
					SELECT * FROM user_account WHERE username = $1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- name: GetUserAccountByEmail :one
 | 
				
			||||||
 | 
					SELECT * FROM user_account WHERE email = $1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-- name: CreateUserAccount :one
 | 
					-- name: CreateUserAccount :one
 | 
				
			||||||
INSERT INTO user_account(full_name, initials, email, username, created_at, password_hash, role_code)
 | 
					INSERT INTO user_account(full_name, initials, email, username, created_at, password_hash, role_code, active)
 | 
				
			||||||
  VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *;
 | 
					  VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-- name: UpdateUserAccountProfileAvatarURL :one
 | 
					-- name: UpdateUserAccountProfileAvatarURL :one
 | 
				
			||||||
UPDATE user_account SET profile_avatar_url = $2 WHERE user_id = $1
 | 
					UPDATE user_account SET profile_avatar_url = $2 WHERE user_id = $1
 | 
				
			||||||
@@ -53,3 +56,48 @@ SELECT * FROM user_account_invited;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
-- name: DeleteInvitedUserAccount :one
 | 
					-- name: DeleteInvitedUserAccount :one
 | 
				
			||||||
DELETE FROM user_account_invited WHERE user_account_invited_id = $1 RETURNING *;
 | 
					DELETE FROM user_account_invited WHERE user_account_invited_id = $1 RETURNING *;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- name: HasAnyUser :one
 | 
				
			||||||
 | 
					SELECT EXISTS(SELECT 1 FROM user_account WHERE username != 'system');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- name: HasActiveUser :one
 | 
				
			||||||
 | 
					SELECT EXISTS(SELECT 1 FROM user_account WHERE username != 'system' AND active = true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- name: CreateConfirmToken :one
 | 
				
			||||||
 | 
					INSERT INTO user_account_confirm_token (email) VALUES ($1) RETURNING *;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- name: GetConfirmTokenByEmail :one
 | 
				
			||||||
 | 
					SELECT * FROM user_account_confirm_token WHERE email = $1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- name: GetConfirmTokenByID :one
 | 
				
			||||||
 | 
					SELECT * FROM user_account_confirm_token WHERE confirm_token_id = $1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- name: SetFirstUserActive :one
 | 
				
			||||||
 | 
					UPDATE user_account SET active = true WHERE user_id = (
 | 
				
			||||||
 | 
					  SELECT user_id from user_account WHERE active = false LIMIT 1
 | 
				
			||||||
 | 
					) RETURNING *;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- name: SetUserActiveByEmail :one
 | 
				
			||||||
 | 
					UPDATE user_account SET active = true WHERE email = $1 RETURNING *;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- name: GetProjectsForInvitedMember :many
 | 
				
			||||||
 | 
					SELECT project_id FROM user_account_invited AS uai
 | 
				
			||||||
 | 
					  INNER JOIN project_member_invited AS pmi
 | 
				
			||||||
 | 
					  ON pmi.user_account_invited_id = uai.user_account_invited_id
 | 
				
			||||||
 | 
					  WHERE uai.email = $1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- name: DeleteProjectMemberInvitedForEmail :exec
 | 
				
			||||||
 | 
					DELETE FROM project_member_invited WHERE project_member_invited_id IN (
 | 
				
			||||||
 | 
					  SELECT pmi.project_member_invited_id FROM user_account_invited AS uai
 | 
				
			||||||
 | 
					  INNER JOIN project_member_invited AS pmi
 | 
				
			||||||
 | 
					  ON pmi.user_account_invited_id = uai.user_account_invited_id
 | 
				
			||||||
 | 
					  WHERE uai.email = $1
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- name: DeleteUserAccountInvitedForEmail :exec
 | 
				
			||||||
 | 
					DELETE FROM user_account_invited WHERE email = $1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- name: DeleteConfirmTokenForEmail :exec
 | 
				
			||||||
 | 
					DELETE FROM user_account_confirm_token WHERE email = $1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,6 +11,17 @@ import (
 | 
				
			|||||||
	"github.com/google/uuid"
 | 
						"github.com/google/uuid"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const createConfirmToken = `-- name: CreateConfirmToken :one
 | 
				
			||||||
 | 
					INSERT INTO user_account_confirm_token (email) VALUES ($1) RETURNING confirm_token_id, email
 | 
				
			||||||
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (q *Queries) CreateConfirmToken(ctx context.Context, email string) (UserAccountConfirmToken, error) {
 | 
				
			||||||
 | 
						row := q.db.QueryRowContext(ctx, createConfirmToken, email)
 | 
				
			||||||
 | 
						var i UserAccountConfirmToken
 | 
				
			||||||
 | 
						err := row.Scan(&i.ConfirmTokenID, &i.Email)
 | 
				
			||||||
 | 
						return i, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const createInvitedProjectMember = `-- name: CreateInvitedProjectMember :one
 | 
					const createInvitedProjectMember = `-- name: CreateInvitedProjectMember :one
 | 
				
			||||||
INSERT INTO project_member_invited (project_id, user_account_invited_id) VALUES ($1, $2)
 | 
					INSERT INTO project_member_invited (project_id, user_account_invited_id) VALUES ($1, $2)
 | 
				
			||||||
  RETURNING project_member_invited_id, project_id, user_account_invited_id
 | 
					  RETURNING project_member_invited_id, project_id, user_account_invited_id
 | 
				
			||||||
@@ -45,8 +56,8 @@ func (q *Queries) CreateInvitedUser(ctx context.Context, email string) (UserAcco
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const createUserAccount = `-- name: CreateUserAccount :one
 | 
					const createUserAccount = `-- name: CreateUserAccount :one
 | 
				
			||||||
INSERT INTO user_account(full_name, initials, email, username, created_at, password_hash, role_code)
 | 
					INSERT INTO user_account(full_name, initials, email, username, created_at, password_hash, role_code, active)
 | 
				
			||||||
  VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio
 | 
					  VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio, active
 | 
				
			||||||
`
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type CreateUserAccountParams struct {
 | 
					type CreateUserAccountParams struct {
 | 
				
			||||||
@@ -57,6 +68,7 @@ type CreateUserAccountParams struct {
 | 
				
			|||||||
	CreatedAt    time.Time `json:"created_at"`
 | 
						CreatedAt    time.Time `json:"created_at"`
 | 
				
			||||||
	PasswordHash string    `json:"password_hash"`
 | 
						PasswordHash string    `json:"password_hash"`
 | 
				
			||||||
	RoleCode     string    `json:"role_code"`
 | 
						RoleCode     string    `json:"role_code"`
 | 
				
			||||||
 | 
						Active       bool      `json:"active"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (q *Queries) CreateUserAccount(ctx context.Context, arg CreateUserAccountParams) (UserAccount, error) {
 | 
					func (q *Queries) CreateUserAccount(ctx context.Context, arg CreateUserAccountParams) (UserAccount, error) {
 | 
				
			||||||
@@ -68,6 +80,7 @@ func (q *Queries) CreateUserAccount(ctx context.Context, arg CreateUserAccountPa
 | 
				
			|||||||
		arg.CreatedAt,
 | 
							arg.CreatedAt,
 | 
				
			||||||
		arg.PasswordHash,
 | 
							arg.PasswordHash,
 | 
				
			||||||
		arg.RoleCode,
 | 
							arg.RoleCode,
 | 
				
			||||||
 | 
							arg.Active,
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
	var i UserAccount
 | 
						var i UserAccount
 | 
				
			||||||
	err := row.Scan(
 | 
						err := row.Scan(
 | 
				
			||||||
@@ -82,10 +95,20 @@ func (q *Queries) CreateUserAccount(ctx context.Context, arg CreateUserAccountPa
 | 
				
			|||||||
		&i.ProfileAvatarUrl,
 | 
							&i.ProfileAvatarUrl,
 | 
				
			||||||
		&i.RoleCode,
 | 
							&i.RoleCode,
 | 
				
			||||||
		&i.Bio,
 | 
							&i.Bio,
 | 
				
			||||||
 | 
							&i.Active,
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
	return i, err
 | 
						return i, err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const deleteConfirmTokenForEmail = `-- name: DeleteConfirmTokenForEmail :exec
 | 
				
			||||||
 | 
					DELETE FROM user_account_confirm_token WHERE email = $1
 | 
				
			||||||
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (q *Queries) DeleteConfirmTokenForEmail(ctx context.Context, email string) error {
 | 
				
			||||||
 | 
						_, err := q.db.ExecContext(ctx, deleteConfirmTokenForEmail, email)
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const deleteInvitedUserAccount = `-- name: DeleteInvitedUserAccount :one
 | 
					const deleteInvitedUserAccount = `-- name: DeleteInvitedUserAccount :one
 | 
				
			||||||
DELETE FROM user_account_invited WHERE user_account_invited_id = $1 RETURNING user_account_invited_id, email, invited_on, has_joined
 | 
					DELETE FROM user_account_invited WHERE user_account_invited_id = $1 RETURNING user_account_invited_id, email, invited_on, has_joined
 | 
				
			||||||
`
 | 
					`
 | 
				
			||||||
@@ -102,6 +125,20 @@ func (q *Queries) DeleteInvitedUserAccount(ctx context.Context, userAccountInvit
 | 
				
			|||||||
	return i, err
 | 
						return i, err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const deleteProjectMemberInvitedForEmail = `-- name: DeleteProjectMemberInvitedForEmail :exec
 | 
				
			||||||
 | 
					DELETE FROM project_member_invited WHERE project_member_invited_id IN (
 | 
				
			||||||
 | 
					  SELECT pmi.project_member_invited_id FROM user_account_invited AS uai
 | 
				
			||||||
 | 
					  INNER JOIN project_member_invited AS pmi
 | 
				
			||||||
 | 
					  ON pmi.user_account_invited_id = uai.user_account_invited_id
 | 
				
			||||||
 | 
					  WHERE uai.email = $1
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (q *Queries) DeleteProjectMemberInvitedForEmail(ctx context.Context, email string) error {
 | 
				
			||||||
 | 
						_, err := q.db.ExecContext(ctx, deleteProjectMemberInvitedForEmail, email)
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const deleteUserAccountByID = `-- name: DeleteUserAccountByID :exec
 | 
					const deleteUserAccountByID = `-- name: DeleteUserAccountByID :exec
 | 
				
			||||||
DELETE FROM user_account WHERE user_id = $1
 | 
					DELETE FROM user_account WHERE user_id = $1
 | 
				
			||||||
`
 | 
					`
 | 
				
			||||||
@@ -111,8 +148,17 @@ func (q *Queries) DeleteUserAccountByID(ctx context.Context, userID uuid.UUID) e
 | 
				
			|||||||
	return err
 | 
						return err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const deleteUserAccountInvitedForEmail = `-- name: DeleteUserAccountInvitedForEmail :exec
 | 
				
			||||||
 | 
					DELETE FROM user_account_invited WHERE email = $1
 | 
				
			||||||
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (q *Queries) DeleteUserAccountInvitedForEmail(ctx context.Context, email string) error {
 | 
				
			||||||
 | 
						_, err := q.db.ExecContext(ctx, deleteUserAccountInvitedForEmail, email)
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getAllUserAccounts = `-- name: GetAllUserAccounts :many
 | 
					const getAllUserAccounts = `-- name: GetAllUserAccounts :many
 | 
				
			||||||
SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio FROM user_account WHERE username != 'system'
 | 
					SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio, active FROM user_account WHERE username != 'system'
 | 
				
			||||||
`
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (q *Queries) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error) {
 | 
					func (q *Queries) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error) {
 | 
				
			||||||
@@ -136,6 +182,7 @@ func (q *Queries) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error)
 | 
				
			|||||||
			&i.ProfileAvatarUrl,
 | 
								&i.ProfileAvatarUrl,
 | 
				
			||||||
			&i.RoleCode,
 | 
								&i.RoleCode,
 | 
				
			||||||
			&i.Bio,
 | 
								&i.Bio,
 | 
				
			||||||
 | 
								&i.Active,
 | 
				
			||||||
		); err != nil {
 | 
							); err != nil {
 | 
				
			||||||
			return nil, err
 | 
								return nil, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -150,6 +197,28 @@ func (q *Queries) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error)
 | 
				
			|||||||
	return items, nil
 | 
						return items, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getConfirmTokenByEmail = `-- name: GetConfirmTokenByEmail :one
 | 
				
			||||||
 | 
					SELECT confirm_token_id, email FROM user_account_confirm_token WHERE email = $1
 | 
				
			||||||
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (q *Queries) GetConfirmTokenByEmail(ctx context.Context, email string) (UserAccountConfirmToken, error) {
 | 
				
			||||||
 | 
						row := q.db.QueryRowContext(ctx, getConfirmTokenByEmail, email)
 | 
				
			||||||
 | 
						var i UserAccountConfirmToken
 | 
				
			||||||
 | 
						err := row.Scan(&i.ConfirmTokenID, &i.Email)
 | 
				
			||||||
 | 
						return i, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getConfirmTokenByID = `-- name: GetConfirmTokenByID :one
 | 
				
			||||||
 | 
					SELECT confirm_token_id, email FROM user_account_confirm_token WHERE confirm_token_id = $1
 | 
				
			||||||
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (q *Queries) GetConfirmTokenByID(ctx context.Context, confirmTokenID uuid.UUID) (UserAccountConfirmToken, error) {
 | 
				
			||||||
 | 
						row := q.db.QueryRowContext(ctx, getConfirmTokenByID, confirmTokenID)
 | 
				
			||||||
 | 
						var i UserAccountConfirmToken
 | 
				
			||||||
 | 
						err := row.Scan(&i.ConfirmTokenID, &i.Email)
 | 
				
			||||||
 | 
						return i, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getInvitedUserAccounts = `-- name: GetInvitedUserAccounts :many
 | 
					const getInvitedUserAccounts = `-- name: GetInvitedUserAccounts :many
 | 
				
			||||||
SELECT user_account_invited_id, email, invited_on, has_joined FROM user_account_invited
 | 
					SELECT user_account_invited_id, email, invited_on, has_joined FROM user_account_invited
 | 
				
			||||||
`
 | 
					`
 | 
				
			||||||
@@ -199,7 +268,7 @@ func (q *Queries) GetInvitedUserByEmail(ctx context.Context, email string) (User
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getMemberData = `-- name: GetMemberData :many
 | 
					const getMemberData = `-- name: GetMemberData :many
 | 
				
			||||||
SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio FROM user_account
 | 
					SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio, active FROM user_account
 | 
				
			||||||
  WHERE username != 'system'
 | 
					  WHERE username != 'system'
 | 
				
			||||||
  AND user_id NOT IN (SELECT user_id FROM project_member WHERE project_id = $1)
 | 
					  AND user_id NOT IN (SELECT user_id FROM project_member WHERE project_id = $1)
 | 
				
			||||||
`
 | 
					`
 | 
				
			||||||
@@ -225,6 +294,7 @@ func (q *Queries) GetMemberData(ctx context.Context, projectID uuid.UUID) ([]Use
 | 
				
			|||||||
			&i.ProfileAvatarUrl,
 | 
								&i.ProfileAvatarUrl,
 | 
				
			||||||
			&i.RoleCode,
 | 
								&i.RoleCode,
 | 
				
			||||||
			&i.Bio,
 | 
								&i.Bio,
 | 
				
			||||||
 | 
								&i.Active,
 | 
				
			||||||
		); err != nil {
 | 
							); err != nil {
 | 
				
			||||||
			return nil, err
 | 
								return nil, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -239,6 +309,36 @@ func (q *Queries) GetMemberData(ctx context.Context, projectID uuid.UUID) ([]Use
 | 
				
			|||||||
	return items, nil
 | 
						return items, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getProjectsForInvitedMember = `-- name: GetProjectsForInvitedMember :many
 | 
				
			||||||
 | 
					SELECT project_id FROM user_account_invited AS uai
 | 
				
			||||||
 | 
					  INNER JOIN project_member_invited AS pmi
 | 
				
			||||||
 | 
					  ON pmi.user_account_invited_id = uai.user_account_invited_id
 | 
				
			||||||
 | 
					  WHERE uai.email = $1
 | 
				
			||||||
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (q *Queries) GetProjectsForInvitedMember(ctx context.Context, email string) ([]uuid.UUID, error) {
 | 
				
			||||||
 | 
						rows, err := q.db.QueryContext(ctx, getProjectsForInvitedMember, email)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer rows.Close()
 | 
				
			||||||
 | 
						var items []uuid.UUID
 | 
				
			||||||
 | 
						for rows.Next() {
 | 
				
			||||||
 | 
							var project_id uuid.UUID
 | 
				
			||||||
 | 
							if err := rows.Scan(&project_id); err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							items = append(items, project_id)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := rows.Close(); err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := rows.Err(); err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return items, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getRoleForUserID = `-- name: GetRoleForUserID :one
 | 
					const getRoleForUserID = `-- name: GetRoleForUserID :one
 | 
				
			||||||
SELECT username, role.code, role.name FROM user_account
 | 
					SELECT username, role.code, role.name FROM user_account
 | 
				
			||||||
  INNER JOIN role ON role.code = user_account.role_code
 | 
					  INNER JOIN role ON role.code = user_account.role_code
 | 
				
			||||||
@@ -258,8 +358,32 @@ func (q *Queries) GetRoleForUserID(ctx context.Context, userID uuid.UUID) (GetRo
 | 
				
			|||||||
	return i, err
 | 
						return i, err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getUserAccountByEmail = `-- name: GetUserAccountByEmail :one
 | 
				
			||||||
 | 
					SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio, active FROM user_account WHERE email = $1
 | 
				
			||||||
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (q *Queries) GetUserAccountByEmail(ctx context.Context, email string) (UserAccount, error) {
 | 
				
			||||||
 | 
						row := q.db.QueryRowContext(ctx, getUserAccountByEmail, email)
 | 
				
			||||||
 | 
						var i UserAccount
 | 
				
			||||||
 | 
						err := row.Scan(
 | 
				
			||||||
 | 
							&i.UserID,
 | 
				
			||||||
 | 
							&i.CreatedAt,
 | 
				
			||||||
 | 
							&i.Email,
 | 
				
			||||||
 | 
							&i.Username,
 | 
				
			||||||
 | 
							&i.PasswordHash,
 | 
				
			||||||
 | 
							&i.ProfileBgColor,
 | 
				
			||||||
 | 
							&i.FullName,
 | 
				
			||||||
 | 
							&i.Initials,
 | 
				
			||||||
 | 
							&i.ProfileAvatarUrl,
 | 
				
			||||||
 | 
							&i.RoleCode,
 | 
				
			||||||
 | 
							&i.Bio,
 | 
				
			||||||
 | 
							&i.Active,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						return i, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getUserAccountByID = `-- name: GetUserAccountByID :one
 | 
					const getUserAccountByID = `-- name: GetUserAccountByID :one
 | 
				
			||||||
SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio FROM user_account WHERE user_id = $1
 | 
					SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio, active FROM user_account WHERE user_id = $1
 | 
				
			||||||
`
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (q *Queries) GetUserAccountByID(ctx context.Context, userID uuid.UUID) (UserAccount, error) {
 | 
					func (q *Queries) GetUserAccountByID(ctx context.Context, userID uuid.UUID) (UserAccount, error) {
 | 
				
			||||||
@@ -277,12 +401,13 @@ func (q *Queries) GetUserAccountByID(ctx context.Context, userID uuid.UUID) (Use
 | 
				
			|||||||
		&i.ProfileAvatarUrl,
 | 
							&i.ProfileAvatarUrl,
 | 
				
			||||||
		&i.RoleCode,
 | 
							&i.RoleCode,
 | 
				
			||||||
		&i.Bio,
 | 
							&i.Bio,
 | 
				
			||||||
 | 
							&i.Active,
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
	return i, err
 | 
						return i, err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getUserAccountByUsername = `-- name: GetUserAccountByUsername :one
 | 
					const getUserAccountByUsername = `-- name: GetUserAccountByUsername :one
 | 
				
			||||||
SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio FROM user_account WHERE username = $1
 | 
					SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio, active FROM user_account WHERE username = $1
 | 
				
			||||||
`
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (q *Queries) GetUserAccountByUsername(ctx context.Context, username string) (UserAccount, error) {
 | 
					func (q *Queries) GetUserAccountByUsername(ctx context.Context, username string) (UserAccount, error) {
 | 
				
			||||||
@@ -300,12 +425,85 @@ func (q *Queries) GetUserAccountByUsername(ctx context.Context, username string)
 | 
				
			|||||||
		&i.ProfileAvatarUrl,
 | 
							&i.ProfileAvatarUrl,
 | 
				
			||||||
		&i.RoleCode,
 | 
							&i.RoleCode,
 | 
				
			||||||
		&i.Bio,
 | 
							&i.Bio,
 | 
				
			||||||
 | 
							&i.Active,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						return i, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const hasActiveUser = `-- name: HasActiveUser :one
 | 
				
			||||||
 | 
					SELECT EXISTS(SELECT 1 FROM user_account WHERE username != 'system' AND active = true)
 | 
				
			||||||
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (q *Queries) HasActiveUser(ctx context.Context) (bool, error) {
 | 
				
			||||||
 | 
						row := q.db.QueryRowContext(ctx, hasActiveUser)
 | 
				
			||||||
 | 
						var exists bool
 | 
				
			||||||
 | 
						err := row.Scan(&exists)
 | 
				
			||||||
 | 
						return exists, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const hasAnyUser = `-- name: HasAnyUser :one
 | 
				
			||||||
 | 
					SELECT EXISTS(SELECT 1 FROM user_account WHERE username != 'system')
 | 
				
			||||||
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (q *Queries) HasAnyUser(ctx context.Context) (bool, error) {
 | 
				
			||||||
 | 
						row := q.db.QueryRowContext(ctx, hasAnyUser)
 | 
				
			||||||
 | 
						var exists bool
 | 
				
			||||||
 | 
						err := row.Scan(&exists)
 | 
				
			||||||
 | 
						return exists, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const setFirstUserActive = `-- name: SetFirstUserActive :one
 | 
				
			||||||
 | 
					UPDATE user_account SET active = true WHERE user_id = (
 | 
				
			||||||
 | 
					  SELECT user_id from user_account WHERE active = false LIMIT 1
 | 
				
			||||||
 | 
					) RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio, active
 | 
				
			||||||
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (q *Queries) SetFirstUserActive(ctx context.Context) (UserAccount, error) {
 | 
				
			||||||
 | 
						row := q.db.QueryRowContext(ctx, setFirstUserActive)
 | 
				
			||||||
 | 
						var i UserAccount
 | 
				
			||||||
 | 
						err := row.Scan(
 | 
				
			||||||
 | 
							&i.UserID,
 | 
				
			||||||
 | 
							&i.CreatedAt,
 | 
				
			||||||
 | 
							&i.Email,
 | 
				
			||||||
 | 
							&i.Username,
 | 
				
			||||||
 | 
							&i.PasswordHash,
 | 
				
			||||||
 | 
							&i.ProfileBgColor,
 | 
				
			||||||
 | 
							&i.FullName,
 | 
				
			||||||
 | 
							&i.Initials,
 | 
				
			||||||
 | 
							&i.ProfileAvatarUrl,
 | 
				
			||||||
 | 
							&i.RoleCode,
 | 
				
			||||||
 | 
							&i.Bio,
 | 
				
			||||||
 | 
							&i.Active,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						return i, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const setUserActiveByEmail = `-- name: SetUserActiveByEmail :one
 | 
				
			||||||
 | 
					UPDATE user_account SET active = true WHERE email = $1 RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio, active
 | 
				
			||||||
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (q *Queries) SetUserActiveByEmail(ctx context.Context, email string) (UserAccount, error) {
 | 
				
			||||||
 | 
						row := q.db.QueryRowContext(ctx, setUserActiveByEmail, email)
 | 
				
			||||||
 | 
						var i UserAccount
 | 
				
			||||||
 | 
						err := row.Scan(
 | 
				
			||||||
 | 
							&i.UserID,
 | 
				
			||||||
 | 
							&i.CreatedAt,
 | 
				
			||||||
 | 
							&i.Email,
 | 
				
			||||||
 | 
							&i.Username,
 | 
				
			||||||
 | 
							&i.PasswordHash,
 | 
				
			||||||
 | 
							&i.ProfileBgColor,
 | 
				
			||||||
 | 
							&i.FullName,
 | 
				
			||||||
 | 
							&i.Initials,
 | 
				
			||||||
 | 
							&i.ProfileAvatarUrl,
 | 
				
			||||||
 | 
							&i.RoleCode,
 | 
				
			||||||
 | 
							&i.Bio,
 | 
				
			||||||
 | 
							&i.Active,
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
	return i, err
 | 
						return i, err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const setUserPassword = `-- name: SetUserPassword :one
 | 
					const setUserPassword = `-- name: SetUserPassword :one
 | 
				
			||||||
UPDATE user_account SET password_hash = $2 WHERE user_id = $1 RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio
 | 
					UPDATE user_account SET password_hash = $2 WHERE user_id = $1 RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio, active
 | 
				
			||||||
`
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type SetUserPasswordParams struct {
 | 
					type SetUserPasswordParams struct {
 | 
				
			||||||
@@ -328,13 +526,14 @@ func (q *Queries) SetUserPassword(ctx context.Context, arg SetUserPasswordParams
 | 
				
			|||||||
		&i.ProfileAvatarUrl,
 | 
							&i.ProfileAvatarUrl,
 | 
				
			||||||
		&i.RoleCode,
 | 
							&i.RoleCode,
 | 
				
			||||||
		&i.Bio,
 | 
							&i.Bio,
 | 
				
			||||||
 | 
							&i.Active,
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
	return i, err
 | 
						return i, err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const updateUserAccountInfo = `-- name: UpdateUserAccountInfo :one
 | 
					const updateUserAccountInfo = `-- name: UpdateUserAccountInfo :one
 | 
				
			||||||
UPDATE user_account SET bio = $2, full_name = $3, initials = $4, email = $5
 | 
					UPDATE user_account SET bio = $2, full_name = $3, initials = $4, email = $5
 | 
				
			||||||
  WHERE user_id = $1 RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio
 | 
					  WHERE user_id = $1 RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio, active
 | 
				
			||||||
`
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type UpdateUserAccountInfoParams struct {
 | 
					type UpdateUserAccountInfoParams struct {
 | 
				
			||||||
@@ -366,13 +565,14 @@ func (q *Queries) UpdateUserAccountInfo(ctx context.Context, arg UpdateUserAccou
 | 
				
			|||||||
		&i.ProfileAvatarUrl,
 | 
							&i.ProfileAvatarUrl,
 | 
				
			||||||
		&i.RoleCode,
 | 
							&i.RoleCode,
 | 
				
			||||||
		&i.Bio,
 | 
							&i.Bio,
 | 
				
			||||||
 | 
							&i.Active,
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
	return i, err
 | 
						return i, err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const updateUserAccountProfileAvatarURL = `-- name: UpdateUserAccountProfileAvatarURL :one
 | 
					const updateUserAccountProfileAvatarURL = `-- name: UpdateUserAccountProfileAvatarURL :one
 | 
				
			||||||
UPDATE user_account SET profile_avatar_url = $2 WHERE user_id = $1
 | 
					UPDATE user_account SET profile_avatar_url = $2 WHERE user_id = $1
 | 
				
			||||||
  RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio
 | 
					  RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio, active
 | 
				
			||||||
`
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type UpdateUserAccountProfileAvatarURLParams struct {
 | 
					type UpdateUserAccountProfileAvatarURLParams struct {
 | 
				
			||||||
@@ -395,12 +595,13 @@ func (q *Queries) UpdateUserAccountProfileAvatarURL(ctx context.Context, arg Upd
 | 
				
			|||||||
		&i.ProfileAvatarUrl,
 | 
							&i.ProfileAvatarUrl,
 | 
				
			||||||
		&i.RoleCode,
 | 
							&i.RoleCode,
 | 
				
			||||||
		&i.Bio,
 | 
							&i.Bio,
 | 
				
			||||||
 | 
							&i.Active,
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
	return i, err
 | 
						return i, err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const updateUserRole = `-- name: UpdateUserRole :one
 | 
					const updateUserRole = `-- name: UpdateUserRole :one
 | 
				
			||||||
UPDATE user_account SET role_code = $2 WHERE user_id = $1 RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio
 | 
					UPDATE user_account SET role_code = $2 WHERE user_id = $1 RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio, active
 | 
				
			||||||
`
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type UpdateUserRoleParams struct {
 | 
					type UpdateUserRoleParams struct {
 | 
				
			||||||
@@ -423,6 +624,7 @@ func (q *Queries) UpdateUserRole(ctx context.Context, arg UpdateUserRoleParams)
 | 
				
			|||||||
		&i.ProfileAvatarUrl,
 | 
							&i.ProfileAvatarUrl,
 | 
				
			||||||
		&i.RoleCode,
 | 
							&i.RoleCode,
 | 
				
			||||||
		&i.Bio,
 | 
							&i.Bio,
 | 
				
			||||||
 | 
							&i.Active,
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
	return i, err
 | 
						return i, err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,7 +5,9 @@ package graph
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
 | 
						"crypto/tls"
 | 
				
			||||||
	"database/sql"
 | 
						"database/sql"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
@@ -15,6 +17,9 @@ import (
 | 
				
			|||||||
	"github.com/jordanknott/taskcafe/internal/db"
 | 
						"github.com/jordanknott/taskcafe/internal/db"
 | 
				
			||||||
	"github.com/jordanknott/taskcafe/internal/logger"
 | 
						"github.com/jordanknott/taskcafe/internal/logger"
 | 
				
			||||||
	"github.com/lithammer/fuzzysearch/fuzzy"
 | 
						"github.com/lithammer/fuzzysearch/fuzzy"
 | 
				
			||||||
 | 
						gomail "gopkg.in/mail.v2"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						hermes "github.com/matcornic/hermes/v2"
 | 
				
			||||||
	log "github.com/sirupsen/logrus"
 | 
						log "github.com/sirupsen/logrus"
 | 
				
			||||||
	"github.com/vektah/gqlparser/v2/gqlerror"
 | 
						"github.com/vektah/gqlparser/v2/gqlerror"
 | 
				
			||||||
	"golang.org/x/crypto/bcrypt"
 | 
						"golang.org/x/crypto/bcrypt"
 | 
				
			||||||
@@ -185,6 +190,84 @@ func (r *mutationResolver) InviteProjectMembers(ctx context.Context, input Invit
 | 
				
			|||||||
					if err != nil {
 | 
										if err != nil {
 | 
				
			||||||
						return &InviteProjectMembersPayload{Ok: false}, err
 | 
											return &InviteProjectMembersPayload{Ok: false}, err
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
 | 
										confirmToken, err := r.Repository.CreateConfirmToken(ctx, *invitedMember.Email)
 | 
				
			||||||
 | 
										if err != nil {
 | 
				
			||||||
 | 
											return &InviteProjectMembersPayload{Ok: false}, err
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										// send out invitation
 | 
				
			||||||
 | 
										// add project invite entry
 | 
				
			||||||
 | 
										// send out notification?
 | 
				
			||||||
 | 
										h := hermes.Hermes{
 | 
				
			||||||
 | 
											// Optional Theme
 | 
				
			||||||
 | 
											Product: hermes.Product{
 | 
				
			||||||
 | 
												// Appears in header & footer of e-mails
 | 
				
			||||||
 | 
												Name: "Taskscafe",
 | 
				
			||||||
 | 
												Link: "http://localhost:3333/",
 | 
				
			||||||
 | 
												// Optional product logo
 | 
				
			||||||
 | 
												Logo: "https://github.com/JordanKnott/taskcafe/raw/master/.github/taskcafe-full.png",
 | 
				
			||||||
 | 
											},
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										email := hermes.Email{
 | 
				
			||||||
 | 
											Body: hermes.Body{
 | 
				
			||||||
 | 
												Name: "Jordan Knott",
 | 
				
			||||||
 | 
												Intros: []string{
 | 
				
			||||||
 | 
													"You have been invited to join Taskcafe",
 | 
				
			||||||
 | 
												},
 | 
				
			||||||
 | 
												Actions: []hermes.Action{
 | 
				
			||||||
 | 
													{
 | 
				
			||||||
 | 
														Instructions: "To get started with Taskcafe, please click here:",
 | 
				
			||||||
 | 
														Button: hermes.Button{
 | 
				
			||||||
 | 
															Color:     "#7367F0", // Optional action button color
 | 
				
			||||||
 | 
															TextColor: "#FFFFFF",
 | 
				
			||||||
 | 
															Text:      "Register your account",
 | 
				
			||||||
 | 
															Link:      "http://localhost:3000/register?confirmToken=" + confirmToken.ConfirmTokenID.String(),
 | 
				
			||||||
 | 
														},
 | 
				
			||||||
 | 
													},
 | 
				
			||||||
 | 
												},
 | 
				
			||||||
 | 
												Outros: []string{
 | 
				
			||||||
 | 
													"Need help, or have questions? Just reply to this email, we'd love to help.",
 | 
				
			||||||
 | 
												},
 | 
				
			||||||
 | 
											},
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										// Generate an HTML email with the provided contents (for modern clients)
 | 
				
			||||||
 | 
										emailBody, err := h.GenerateHTML(email)
 | 
				
			||||||
 | 
										if err != nil {
 | 
				
			||||||
 | 
											panic(err) // Tip: Handle error with something else than a panic ;)
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										emailBodyPlain, err := h.GeneratePlainText(email)
 | 
				
			||||||
 | 
										if err != nil {
 | 
				
			||||||
 | 
											panic(err) // Tip: Handle error with something else than a panic ;)
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										m := gomail.NewMessage()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										// Set E-Mail sender
 | 
				
			||||||
 | 
										m.SetHeader("From", "no-reply@taskcafe.com")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										// Set E-Mail receivers
 | 
				
			||||||
 | 
										m.SetHeader("To", invitedUser.Email)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										// Set E-Mail subject
 | 
				
			||||||
 | 
										m.SetHeader("Subject", "You have been invited to Taskcafe")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										// Set E-Mail body. You can set plain text or html with text/html
 | 
				
			||||||
 | 
										m.SetBody("text/html", emailBody)
 | 
				
			||||||
 | 
										m.AddAlternative("text/plain", emailBodyPlain)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										// Settings for SMTP server
 | 
				
			||||||
 | 
										d := gomail.NewDialer("127.0.0.1", 11500, "no-reply@taskcafe.com", "")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										// This is only needed when SSL/TLS certificate is not valid on server.
 | 
				
			||||||
 | 
										// In production this should be set to false.
 | 
				
			||||||
 | 
										d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										// Now send E-Mail
 | 
				
			||||||
 | 
										if err := d.DialAndSend(m); err != nil {
 | 
				
			||||||
 | 
											fmt.Println(err)
 | 
				
			||||||
 | 
											panic(err)
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
				} else {
 | 
									} else {
 | 
				
			||||||
					return &InviteProjectMembersPayload{Ok: false}, err
 | 
										return &InviteProjectMembersPayload{Ok: false}, err
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
@@ -199,9 +282,6 @@ func (r *mutationResolver) InviteProjectMembers(ctx context.Context, input Invit
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
			logger.New(ctx).Info("adding invited member")
 | 
								logger.New(ctx).Info("adding invited member")
 | 
				
			||||||
			invitedMembers = append(invitedMembers, InvitedMember{Email: *invitedMember.Email, InvitedOn: now})
 | 
								invitedMembers = append(invitedMembers, InvitedMember{Email: *invitedMember.Email, InvitedOn: now})
 | 
				
			||||||
			// send out invitation
 | 
					 | 
				
			||||||
			// add project invite entry
 | 
					 | 
				
			||||||
			// send out notification?
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -860,6 +940,11 @@ func (r *mutationResolver) DeleteInvitedUserAccount(ctx context.Context, input D
 | 
				
			|||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return &DeleteInvitedUserAccountPayload{}, err
 | 
							return &DeleteInvitedUserAccountPayload{}, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						err = r.Repository.DeleteConfirmTokenForEmail(ctx, user.Email)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							logger.New(ctx).WithError(err).Error("issue deleting confirm token")
 | 
				
			||||||
 | 
							return &DeleteInvitedUserAccountPayload{}, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	return &DeleteInvitedUserAccountPayload{
 | 
						return &DeleteInvitedUserAccountPayload{
 | 
				
			||||||
		InvitedUser: &InvitedUserAccount{
 | 
							InvitedUser: &InvitedUserAccount{
 | 
				
			||||||
			Email:     user.Email,
 | 
								Email:     user.Email,
 | 
				
			||||||
@@ -1354,6 +1439,7 @@ func (r *queryResolver) SearchMembers(ctx context.Context, input MemberSearchFil
 | 
				
			|||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				logger.New(ctx).WithField("id", rank.Target).Info("adding target")
 | 
									logger.New(ctx).WithField("id", rank.Target).Info("adding target")
 | 
				
			||||||
				results = append(results, MemberSearchResult{ID: rank.Target, Status: ShareStatusInvited, Similarity: rank.Distance})
 | 
									results = append(results, MemberSearchResult{ID: rank.Target, Status: ShareStatusInvited, Similarity: rank.Distance})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			memberList[entry.ID] = true
 | 
								memberList[entry.ID] = true
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -1642,7 +1728,9 @@ func (r *Resolver) Task() TaskResolver { return &taskResolver{r} }
 | 
				
			|||||||
func (r *Resolver) TaskChecklist() TaskChecklistResolver { return &taskChecklistResolver{r} }
 | 
					func (r *Resolver) TaskChecklist() TaskChecklistResolver { return &taskChecklistResolver{r} }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// TaskChecklistItem returns TaskChecklistItemResolver implementation.
 | 
					// TaskChecklistItem returns TaskChecklistItemResolver implementation.
 | 
				
			||||||
func (r *Resolver) TaskChecklistItem() TaskChecklistItemResolver { return &taskChecklistItemResolver{r} }
 | 
					func (r *Resolver) TaskChecklistItem() TaskChecklistItemResolver {
 | 
				
			||||||
 | 
						return &taskChecklistItemResolver{r}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// TaskGroup returns TaskGroupResolver implementation.
 | 
					// TaskGroup returns TaskGroupResolver implementation.
 | 
				
			||||||
func (r *Resolver) TaskGroup() TaskGroupResolver { return &taskGroupResolver{r} }
 | 
					func (r *Resolver) TaskGroup() TaskGroupResolver { return &taskGroupResolver{r} }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -31,15 +31,33 @@ type NewUserAccount struct {
 | 
				
			|||||||
	Email    string
 | 
						Email    string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RegisterUserRequestData is the request data for registering a new user (duh)
 | 
				
			||||||
 | 
					type RegisterUserRequestData struct {
 | 
				
			||||||
 | 
						User NewUserAccount
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type RegisteredUserResponseData struct {
 | 
				
			||||||
 | 
						Setup bool `json:"setup"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ConfirmUserRequestData is the request data for upgrading an invited user to a normal user
 | 
				
			||||||
 | 
					type ConfirmUserRequestData struct {
 | 
				
			||||||
 | 
						ConfirmToken string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// InstallRequestData is the request data for installing new Taskcafe app
 | 
					// InstallRequestData is the request data for installing new Taskcafe app
 | 
				
			||||||
type InstallRequestData struct {
 | 
					type InstallRequestData struct {
 | 
				
			||||||
	User NewUserAccount
 | 
						User NewUserAccount
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Setup struct {
 | 
				
			||||||
 | 
						ConfirmToken string `json:"confirmToken"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// LoginResponseData is the response data for when a user logs in
 | 
					// LoginResponseData is the response data for when a user logs in
 | 
				
			||||||
type LoginResponseData struct {
 | 
					type LoginResponseData struct {
 | 
				
			||||||
	AccessToken string `json:"accessToken"`
 | 
						AccessToken string `json:"accessToken"`
 | 
				
			||||||
	IsInstalled bool   `json:"isInstalled"`
 | 
						Setup       bool   `json:"setup"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// LogoutResponseData is the response data for when a user logs out
 | 
					// LogoutResponseData is the response data for when a user logs out
 | 
				
			||||||
@@ -60,30 +78,24 @@ type AvatarUploadResponseData struct {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// RefreshTokenHandler handles when a user attempts to refresh token
 | 
					// RefreshTokenHandler handles when a user attempts to refresh token
 | 
				
			||||||
func (h *TaskcafeHandler) RefreshTokenHandler(w http.ResponseWriter, r *http.Request) {
 | 
					func (h *TaskcafeHandler) RefreshTokenHandler(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
	_, err := h.repo.GetSystemOptionByKey(r.Context(), "is_installed")
 | 
						userExists, err := h.repo.HasAnyUser(r.Context())
 | 
				
			||||||
	if err == sql.ErrNoRows {
 | 
						if err != nil {
 | 
				
			||||||
		user, err := h.repo.GetUserAccountByUsername(r.Context(), "system")
 | 
							w.WriteHeader(http.StatusInternalServerError)
 | 
				
			||||||
		if err != nil {
 | 
							log.WithError(err).Error("issue while fetching if user accounts exist")
 | 
				
			||||||
			w.WriteHeader(http.StatusInternalServerError)
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		accessTokenString, err := auth.NewAccessToken(user.UserID.String(), auth.InstallOnly, user.RoleCode, h.jwtKey)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			w.WriteHeader(http.StatusInternalServerError)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		w.Header().Set("Content-type", "application/json")
 | 
					 | 
				
			||||||
		json.NewEncoder(w).Encode(LoginResponseData{AccessToken: accessTokenString, IsInstalled: false})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	} else if err != nil {
 | 
						}
 | 
				
			||||||
		log.WithError(err).Error("get system option")
 | 
					
 | 
				
			||||||
		w.WriteHeader(http.StatusBadRequest)
 | 
						log.WithField("userExists", userExists).Info("checking if setup")
 | 
				
			||||||
 | 
						if !userExists {
 | 
				
			||||||
 | 
							w.Header().Set("Content-type", "application/json")
 | 
				
			||||||
 | 
							json.NewEncoder(w).Encode(LoginResponseData{AccessToken: "", Setup: true})
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	c, err := r.Cookie("refreshToken")
 | 
						c, err := r.Cookie("refreshToken")
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		if err == http.ErrNoCookie {
 | 
							if err == http.ErrNoCookie {
 | 
				
			||||||
 | 
								log.Warn("no cookie")
 | 
				
			||||||
			w.WriteHeader(http.StatusBadRequest)
 | 
								w.WriteHeader(http.StatusBadRequest)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -112,6 +124,14 @@ func (h *TaskcafeHandler) RefreshTokenHandler(w http.ResponseWriter, r *http.Req
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !user.Active {
 | 
				
			||||||
 | 
							log.WithFields(log.Fields{
 | 
				
			||||||
 | 
								"username": user.Username,
 | 
				
			||||||
 | 
							}).Warn("attempt to refresh token with inactive user")
 | 
				
			||||||
 | 
							w.WriteHeader(http.StatusUnauthorized)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	refreshCreatedAt := time.Now().UTC()
 | 
						refreshCreatedAt := time.Now().UTC()
 | 
				
			||||||
	refreshExpiresAt := refreshCreatedAt.AddDate(0, 0, 1)
 | 
						refreshExpiresAt := refreshCreatedAt.AddDate(0, 0, 1)
 | 
				
			||||||
	refreshTokenString, err := h.repo.CreateRefreshToken(r.Context(), db.CreateRefreshTokenParams{token.UserID, refreshCreatedAt, refreshExpiresAt})
 | 
						refreshTokenString, err := h.repo.CreateRefreshToken(r.Context(), db.CreateRefreshTokenParams{token.UserID, refreshCreatedAt, refreshExpiresAt})
 | 
				
			||||||
@@ -119,13 +139,17 @@ func (h *TaskcafeHandler) RefreshTokenHandler(w http.ResponseWriter, r *http.Req
 | 
				
			|||||||
	err = h.repo.DeleteRefreshTokenByID(r.Context(), token.TokenID)
 | 
						err = h.repo.DeleteRefreshTokenByID(r.Context(), token.TokenID)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		w.WriteHeader(http.StatusInternalServerError)
 | 
							w.WriteHeader(http.StatusInternalServerError)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						log.Info("here 1")
 | 
				
			||||||
	accessTokenString, err := auth.NewAccessToken(token.UserID.String(), auth.Unrestricted, user.RoleCode, h.jwtKey)
 | 
						accessTokenString, err := auth.NewAccessToken(token.UserID.String(), auth.Unrestricted, user.RoleCode, h.jwtKey)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		w.WriteHeader(http.StatusInternalServerError)
 | 
							w.WriteHeader(http.StatusInternalServerError)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						log.Info("here 2")
 | 
				
			||||||
	w.Header().Set("Content-type", "application/json")
 | 
						w.Header().Set("Content-type", "application/json")
 | 
				
			||||||
	http.SetCookie(w, &http.Cookie{
 | 
						http.SetCookie(w, &http.Cookie{
 | 
				
			||||||
		Name:     "refreshToken",
 | 
							Name:     "refreshToken",
 | 
				
			||||||
@@ -133,7 +157,7 @@ func (h *TaskcafeHandler) RefreshTokenHandler(w http.ResponseWriter, r *http.Req
 | 
				
			|||||||
		Expires:  refreshExpiresAt,
 | 
							Expires:  refreshExpiresAt,
 | 
				
			||||||
		HttpOnly: true,
 | 
							HttpOnly: true,
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
	json.NewEncoder(w).Encode(LoginResponseData{AccessToken: accessTokenString, IsInstalled: true})
 | 
						json.NewEncoder(w).Encode(LoginResponseData{AccessToken: accessTokenString, Setup: false})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// LogoutHandler removes all refresh tokens to log out user
 | 
					// LogoutHandler removes all refresh tokens to log out user
 | 
				
			||||||
@@ -175,6 +199,14 @@ func (h *TaskcafeHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !user.Active {
 | 
				
			||||||
 | 
							log.WithFields(log.Fields{
 | 
				
			||||||
 | 
								"username": requestData.Username,
 | 
				
			||||||
 | 
							}).Warn("attempt to login with inactive user")
 | 
				
			||||||
 | 
							w.WriteHeader(http.StatusUnauthorized)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(requestData.Password))
 | 
						err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(requestData.Password))
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		log.WithFields(log.Fields{
 | 
							log.WithFields(log.Fields{
 | 
				
			||||||
@@ -203,6 +235,7 @@ func (h *TaskcafeHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
	json.NewEncoder(w).Encode(LoginResponseData{accessTokenString, false})
 | 
						json.NewEncoder(w).Encode(LoginResponseData{accessTokenString, false})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TODO: remove
 | 
				
			||||||
// InstallHandler creates first user on fresh install
 | 
					// InstallHandler creates first user on fresh install
 | 
				
			||||||
func (h *TaskcafeHandler) InstallHandler(w http.ResponseWriter, r *http.Request) {
 | 
					func (h *TaskcafeHandler) InstallHandler(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
	if restricted, ok := r.Context().Value("restricted_mode").(auth.RestrictedMode); ok {
 | 
						if restricted, ok := r.Context().Value("restricted_mode").(auth.RestrictedMode); ok {
 | 
				
			||||||
@@ -266,6 +299,172 @@ func (h *TaskcafeHandler) InstallHandler(w http.ResponseWriter, r *http.Request)
 | 
				
			|||||||
	json.NewEncoder(w).Encode(LoginResponseData{accessTokenString, false})
 | 
						json.NewEncoder(w).Encode(LoginResponseData{accessTokenString, false})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (h *TaskcafeHandler) ConfirmUser(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
						usersExist, err := h.repo.HasActiveUser(r.Context())
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.WithError(err).Error("issue checking if user accounts exist")
 | 
				
			||||||
 | 
							w.WriteHeader(http.StatusInternalServerError)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						var user db.UserAccount
 | 
				
			||||||
 | 
						if !usersExist {
 | 
				
			||||||
 | 
							log.Info("setting first inactive user to active")
 | 
				
			||||||
 | 
							user, err = h.repo.SetFirstUserActive(r.Context())
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.WithError(err).Error("issue checking if user accounts exist")
 | 
				
			||||||
 | 
								w.WriteHeader(http.StatusInternalServerError)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							var requestData ConfirmUserRequestData
 | 
				
			||||||
 | 
							err = json.NewDecoder(r.Body).Decode(&requestData)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.WithError(err).Error("issue decoding request data")
 | 
				
			||||||
 | 
								w.WriteHeader(http.StatusBadRequest)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							confirmTokenID, err := uuid.Parse(requestData.ConfirmToken)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.WithError(err).Error("issue parsing confirm token")
 | 
				
			||||||
 | 
								w.WriteHeader(http.StatusBadRequest)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							confirmToken, err := h.repo.GetConfirmTokenByID(r.Context(), confirmTokenID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.WithError(err).Error("issue getting token by id")
 | 
				
			||||||
 | 
								w.WriteHeader(http.StatusBadRequest)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							user, err = h.repo.SetUserActiveByEmail(r.Context(), confirmToken.Email)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.WithError(err).Error("issue getting account by email")
 | 
				
			||||||
 | 
								w.WriteHeader(http.StatusBadRequest)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						now := time.Now().UTC()
 | 
				
			||||||
 | 
						projects, err := h.repo.GetProjectsForInvitedMember(r.Context(), user.Email)
 | 
				
			||||||
 | 
						for _, project := range projects {
 | 
				
			||||||
 | 
							member, err := h.repo.CreateProjectMember(r.Context(),
 | 
				
			||||||
 | 
								db.CreateProjectMemberParams{
 | 
				
			||||||
 | 
									ProjectID: project,
 | 
				
			||||||
 | 
									UserID:    user.UserID,
 | 
				
			||||||
 | 
									AddedAt:   now,
 | 
				
			||||||
 | 
									RoleCode:  "member",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.WithError(err).Error("issue creating project member")
 | 
				
			||||||
 | 
								w.WriteHeader(http.StatusInternalServerError)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							log.WithField("memberID", member.ProjectMemberID).Info("creating project member")
 | 
				
			||||||
 | 
							err = h.repo.DeleteProjectMemberInvitedForEmail(r.Context(), user.Email)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.WithError(err).Error("issue deleting project member invited")
 | 
				
			||||||
 | 
								w.WriteHeader(http.StatusInternalServerError)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							err = h.repo.DeleteUserAccountInvitedForEmail(r.Context(), user.Email)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.WithError(err).Error("issue deleting user account invited")
 | 
				
			||||||
 | 
								w.WriteHeader(http.StatusInternalServerError)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							err = h.repo.DeleteConfirmTokenForEmail(r.Context(), user.Email)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.WithError(err).Error("issue deleting confirm token")
 | 
				
			||||||
 | 
								w.WriteHeader(http.StatusInternalServerError)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						refreshCreatedAt := time.Now().UTC()
 | 
				
			||||||
 | 
						refreshExpiresAt := refreshCreatedAt.AddDate(0, 0, 1)
 | 
				
			||||||
 | 
						refreshTokenString, err := h.repo.CreateRefreshToken(r.Context(), db.CreateRefreshTokenParams{user.UserID, refreshCreatedAt, refreshExpiresAt})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						accessTokenString, err := auth.NewAccessToken(user.UserID.String(), auth.Unrestricted, user.RoleCode, h.jwtKey)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							w.WriteHeader(http.StatusInternalServerError)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						w.Header().Set("Content-type", "application/json")
 | 
				
			||||||
 | 
						http.SetCookie(w, &http.Cookie{
 | 
				
			||||||
 | 
							Name:     "refreshToken",
 | 
				
			||||||
 | 
							Value:    refreshTokenString.TokenID.String(),
 | 
				
			||||||
 | 
							Expires:  refreshExpiresAt,
 | 
				
			||||||
 | 
							HttpOnly: true,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						json.NewEncoder(w).Encode(LoginResponseData{accessTokenString, false})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (h *TaskcafeHandler) RegisterUser(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
						userExists, err := h.repo.HasAnyUser(r.Context())
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.WithError(err).Error("issue checking if user accounts exist")
 | 
				
			||||||
 | 
							w.WriteHeader(http.StatusInternalServerError)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var requestData RegisterUserRequestData
 | 
				
			||||||
 | 
						err = json.NewDecoder(r.Body).Decode(&requestData)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.WithError(err).Error("issue decoding register user request data")
 | 
				
			||||||
 | 
							w.WriteHeader(http.StatusBadRequest)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if userExists {
 | 
				
			||||||
 | 
							_, err := h.repo.GetInvitedUserByEmail(r.Context(), requestData.User.Email)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if err == sql.ErrNoRows {
 | 
				
			||||||
 | 
									hasActiveUser, err := h.repo.HasActiveUser(r.Context())
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										log.WithError(err).Error("error checking for active user")
 | 
				
			||||||
 | 
										w.WriteHeader(http.StatusInternalServerError)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									if !hasActiveUser {
 | 
				
			||||||
 | 
										json.NewEncoder(w).Encode(RegisteredUserResponseData{Setup: true})
 | 
				
			||||||
 | 
										return
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									log.WithError(err).Error("error while retrieving invited user by email")
 | 
				
			||||||
 | 
									w.WriteHeader(http.StatusForbidden)
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// TODO: accept user if public registration is enabled
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						createdAt := time.Now().UTC()
 | 
				
			||||||
 | 
						hashedPwd, err := bcrypt.GenerateFromPassword([]byte(requestData.User.Password), 14)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Error("issue generating passoed")
 | 
				
			||||||
 | 
							w.WriteHeader(http.StatusInternalServerError)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						user, err := h.repo.CreateUserAccount(r.Context(), db.CreateUserAccountParams{
 | 
				
			||||||
 | 
							FullName:     requestData.User.FullName,
 | 
				
			||||||
 | 
							Username:     requestData.User.Username,
 | 
				
			||||||
 | 
							Initials:     requestData.User.Initials,
 | 
				
			||||||
 | 
							Email:        requestData.User.Email,
 | 
				
			||||||
 | 
							PasswordHash: string(hashedPwd),
 | 
				
			||||||
 | 
							CreatedAt:    createdAt,
 | 
				
			||||||
 | 
							RoleCode:     "admin",
 | 
				
			||||||
 | 
							Active:       false,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Error("issue registering user account")
 | 
				
			||||||
 | 
							w.WriteHeader(http.StatusInternalServerError)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						log.WithField("username", user.UserID).Info("registered new user account")
 | 
				
			||||||
 | 
						json.NewEncoder(w).Encode(RegisteredUserResponseData{Setup: !userExists})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Routes registers all authentication routes
 | 
					// Routes registers all authentication routes
 | 
				
			||||||
func (rs authResource) Routes(taskcafeHandler TaskcafeHandler) chi.Router {
 | 
					func (rs authResource) Routes(taskcafeHandler TaskcafeHandler) chi.Router {
 | 
				
			||||||
	r := chi.NewRouter()
 | 
						r := chi.NewRouter()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -87,13 +87,13 @@ func NewRouter(dbConnection *sqlx.DB, jwtKey []byte) (chi.Router, error) {
 | 
				
			|||||||
		mux.Mount("/auth", authResource{}.Routes(taskcafeHandler))
 | 
							mux.Mount("/auth", authResource{}.Routes(taskcafeHandler))
 | 
				
			||||||
		mux.Handle("/__graphql", graph.NewPlaygroundHandler("/graphql"))
 | 
							mux.Handle("/__graphql", graph.NewPlaygroundHandler("/graphql"))
 | 
				
			||||||
		mux.Mount("/uploads/", http.StripPrefix("/uploads/", imgServer))
 | 
							mux.Mount("/uploads/", http.StripPrefix("/uploads/", imgServer))
 | 
				
			||||||
 | 
							mux.Post("/auth/confirm", taskcafeHandler.ConfirmUser)
 | 
				
			||||||
 | 
							mux.Post("/auth/register", taskcafeHandler.RegisterUser)
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
	auth := AuthenticationMiddleware{jwtKey}
 | 
						auth := AuthenticationMiddleware{jwtKey}
 | 
				
			||||||
	r.Group(func(mux chi.Router) {
 | 
						r.Group(func(mux chi.Router) {
 | 
				
			||||||
		mux.Use(auth.Middleware)
 | 
							mux.Use(auth.Middleware)
 | 
				
			||||||
		mux.Post("/users/me/avatar", taskcafeHandler.ProfileImageUpload)
 | 
							mux.Post("/users/me/avatar", taskcafeHandler.ProfileImageUpload)
 | 
				
			||||||
		mux.Post("/auth/install", taskcafeHandler.InstallHandler)
 | 
					 | 
				
			||||||
		mux.Handle("/graphql", graph.NewHandler(*repository))
 | 
							mux.Handle("/graphql", graph.NewHandler(*repository))
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								migrations/0058_add-active-column-to-user_account.up.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								migrations/0058_add-active-column-to-user_account.up.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					ALTER TABLE user_account ADD COLUMN active boolean NOT NULL DEFAULT false;
 | 
				
			||||||
 | 
					UPDATE user_account SET active = true;
 | 
				
			||||||
							
								
								
									
										4
									
								
								migrations/0059_add-confirm_token_table.up.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								migrations/0059_add-confirm_token_table.up.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					CREATE TABLE user_account_confirm_token (
 | 
				
			||||||
 | 
					  confirm_token_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
 | 
				
			||||||
 | 
					  email text NOT NULL UNIQUE
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
		Reference in New Issue
	
	Block a user