feat: enforce user roles
enforces user admin role requirement for - creating / deleting / setting role for organization users - creating / deleting / setting role for project users - updating project name - deleting project hides action elements based on role for - admin console - team settings if team is only visible through project membership - add project tile if not team admin - project name text editor if not team / project admin - add redirect from team page if settings only visible through project membership - add redirect from admin console if not org admin role enforcement is handled on the api side through a custom GraphQL directive `hasRole`. on the client side, role information is fetched in the TopNavbar's `me` query and stored in the `UserContext`. there is a custom hook, `useCurrentUser`, that provides a user object with two functions, `isVisibile` & `isAdmin` which is used to check roles in order to render/hide relevant UI elements.
This commit is contained in:
committed by
Jordan Knott
parent
5dbdc20b36
commit
e64f6f8569
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState, useContext } from 'react';
|
||||
import Admin from 'shared/components/Admin';
|
||||
import Select from 'shared/components/Select';
|
||||
import GlobalTopNavbar from 'App/TopNavbar';
|
||||
@ -16,6 +16,8 @@ import { useForm, Controller } from 'react-hook-form';
|
||||
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
||||
import produce from 'immer';
|
||||
import updateApolloCache from 'shared/utils/cache';
|
||||
import UserContext, { useCurrentUser } from 'App/context';
|
||||
import { Redirect } from 'react-router';
|
||||
|
||||
const DeleteUserWrapper = styled.div`
|
||||
display: flex;
|
||||
@ -170,6 +172,7 @@ const AdminRoute = () => {
|
||||
}, []);
|
||||
const { loading, data } = useUsersQuery();
|
||||
const { showPopup, hidePopup } = usePopup();
|
||||
const { user } = useCurrentUser();
|
||||
const [deleteUser] = useDeleteUserAccountMutation({
|
||||
update: (client, response) => {
|
||||
updateApolloCache<UsersQuery>(client, UsersDocument, cache =>
|
||||
@ -201,13 +204,17 @@ const AdminRoute = () => {
|
||||
if (loading) {
|
||||
return <GlobalTopNavbar projectID={null} onSaveProjectName={() => {}} name={null} />;
|
||||
}
|
||||
if (data) {
|
||||
if (data && user) {
|
||||
if (user.roles.org != 'admin') {
|
||||
return <Redirect to="/" />;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<GlobalTopNavbar projectID={null} onSaveProjectName={() => {}} name={null} />
|
||||
<Admin
|
||||
initialTab={0}
|
||||
users={data.users}
|
||||
canInviteUser={user.roles.org == 'admin'}
|
||||
onInviteUser={() => {}}
|
||||
onUpdateUserPassword={(user, password) => {
|
||||
console.log(user);
|
||||
|
@ -1,31 +0,0 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Home, Stack } from 'shared/icons';
|
||||
import Navbar, { ActionButton, ButtonContainer, PrimaryLogo } from 'shared/components/Navbar';
|
||||
import { Link } from 'react-router-dom';
|
||||
import UserIDContext from './context';
|
||||
|
||||
const GlobalNavbar = () => {
|
||||
const { userID } = useContext(UserIDContext);
|
||||
if (!userID) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Navbar>
|
||||
<PrimaryLogo />
|
||||
<ButtonContainer>
|
||||
<Link to="/">
|
||||
<ActionButton name="Home">
|
||||
<Home width={28} height={28} />
|
||||
</ActionButton>
|
||||
</Link>
|
||||
<Link to="/projects">
|
||||
<ActionButton name="Projects">
|
||||
<Stack size={28} color="#c2c6dc" />
|
||||
</ActionButton>
|
||||
</Link>
|
||||
</ButtonContainer>
|
||||
</Navbar>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalNavbar;
|
@ -4,7 +4,7 @@ import styled from 'styled-components/macro';
|
||||
import DropdownMenu, { ProfileMenu } from 'shared/components/DropdownMenu';
|
||||
import ProjectSettings, { DeleteConfirm, DELETE_INFO } from 'shared/components/ProjectSettings';
|
||||
import { useHistory } from 'react-router';
|
||||
import UserIDContext from 'App/context';
|
||||
import { UserContext, PermissionLevel, PermissionObjectType, useCurrentUser } from 'App/context';
|
||||
import {
|
||||
RoleCode,
|
||||
useMeQuery,
|
||||
@ -16,6 +16,8 @@ import { usePopup, Popup } from 'shared/components/PopupMenu';
|
||||
import { History } from 'history';
|
||||
import produce from 'immer';
|
||||
import { Link } from 'react-router-dom';
|
||||
import MiniProfile from 'shared/components/MiniProfile';
|
||||
import cache from 'App/cache';
|
||||
|
||||
const TeamContainer = styled.div`
|
||||
display: flex;
|
||||
@ -221,6 +223,7 @@ export const ProjectPopup: React.FC<ProjectPopupProps> = ({ history, name, proje
|
||||
type GlobalTopNavbarProps = {
|
||||
nameOnly?: boolean;
|
||||
projectID: string | null;
|
||||
teamID?: string | null;
|
||||
onChangeProjectOwner?: (userID: string) => void;
|
||||
name: string | null;
|
||||
currentTab?: number;
|
||||
@ -239,6 +242,7 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
|
||||
onSetTab,
|
||||
menuType,
|
||||
projectID,
|
||||
teamID,
|
||||
onChangeProjectOwner,
|
||||
onChangeRole,
|
||||
name,
|
||||
@ -250,10 +254,27 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
|
||||
nameOnly,
|
||||
}) => {
|
||||
console.log(popupContent);
|
||||
const { loading, data } = useMeQuery();
|
||||
const { user, setUserRoles, setUser } = useCurrentUser();
|
||||
const { loading, data } = useMeQuery({
|
||||
onCompleted: data => {
|
||||
console.log('me query has completed!');
|
||||
if (user && user.roles) {
|
||||
setUserRoles({
|
||||
org: user.roles.org,
|
||||
teams: data.me.teamRoles.reduce((map, obj) => {
|
||||
map.set(obj.teamID, obj.roleCode);
|
||||
return map;
|
||||
}, new Map<string, string>()),
|
||||
projects: data.me.projectRoles.reduce((map, obj) => {
|
||||
map.set(obj.projectID, obj.roleCode);
|
||||
return map;
|
||||
}, new Map<string, string>()),
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
const { showPopup, hidePopup, setTab } = usePopup();
|
||||
const history = useHistory();
|
||||
const { userID, setUserID } = useContext(UserIDContext);
|
||||
const onLogout = () => {
|
||||
fetch('/auth/logout', {
|
||||
method: 'POST',
|
||||
@ -261,8 +282,9 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
|
||||
}).then(async x => {
|
||||
const { status } = x;
|
||||
if (status === 200) {
|
||||
cache.reset();
|
||||
history.replace('/login');
|
||||
setUserID(null);
|
||||
setUser(null);
|
||||
hidePopup();
|
||||
}
|
||||
});
|
||||
@ -273,6 +295,7 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
|
||||
<Popup title={null} tab={0}>
|
||||
<ProfileMenu
|
||||
onLogout={onLogout}
|
||||
showAdminConsole={user ? user.roles.org === 'admin' : false}
|
||||
onAdminConsole={() => {
|
||||
history.push('/admin');
|
||||
hidePopup();
|
||||
@ -295,9 +318,41 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
if (!userID) {
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
const userIsTeamOrProjectAdmin = user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID);
|
||||
const onMemberProfile = ($targetRef: React.RefObject<HTMLElement>, memberID: string) => {
|
||||
const member = projectMembers ? projectMembers.find(u => u.id === memberID) : null;
|
||||
const warning =
|
||||
'You can’t leave because you are the only admin. To make another user an admin, click their avatar, select “Change permissions…”, and select “Admin”.';
|
||||
if (member) {
|
||||
showPopup(
|
||||
$targetRef,
|
||||
<MiniProfile
|
||||
warning={member.role && member.role.code === 'owner' ? warning : null}
|
||||
canChangeRole={userIsTeamOrProjectAdmin}
|
||||
onChangeRole={roleCode => {
|
||||
if (onChangeRole) {
|
||||
onChangeRole(member.id, roleCode);
|
||||
}
|
||||
}}
|
||||
onRemoveFromBoard={
|
||||
member.role && member.role.code === 'owner'
|
||||
? undefined
|
||||
: () => {
|
||||
if (onRemoveFromBoard) {
|
||||
onRemoveFromBoard(member.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
user={member}
|
||||
bio=""
|
||||
/>,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TopNavbar
|
||||
@ -312,7 +367,10 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
|
||||
);
|
||||
}}
|
||||
currentTab={currentTab}
|
||||
user={data ? data.me : null}
|
||||
user={data ? data.me.user : null}
|
||||
canEditProjectName={userIsTeamOrProjectAdmin}
|
||||
canInviteUser={userIsTeamOrProjectAdmin}
|
||||
onMemberProfile={onMemberProfile}
|
||||
onInviteUser={onInviteUser}
|
||||
onChangeRole={onChangeRole}
|
||||
onChangeProjectOwner={onChangeProjectOwner}
|
||||
|
5
frontend/src/App/cache.ts
Normal file
5
frontend/src/App/cache.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { InMemoryCache } from 'apollo-cache-inmemory';
|
||||
|
||||
const cache = new InMemoryCache();
|
||||
|
||||
export default cache;
|
@ -1,9 +1,80 @@
|
||||
import React from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
|
||||
type UserIDContextState = {
|
||||
userID: string | null;
|
||||
setUserID: (userID: string | null) => void;
|
||||
export enum PermissionLevel {
|
||||
ORG,
|
||||
TEAM,
|
||||
PROJECT,
|
||||
}
|
||||
|
||||
export enum PermissionObjectType {
|
||||
ORG,
|
||||
TEAM,
|
||||
PROJECT,
|
||||
TASK,
|
||||
}
|
||||
|
||||
export type CurrentUserRoles = {
|
||||
org: string;
|
||||
teams: Map<string, string>;
|
||||
projects: Map<string, string>;
|
||||
};
|
||||
export const UserIDContext = React.createContext<UserIDContextState>({ userID: null, setUserID: _userID => null });
|
||||
|
||||
export default UserIDContext;
|
||||
export interface CurrentUserRaw {
|
||||
id: string;
|
||||
roles: CurrentUserRoles;
|
||||
}
|
||||
|
||||
type UserContextState = {
|
||||
user: CurrentUserRaw | null;
|
||||
setUser: (user: CurrentUserRaw | null) => void;
|
||||
setUserRoles: (roles: CurrentUserRoles) => void;
|
||||
};
|
||||
export const UserContext = React.createContext<UserContextState>({
|
||||
user: null,
|
||||
setUser: _user => null,
|
||||
setUserRoles: roles => null,
|
||||
});
|
||||
|
||||
export interface CurrentUser extends CurrentUserRaw {
|
||||
isAdmin: (level: PermissionLevel, objectType: PermissionObjectType, subjectID?: string | null) => boolean;
|
||||
isVisible: (level: PermissionLevel, objectType: PermissionObjectType, subjectID?: string | null) => boolean;
|
||||
}
|
||||
|
||||
export const useCurrentUser = () => {
|
||||
const { user, setUser, setUserRoles } = useContext(UserContext);
|
||||
let currentUser: CurrentUser | null = null;
|
||||
if (user) {
|
||||
currentUser = {
|
||||
...user,
|
||||
isAdmin(level: PermissionLevel, objectType: PermissionObjectType, subjectID?: string | null) {
|
||||
if (user.roles.org === 'admin') {
|
||||
return true;
|
||||
}
|
||||
switch (level) {
|
||||
case PermissionLevel.TEAM:
|
||||
return subjectID ? this.roles.teams.get(subjectID) === 'admin' : false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
},
|
||||
isVisible(level: PermissionLevel, objectType: PermissionObjectType, subjectID?: string | null) {
|
||||
if (user.roles.org === 'admin') {
|
||||
return true;
|
||||
}
|
||||
switch (level) {
|
||||
case PermissionLevel.TEAM:
|
||||
return subjectID ? this.roles.teams.get(subjectID) !== null : false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
user: currentUser,
|
||||
setUser,
|
||||
setUserRoles,
|
||||
};
|
||||
};
|
||||
|
||||
export default UserContext;
|
||||
|
@ -9,8 +9,7 @@ import NormalizeStyles from './NormalizeStyles';
|
||||
import BaseStyles from './BaseStyles';
|
||||
import { theme } from './ThemeStyles';
|
||||
import Routes from './Routes';
|
||||
import { UserIDContext } from './context';
|
||||
import Navbar from './Navbar';
|
||||
import { UserContext, CurrentUserRaw, CurrentUserRoles, PermissionLevel, PermissionObjectType } from './context';
|
||||
|
||||
const history = createBrowserHistory();
|
||||
type RefreshTokenResponse = {
|
||||
@ -20,7 +19,15 @@ type RefreshTokenResponse = {
|
||||
|
||||
const App = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [userID, setUserID] = useState<string | null>(null);
|
||||
const [user, setUser] = useState<CurrentUserRaw | null>(null);
|
||||
const setUserRoles = (roles: CurrentUserRoles) => {
|
||||
if (user) {
|
||||
setUser({
|
||||
...user,
|
||||
roles,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/auth/refresh_token', {
|
||||
@ -34,7 +41,11 @@ const App = () => {
|
||||
const response: RefreshTokenResponse = await x.json();
|
||||
const { accessToken, isInstalled } = response;
|
||||
const claims: JWTToken = jwtDecode(accessToken);
|
||||
setUserID(claims.userId);
|
||||
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');
|
||||
@ -46,7 +57,7 @@ const App = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<UserIDContext.Provider value={{ userID, setUserID }}>
|
||||
<UserContext.Provider value={{ user, setUser, setUserRoles }}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<NormalizeStyles />
|
||||
<BaseStyles />
|
||||
@ -62,7 +73,7 @@ const App = () => {
|
||||
</PopupProvider>
|
||||
</Router>
|
||||
</ThemeProvider>
|
||||
</UserIDContext.Provider>
|
||||
</UserContext.Provider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -6,13 +6,13 @@ import { setAccessToken } from 'shared/utils/accessToken';
|
||||
|
||||
import Login from 'shared/components/Login';
|
||||
import { Container, LoginWrapper } from './Styles';
|
||||
import UserIDContext from 'App/context';
|
||||
import UserContext, { PermissionLevel, PermissionObjectType } from 'App/context';
|
||||
import JwtDecode from 'jwt-decode';
|
||||
|
||||
const Auth = () => {
|
||||
const [invalidLoginAttempt, setInvalidLoginAttempt] = useState(0);
|
||||
const history = useHistory();
|
||||
const { setUserID } = useContext(UserIDContext);
|
||||
const { setUser } = useContext(UserContext);
|
||||
const login = (
|
||||
data: LoginFormData,
|
||||
setComplete: (val: boolean) => void,
|
||||
@ -35,7 +35,11 @@ const Auth = () => {
|
||||
const response = await x.json();
|
||||
const { accessToken } = response;
|
||||
const claims: JWTToken = JwtDecode(accessToken);
|
||||
setUserID(claims.userId);
|
||||
const currentUser = {
|
||||
id: claims.userId,
|
||||
roles: { org: claims.orgRole, teams: new Map<string, string>(), projects: new Map<string, string>() },
|
||||
};
|
||||
setUser(currentUser);
|
||||
setComplete(true);
|
||||
setAccessToken(accessToken);
|
||||
|
||||
|
@ -8,13 +8,13 @@ import { getAccessToken, setAccessToken } from 'shared/utils/accessToken';
|
||||
import updateApolloCache from 'shared/utils/cache';
|
||||
import produce from 'immer';
|
||||
import { useApolloClient } from '@apollo/react-hooks';
|
||||
import UserIDContext from 'App/context';
|
||||
import UserContext, { PermissionLevel, PermissionObjectType } from 'App/context';
|
||||
import jwtDecode from 'jwt-decode';
|
||||
|
||||
const Install = () => {
|
||||
const client = useApolloClient();
|
||||
const history = useHistory();
|
||||
const { setUserID } = useContext(UserIDContext);
|
||||
const { setUser } = useContext(UserContext);
|
||||
useEffect(() => {
|
||||
fetch('/auth/refresh_token', {
|
||||
method: 'POST',
|
||||
@ -65,7 +65,15 @@ const Install = () => {
|
||||
const response: RefreshTokenResponse = await x.data;
|
||||
const { accessToken, isInstalled } = response;
|
||||
const claims: JWTToken = jwtDecode(accessToken);
|
||||
setUserID(claims.userId);
|
||||
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');
|
||||
|
@ -3,9 +3,7 @@ import styled from 'styled-components/macro';
|
||||
import GlobalTopNavbar from 'App/TopNavbar';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { getAccessToken } from 'shared/utils/accessToken';
|
||||
import Navbar from 'App/Navbar';
|
||||
import Settings from 'shared/components/Settings';
|
||||
import UserIDContext from 'App/context';
|
||||
import { useMeQuery, useClearProfileAvatarMutation } from 'shared/generated/graphql';
|
||||
import axios from 'axios';
|
||||
|
||||
@ -53,7 +51,7 @@ const Projects = () => {
|
||||
<GlobalTopNavbar projectID={null} onSaveProjectName={() => {}} name={null} />
|
||||
{!loading && data && (
|
||||
<Settings
|
||||
profile={data.me}
|
||||
profile={data.me.user}
|
||||
onProfileAvatarChange={() => {
|
||||
if ($fileUpload && $fileUpload.current) {
|
||||
$fileUpload.current.click();
|
||||
|
@ -8,7 +8,6 @@ import { Bolt, ToggleOn, Tags, CheckCircle, Sort, Filter } from 'shared/icons';
|
||||
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
||||
import { useParams, Route, useRouteMatch, useHistory, RouteComponentProps, useLocation } from 'react-router-dom';
|
||||
import {
|
||||
useSetProjectOwnerMutation,
|
||||
useUpdateProjectMemberRoleMutation,
|
||||
useCreateProjectMemberMutation,
|
||||
useDeleteProjectMemberMutation,
|
||||
@ -44,7 +43,7 @@ import SimpleLists from 'shared/components/Lists';
|
||||
import produce from 'immer';
|
||||
import MiniProfile from 'shared/components/MiniProfile';
|
||||
import DueDateManager from 'shared/components/DueDateManager';
|
||||
import UserIDContext from 'App/context';
|
||||
import UserContext, { useCurrentUser } from 'App/context';
|
||||
import LabelManager from 'shared/components/PopupMenu/LabelManager';
|
||||
import LabelEditor from 'shared/components/PopupMenu/LabelEditor';
|
||||
import EmptyBoard from 'shared/components/EmptyBoard';
|
||||
@ -106,16 +105,48 @@ const initialQuickCardEditorState: QuickCardEditorState = {
|
||||
type ProjectBoardProps = {
|
||||
onCardLabelClick?: () => void;
|
||||
cardLabelVariant?: CardLabelVariant;
|
||||
projectID?: string;
|
||||
loading?: boolean;
|
||||
projectID: string;
|
||||
};
|
||||
|
||||
const ProjectBoard: React.FC<ProjectBoardProps> = ({
|
||||
projectID,
|
||||
onCardLabelClick,
|
||||
cardLabelVariant,
|
||||
loading: isLoading = false,
|
||||
}) => {
|
||||
export const BoardLoading = () => {
|
||||
return (
|
||||
<>
|
||||
<ProjectBar>
|
||||
<ProjectActions>
|
||||
<ProjectAction disabled>
|
||||
<CheckCircle width={13} height={13} />
|
||||
<ProjectActionText>All Tasks</ProjectActionText>
|
||||
</ProjectAction>
|
||||
<ProjectAction disabled>
|
||||
<Filter width={13} height={13} />
|
||||
<ProjectActionText>Filter</ProjectActionText>
|
||||
</ProjectAction>
|
||||
<ProjectAction disabled>
|
||||
<Sort width={13} height={13} />
|
||||
<ProjectActionText>Sort</ProjectActionText>
|
||||
</ProjectAction>
|
||||
</ProjectActions>
|
||||
<ProjectActions>
|
||||
<ProjectAction>
|
||||
<Tags width={13} height={13} />
|
||||
<ProjectActionText>Labels</ProjectActionText>
|
||||
</ProjectAction>
|
||||
<ProjectAction disabled>
|
||||
<ToggleOn width={13} height={13} />
|
||||
<ProjectActionText>Fields</ProjectActionText>
|
||||
</ProjectAction>
|
||||
<ProjectAction disabled>
|
||||
<Bolt width={13} height={13} />
|
||||
<ProjectActionText>Rules</ProjectActionText>
|
||||
</ProjectAction>
|
||||
</ProjectActions>
|
||||
</ProjectBar>
|
||||
<EmptyBoard />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick, cardLabelVariant }) => {
|
||||
const [assignTask] = useAssignTaskMutation();
|
||||
const [unassignTask] = useUnassignTaskMutation();
|
||||
const $labelsRef = useRef<HTMLDivElement>(null);
|
||||
@ -124,7 +155,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({
|
||||
const { showPopup, hidePopup } = usePopup();
|
||||
const taskLabelsRef = useRef<Array<TaskLabel>>([]);
|
||||
const [quickCardEditor, setQuickCardEditor] = useState(initialQuickCardEditorState);
|
||||
const { userID } = useContext(UserIDContext);
|
||||
const { user } = useCurrentUser();
|
||||
const [updateTaskGroupLocation] = useUpdateTaskGroupLocationMutation({});
|
||||
const history = useHistory();
|
||||
const [deleteTaskGroup] = useDeleteTaskGroupMutation({
|
||||
@ -138,7 +169,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({
|
||||
(taskGroup: TaskGroup) => taskGroup.id !== deletedTaskGroupData.data.deleteTaskGroup.taskGroup.id,
|
||||
);
|
||||
}),
|
||||
{ projectId: projectID },
|
||||
{ projectID },
|
||||
);
|
||||
},
|
||||
});
|
||||
@ -156,7 +187,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({
|
||||
draftCache.findProject.taskGroups[idx].tasks.push({ ...newTaskData.data.createTask });
|
||||
}
|
||||
}),
|
||||
{ projectId: projectID },
|
||||
{ projectID },
|
||||
);
|
||||
},
|
||||
});
|
||||
@ -170,14 +201,14 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({
|
||||
produce(cache, draftCache => {
|
||||
draftCache.findProject.taskGroups.push({ ...newTaskGroupData.data.createTaskGroup, tasks: [] });
|
||||
}),
|
||||
{ projectId: projectID },
|
||||
{ projectID },
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const [updateTaskGroupName] = useUpdateTaskGroupNameMutation({});
|
||||
const { loading, data } = useFindProjectQuery({
|
||||
variables: { projectId: projectID ?? '' },
|
||||
variables: { projectID },
|
||||
});
|
||||
|
||||
const [updateTaskDueDate] = useUpdateTaskDueDateMutation();
|
||||
@ -205,7 +236,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({
|
||||
}
|
||||
}
|
||||
}),
|
||||
{ projectId: projectID },
|
||||
{ projectID },
|
||||
);
|
||||
},
|
||||
});
|
||||
@ -238,7 +269,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({
|
||||
__typename: 'Mutation',
|
||||
createTask: {
|
||||
__typename: 'Task',
|
||||
id: '' + Math.round(Math.random() * -1000000),
|
||||
id: `${Math.round(Math.random() * -1000000)}`,
|
||||
name,
|
||||
complete: false,
|
||||
taskGroup: {
|
||||
@ -248,6 +279,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({
|
||||
position: taskGroup.position,
|
||||
},
|
||||
badges: {
|
||||
__typename: 'TaskBadges',
|
||||
checklist: null,
|
||||
},
|
||||
position,
|
||||
@ -273,42 +305,8 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
if (loading || isLoading) {
|
||||
return (
|
||||
<>
|
||||
<ProjectBar>
|
||||
<ProjectActions>
|
||||
<ProjectAction disabled>
|
||||
<CheckCircle width={13} height={13} />
|
||||
<ProjectActionText>All Tasks</ProjectActionText>
|
||||
</ProjectAction>
|
||||
<ProjectAction disabled>
|
||||
<Filter width={13} height={13} />
|
||||
<ProjectActionText>Filter</ProjectActionText>
|
||||
</ProjectAction>
|
||||
<ProjectAction disabled>
|
||||
<Sort width={13} height={13} />
|
||||
<ProjectActionText>Sort</ProjectActionText>
|
||||
</ProjectAction>
|
||||
</ProjectActions>
|
||||
<ProjectActions>
|
||||
<ProjectAction>
|
||||
<Tags width={13} height={13} />
|
||||
<ProjectActionText>Labels</ProjectActionText>
|
||||
</ProjectAction>
|
||||
<ProjectAction disabled>
|
||||
<ToggleOn width={13} height={13} />
|
||||
<ProjectActionText>Fields</ProjectActionText>
|
||||
</ProjectAction>
|
||||
<ProjectAction disabled>
|
||||
<Bolt width={13} height={13} />
|
||||
<ProjectActionText>Rules</ProjectActionText>
|
||||
</ProjectAction>
|
||||
</ProjectActions>
|
||||
</ProjectBar>
|
||||
<EmptyBoard />
|
||||
</>
|
||||
);
|
||||
if (loading) {
|
||||
return <BoardLoading />;
|
||||
}
|
||||
if (data) {
|
||||
labelsRef.current = data.findProject.labels;
|
||||
@ -534,7 +532,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({
|
||||
tasks: taskGroup.tasks.filter(t => t.id !== cardId),
|
||||
}));
|
||||
}),
|
||||
{ projectId: projectID },
|
||||
{ projectID },
|
||||
);
|
||||
},
|
||||
})
|
||||
|
@ -22,7 +22,7 @@ import {
|
||||
FindTaskDocument,
|
||||
FindTaskQuery,
|
||||
} from 'shared/generated/graphql';
|
||||
import UserIDContext from 'App/context';
|
||||
import UserContext, { useCurrentUser } from 'App/context';
|
||||
import MiniProfile from 'shared/components/MiniProfile';
|
||||
import DueDateManager from 'shared/components/DueDateManager';
|
||||
import produce from 'immer';
|
||||
@ -129,7 +129,7 @@ const Details: React.FC<DetailsProps> = ({
|
||||
availableMembers,
|
||||
refreshCache,
|
||||
}) => {
|
||||
const { userID } = useContext(UserIDContext);
|
||||
const { user } = useCurrentUser();
|
||||
const { showPopup, hidePopup } = usePopup();
|
||||
const history = useHistory();
|
||||
const match = useRouteMatch();
|
||||
@ -407,7 +407,9 @@ const Details: React.FC<DetailsProps> = ({
|
||||
user={member}
|
||||
bio="None"
|
||||
onRemoveFromTask={() => {
|
||||
unassignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } });
|
||||
if (user) {
|
||||
unassignTask({ variables: { taskID: data.findTask.id, userID: user.id } });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Popup>,
|
||||
@ -422,10 +424,12 @@ const Details: React.FC<DetailsProps> = ({
|
||||
availableMembers={availableMembers}
|
||||
activeMembers={data.findTask.assigned}
|
||||
onMemberChange={(member, isActive) => {
|
||||
if (isActive) {
|
||||
assignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } });
|
||||
} else {
|
||||
unassignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } });
|
||||
if (user) {
|
||||
if (isActive) {
|
||||
assignTask({ variables: { taskID: data.findTask.id, userID: user.id } });
|
||||
} else {
|
||||
unassignTask({ variables: { taskID: data.findTask.id, userID: user.id } });
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
@ -1,9 +1,8 @@
|
||||
import React, {useState} from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import updateApolloCache from 'shared/utils/cache';
|
||||
import {usePopup, Popup} from 'shared/components/PopupMenu';
|
||||
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
||||
import produce from 'immer';
|
||||
import {
|
||||
useSetProjectOwnerMutation,
|
||||
useUpdateProjectMemberRoleMutation,
|
||||
useCreateProjectMemberMutation,
|
||||
useDeleteProjectMemberMutation,
|
||||
@ -50,7 +49,7 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
|
||||
taskLabels: taskLabelsRef,
|
||||
}) => {
|
||||
const [currentLabel, setCurrentLabel] = useState('');
|
||||
const {setTab, hidePopup} = usePopup();
|
||||
const { setTab, hidePopup } = usePopup();
|
||||
const [createProjectLabel] = useCreateProjectLabelMutation({
|
||||
update: (client, newLabelData) => {
|
||||
updateApolloCache<FindProjectQuery>(
|
||||
@ -58,10 +57,10 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
|
||||
FindProjectDocument,
|
||||
cache =>
|
||||
produce(cache, draftCache => {
|
||||
draftCache.findProject.labels.push({...newLabelData.data.createProjectLabel});
|
||||
draftCache.findProject.labels.push({ ...newLabelData.data.createProjectLabel });
|
||||
}),
|
||||
{
|
||||
projectId: projectID,
|
||||
projectID,
|
||||
},
|
||||
);
|
||||
},
|
||||
@ -78,7 +77,7 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
|
||||
label => label.id !== newLabelData.data.deleteProjectLabel.id,
|
||||
);
|
||||
}),
|
||||
{projectId: projectID},
|
||||
{ projectID },
|
||||
);
|
||||
},
|
||||
});
|
||||
@ -108,7 +107,7 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
|
||||
if (newProjectLabel) {
|
||||
setCurrentTaskLabels([
|
||||
...currentTaskLabels,
|
||||
{id: '', assignedDate: '', projectLabel: {...newProjectLabel}},
|
||||
{ id: '', assignedDate: '', projectLabel: { ...newProjectLabel } },
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -127,12 +126,12 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
|
||||
label={labels.find(label => label.id === currentLabel) ?? null}
|
||||
onLabelEdit={(projectLabelID, name, color) => {
|
||||
if (projectLabelID) {
|
||||
updateProjectLabel({variables: {projectLabelID, labelColorID: color.id, name: name ?? ''}});
|
||||
updateProjectLabel({ variables: { projectLabelID, labelColorID: color.id, name: name ?? '' } });
|
||||
}
|
||||
setTab(0);
|
||||
}}
|
||||
onLabelDelete={labelID => {
|
||||
deleteProjectLabel({variables: {projectLabelID: labelID}});
|
||||
deleteProjectLabel({ variables: { projectLabelID: labelID } });
|
||||
setTab(0);
|
||||
}}
|
||||
/>
|
||||
@ -142,7 +141,7 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
|
||||
labelColors={labelColors}
|
||||
label={null}
|
||||
onLabelEdit={(_labelId, name, color) => {
|
||||
createProjectLabel({variables: {projectID, labelColorID: color.id, name: name ?? ''}});
|
||||
createProjectLabel({ variables: { projectID, labelColorID: color.id, name: name ?? '' } });
|
||||
setTab(0);
|
||||
}}
|
||||
/>
|
||||
@ -151,4 +150,4 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default LabelManagerEditor
|
||||
export default LabelManagerEditor;
|
||||
|
@ -15,7 +15,6 @@ import {
|
||||
Redirect,
|
||||
} from 'react-router-dom';
|
||||
import {
|
||||
useSetProjectOwnerMutation,
|
||||
useUpdateProjectMemberRoleMutation,
|
||||
useCreateProjectMemberMutation,
|
||||
useDeleteProjectMemberMutation,
|
||||
@ -34,10 +33,10 @@ import {
|
||||
} from 'shared/generated/graphql';
|
||||
|
||||
import produce from 'immer';
|
||||
import UserIDContext from 'App/context';
|
||||
import UserContext, { useCurrentUser } from 'App/context';
|
||||
import Input from 'shared/components/Input';
|
||||
import Member from 'shared/components/Member';
|
||||
import Board from './Board';
|
||||
import Board, { BoardLoading } from './Board';
|
||||
import Details from './Details';
|
||||
import EmptyBoard from 'shared/components/EmptyBoard';
|
||||
|
||||
@ -140,7 +139,7 @@ const Project = () => {
|
||||
const [updateTaskName] = useUpdateTaskNameMutation();
|
||||
|
||||
const { loading, data } = useFindProjectQuery({
|
||||
variables: { projectId: projectID },
|
||||
variables: { projectID },
|
||||
});
|
||||
|
||||
const [updateProjectName] = useUpdateProjectNameMutation({
|
||||
@ -152,7 +151,7 @@ const Project = () => {
|
||||
produce(cache, draftCache => {
|
||||
draftCache.findProject.name = newName.data.updateProjectName.name;
|
||||
}),
|
||||
{ projectId: projectID },
|
||||
{ projectID },
|
||||
);
|
||||
},
|
||||
});
|
||||
@ -166,11 +165,10 @@ const Project = () => {
|
||||
produce(cache, draftCache => {
|
||||
draftCache.findProject.members.push({ ...response.data.createProjectMember.member });
|
||||
}),
|
||||
{ projectId: projectID },
|
||||
{ projectID },
|
||||
);
|
||||
},
|
||||
});
|
||||
const [setProjectOwner] = useSetProjectOwnerMutation();
|
||||
const [deleteProjectMember] = useDeleteProjectMemberMutation({
|
||||
update: (client, response) => {
|
||||
updateApolloCache<FindProjectQuery>(
|
||||
@ -184,12 +182,12 @@ const Project = () => {
|
||||
m => m.id !== response.data.deleteProjectMember.member.id,
|
||||
);
|
||||
}),
|
||||
{ projectId: projectID },
|
||||
{ projectID },
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const { userID } = useContext(UserIDContext);
|
||||
const { user } = useCurrentUser();
|
||||
const location = useLocation();
|
||||
|
||||
const { showPopup, hidePopup } = usePopup();
|
||||
@ -205,7 +203,7 @@ const Project = () => {
|
||||
return (
|
||||
<>
|
||||
<GlobalTopNavbar onSaveProjectName={projectName => {}} name="" projectID={null} />
|
||||
<Board loading />
|
||||
<BoardLoading />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -221,7 +219,6 @@ const Project = () => {
|
||||
updateProjectMemberRole({ variables: { userID, roleCode, projectID } });
|
||||
}}
|
||||
onChangeProjectOwner={uid => {
|
||||
setProjectOwner({ variables: { ownerID: uid, projectID } });
|
||||
hidePopup();
|
||||
}}
|
||||
onRemoveFromBoard={userID => {
|
||||
@ -248,6 +245,7 @@ const Project = () => {
|
||||
currentTab={0}
|
||||
projectMembers={data.findProject.members}
|
||||
projectID={projectID}
|
||||
teamID={data.findProject.team.id}
|
||||
name={data.findProject.name}
|
||||
/>
|
||||
<Route path={`${match.path}`} exact render={() => <Redirect to={`${match.url}/board`} />} />
|
||||
|
@ -12,9 +12,8 @@ import {
|
||||
|
||||
import ProjectGridItem, { AddProjectItem } from 'shared/components/ProjectGridItem';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Navbar from 'App/Navbar';
|
||||
import NewProject from 'shared/components/NewProject';
|
||||
import UserIDContext from 'App/context';
|
||||
import UserContext, { PermissionLevel, PermissionObjectType, useCurrentUser } from 'App/context';
|
||||
import Button from 'shared/components/Button';
|
||||
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
||||
import { useForm } from 'react-hook-form';
|
||||
@ -227,7 +226,7 @@ const ProjectLink = styled(Link)``;
|
||||
|
||||
const Projects = () => {
|
||||
const { showPopup, hidePopup } = usePopup();
|
||||
const { loading, data } = useGetProjectsQuery();
|
||||
const { loading, data } = useGetProjectsQuery({ fetchPolicy: 'network-only' });
|
||||
useEffect(() => {
|
||||
document.title = 'Taskcafé';
|
||||
}, []);
|
||||
@ -242,7 +241,7 @@ const Projects = () => {
|
||||
});
|
||||
|
||||
const [showNewProject, setShowNewProject] = useState<ShowNewProject>({ open: false, initialTeamID: null });
|
||||
const { userID, setUserID } = useContext(UserIDContext);
|
||||
const { user, setUser } = useCurrentUser();
|
||||
const [createTeam] = useCreateTeamMutation({
|
||||
update: (client, createData) => {
|
||||
updateApolloCache<GetProjectsQuery>(client, GetProjectsDocument, cache =>
|
||||
@ -261,47 +260,63 @@ const Projects = () => {
|
||||
}
|
||||
|
||||
const colors = ['#e362e3', '#7a6ff0', '#37c5ab', '#aa62e3', '#e8384f'];
|
||||
if (data) {
|
||||
if (data && user) {
|
||||
console.log(user);
|
||||
const { projects, teams, organizations } = data;
|
||||
const organizationID = organizations[0].id ?? null;
|
||||
const projectTeams = teams.map(team => {
|
||||
return {
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
projects: projects.filter(project => project.team.id === team.id),
|
||||
};
|
||||
});
|
||||
const projectTeams = teams
|
||||
.sort((a, b) => {
|
||||
const textA = a.name.toUpperCase();
|
||||
const textB = b.name.toUpperCase();
|
||||
return textA < textB ? -1 : textA > textB ? 1 : 0;
|
||||
})
|
||||
.map(team => {
|
||||
return {
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
projects: projects
|
||||
.filter(project => project.team.id === team.id)
|
||||
.sort((a, b) => {
|
||||
const textA = a.name.toUpperCase();
|
||||
const textB = b.name.toUpperCase();
|
||||
return textA < textB ? -1 : textA > textB ? 1 : 0;
|
||||
}),
|
||||
};
|
||||
});
|
||||
console.log(projectTeams);
|
||||
return (
|
||||
<>
|
||||
<GlobalTopNavbar onSaveProjectName={() => {}} projectID={null} name={null} />
|
||||
<Wrapper>
|
||||
<ProjectsContainer>
|
||||
<AddTeamButton
|
||||
variant="outline"
|
||||
onClick={$target => {
|
||||
showPopup(
|
||||
$target,
|
||||
<Popup
|
||||
title="Create team"
|
||||
tab={0}
|
||||
onClose={() => {
|
||||
hidePopup();
|
||||
}}
|
||||
>
|
||||
<CreateTeamForm
|
||||
onCreateTeam={teamName => {
|
||||
if (organizationID) {
|
||||
createTeam({ variables: { name: teamName, organizationID } });
|
||||
hidePopup();
|
||||
}
|
||||
{user.roles.org === 'admin' && (
|
||||
<AddTeamButton
|
||||
variant="outline"
|
||||
onClick={$target => {
|
||||
showPopup(
|
||||
$target,
|
||||
<Popup
|
||||
title="Create team"
|
||||
tab={0}
|
||||
onClose={() => {
|
||||
hidePopup();
|
||||
}}
|
||||
/>
|
||||
</Popup>,
|
||||
);
|
||||
}}
|
||||
>
|
||||
Add Team
|
||||
</AddTeamButton>
|
||||
>
|
||||
<CreateTeamForm
|
||||
onCreateTeam={teamName => {
|
||||
if (organizationID) {
|
||||
createTeam({ variables: { name: teamName, organizationID } });
|
||||
hidePopup();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Popup>,
|
||||
);
|
||||
}}
|
||||
>
|
||||
Add Team
|
||||
</AddTeamButton>
|
||||
)}
|
||||
{projectTeams.length === 0 && (
|
||||
<EmptyStateContent>
|
||||
<EmptyState width={425} height={425} />
|
||||
@ -340,17 +355,19 @@ const Projects = () => {
|
||||
<div key={team.id}>
|
||||
<ProjectSectionTitleWrapper>
|
||||
<ProjectSectionTitle>{team.name}</ProjectSectionTitle>
|
||||
<SectionActions>
|
||||
<SectionActionLink to={`/teams/${team.id}`}>
|
||||
<SectionAction variant="outline">Projects</SectionAction>
|
||||
</SectionActionLink>
|
||||
<SectionActionLink to={`/teams/${team.id}/members`}>
|
||||
<SectionAction variant="outline">Members</SectionAction>
|
||||
</SectionActionLink>
|
||||
<SectionActionLink to={`/teams/${team.id}/settings`}>
|
||||
<SectionAction variant="outline">Settings</SectionAction>
|
||||
</SectionActionLink>
|
||||
</SectionActions>
|
||||
{user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, team.id) && (
|
||||
<SectionActions>
|
||||
<SectionActionLink to={`/teams/${team.id}`}>
|
||||
<SectionAction variant="outline">Projects</SectionAction>
|
||||
</SectionActionLink>
|
||||
<SectionActionLink to={`/teams/${team.id}/members`}>
|
||||
<SectionAction variant="outline">Members</SectionAction>
|
||||
</SectionActionLink>
|
||||
<SectionActionLink to={`/teams/${team.id}/settings`}>
|
||||
<SectionAction variant="outline">Settings</SectionAction>
|
||||
</SectionActionLink>
|
||||
</SectionActions>
|
||||
)}
|
||||
</ProjectSectionTitleWrapper>
|
||||
<ProjectList>
|
||||
{team.projects.map((project, idx) => (
|
||||
@ -363,18 +380,20 @@ const Projects = () => {
|
||||
</ProjectTile>
|
||||
</ProjectListItem>
|
||||
))}
|
||||
<ProjectListItem>
|
||||
<ProjectAddTile
|
||||
onClick={() => {
|
||||
setShowNewProject({ open: true, initialTeamID: team.id });
|
||||
}}
|
||||
>
|
||||
<ProjectTileFade />
|
||||
<ProjectAddTileDetails>
|
||||
<ProjectTileName centered>Create new project</ProjectTileName>
|
||||
</ProjectAddTileDetails>
|
||||
</ProjectAddTile>
|
||||
</ProjectListItem>
|
||||
{user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, team.id) && (
|
||||
<ProjectListItem>
|
||||
<ProjectAddTile
|
||||
onClick={() => {
|
||||
setShowNewProject({ open: true, initialTeamID: team.id });
|
||||
}}
|
||||
>
|
||||
<ProjectTileFade />
|
||||
<ProjectAddTileDetails>
|
||||
<ProjectTileName centered>Create new project</ProjectTileName>
|
||||
</ProjectAddTileDetails>
|
||||
</ProjectAddTile>
|
||||
</ProjectListItem>
|
||||
)}
|
||||
</ProjectList>
|
||||
</div>
|
||||
);
|
||||
@ -383,8 +402,8 @@ const Projects = () => {
|
||||
<NewProject
|
||||
initialTeamID={showNewProject.initialTeamID}
|
||||
onCreateProject={(name, teamID) => {
|
||||
if (userID) {
|
||||
createProject({ variables: { teamID, name, userID } });
|
||||
if (user) {
|
||||
createProject({ variables: { teamID, name, userID: user.id } });
|
||||
setShowNewProject({ open: false, initialTeamID: null });
|
||||
}
|
||||
}}
|
||||
|
@ -3,15 +3,18 @@ import Input from 'shared/components/Input';
|
||||
import updateApolloCache from 'shared/utils/cache';
|
||||
import produce from 'immer';
|
||||
import Button from 'shared/components/Button';
|
||||
import UserIDContext from 'App/context';
|
||||
import UserContext, { useCurrentUser, PermissionLevel, PermissionObjectType } from 'App/context';
|
||||
import Select from 'shared/components/Select';
|
||||
import {
|
||||
useGetTeamQuery,
|
||||
RoleCode,
|
||||
useCreateTeamMemberMutation,
|
||||
useDeleteTeamMemberMutation,
|
||||
useUpdateTeamMemberRoleMutation,
|
||||
GetTeamQuery,
|
||||
GetTeamDocument,
|
||||
MeDocument,
|
||||
MeQuery,
|
||||
} from 'shared/generated/graphql';
|
||||
import { UserPlus, Checkmark } from 'shared/icons';
|
||||
import styled, { css } from 'styled-components/macro';
|
||||
@ -165,7 +168,6 @@ type TeamRoleManagerPopupProps = {
|
||||
canChangeRole: boolean;
|
||||
onChangeRole: (roleCode: RoleCode) => void;
|
||||
onRemoveFromTeam?: (newOwnerID: string | null) => void;
|
||||
onChangeTeamOwner?: (userID: string) => void;
|
||||
};
|
||||
|
||||
const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
|
||||
@ -175,7 +177,6 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
|
||||
currentUserID,
|
||||
canChangeRole,
|
||||
onRemoveFromTeam,
|
||||
onChangeTeamOwner,
|
||||
onChangeRole,
|
||||
}) => {
|
||||
const { hidePopup, setTab } = usePopup();
|
||||
@ -185,15 +186,6 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
|
||||
<Popup title={null} tab={0}>
|
||||
<MiniProfileActions>
|
||||
<MiniProfileActionWrapper>
|
||||
{onChangeTeamOwner && (
|
||||
<MiniProfileActionItem
|
||||
onClick={() => {
|
||||
setTab(3);
|
||||
}}
|
||||
>
|
||||
Set as team owner...
|
||||
</MiniProfileActionItem>
|
||||
)}
|
||||
{subject.role && (
|
||||
<MiniProfileActionItem
|
||||
onClick={() => {
|
||||
@ -298,24 +290,6 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
|
||||
</RemoveMemberButton>
|
||||
</Content>
|
||||
</Popup>
|
||||
<Popup title="Set as Team Owner?" onClose={() => hidePopup()} tab={3}>
|
||||
<Content>
|
||||
<DeleteDescription>
|
||||
This will change the project owner from you to this subject. They will be able to view and edit cards,
|
||||
remove members, and change all settings for the project. They will also be able to delete the project.
|
||||
</DeleteDescription>
|
||||
<RemoveMemberButton
|
||||
color="warning"
|
||||
onClick={() => {
|
||||
if (onChangeTeamOwner) {
|
||||
onChangeTeamOwner(subject.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Set as Project Owner
|
||||
</RemoveMemberButton>
|
||||
</Content>
|
||||
</Popup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -444,7 +418,7 @@ type MembersProps = {
|
||||
const Members: React.FC<MembersProps> = ({ teamID }) => {
|
||||
const { showPopup, hidePopup } = usePopup();
|
||||
const { loading, data } = useGetTeamQuery({ variables: { teamID } });
|
||||
const { userID } = useContext(UserIDContext);
|
||||
const { user, setUserRoles } = useCurrentUser();
|
||||
const warning =
|
||||
'You can’t leave because you are the only admin. To make another user an admin, click their avatar, select “Change permissions…”, and select “Admin”.';
|
||||
const [createTeamMember] = useCreateTeamMemberMutation({
|
||||
@ -454,12 +428,27 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
|
||||
GetTeamDocument,
|
||||
cache =>
|
||||
produce(cache, draftCache => {
|
||||
draftCache.findTeam.members.push({ ...response.data.createTeamMember.teamMember });
|
||||
draftCache.findTeam.members.push({
|
||||
...response.data.createTeamMember.teamMember,
|
||||
member: { __typename: 'MemberList', projects: [], teams: [] },
|
||||
owned: { __typename: 'OwnedList', projects: [], teams: [] },
|
||||
});
|
||||
}),
|
||||
{ teamID },
|
||||
);
|
||||
},
|
||||
});
|
||||
const [updateTeamMemberRole] = useUpdateTeamMemberRoleMutation({
|
||||
onCompleted: r => {
|
||||
if (user) {
|
||||
setUserRoles(
|
||||
produce(user.roles, draftRoles => {
|
||||
draftRoles.teams.set(r.updateTeamMemberRole.teamID, r.updateTeamMemberRole.member.role.code);
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
const [deleteTeamMember] = useDeleteTeamMemberMutation({
|
||||
update: (client, response) => {
|
||||
updateApolloCache<GetTeamQuery>(
|
||||
@ -479,7 +468,7 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
|
||||
return <span>loading</span>;
|
||||
}
|
||||
|
||||
if (data) {
|
||||
if (data && user) {
|
||||
return (
|
||||
<MemberContainer>
|
||||
<FilterTab>
|
||||
@ -497,23 +486,26 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
|
||||
</ListDesc>
|
||||
<ListActions>
|
||||
<FilterSearch width="250px" variant="alternate" placeholder="Filter by name" />
|
||||
<InviteMemberButton
|
||||
onClick={$target => {
|
||||
showPopup(
|
||||
$target,
|
||||
<UserManagementPopup
|
||||
users={data.users}
|
||||
teamMembers={data.findTeam.members}
|
||||
onAddTeamMember={userID => {
|
||||
createTeamMember({ variables: { userID, teamID } });
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<InviteIcon width={16} height={16} />
|
||||
Invite Team Members
|
||||
</InviteMemberButton>
|
||||
{user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, data.findTeam.id) && (
|
||||
<InviteMemberButton
|
||||
onClick={$target => {
|
||||
showPopup(
|
||||
$target,
|
||||
<UserManagementPopup
|
||||
users={data.users}
|
||||
teamMembers={data.findTeam.members}
|
||||
onAddTeamMember={userID => {
|
||||
console.log(`team: ${userID}`);
|
||||
createTeamMember({ variables: { userID, teamID } });
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<InviteIcon width={16} height={16} />
|
||||
Invite Team Members
|
||||
</InviteMemberButton>
|
||||
)}
|
||||
</ListActions>
|
||||
</MemberListHeader>
|
||||
<MemberList>
|
||||
@ -532,15 +524,14 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
|
||||
showPopup(
|
||||
$target,
|
||||
<TeamRoleManagerPopup
|
||||
currentUserID={userID ?? ''}
|
||||
currentUserID={user.id ?? ''}
|
||||
subject={member}
|
||||
members={data.findTeam.members}
|
||||
warning={member.role && member.role.code === 'owner' ? warning : null}
|
||||
onChangeTeamOwner={
|
||||
member.role && member.role.code !== 'owner' ? (userID: string) => {} : undefined
|
||||
}
|
||||
canChangeRole={member.role && member.role.code !== 'owner'}
|
||||
onChangeRole={roleCode => {}}
|
||||
canChangeRole={user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID)}
|
||||
onChangeRole={roleCode => {
|
||||
updateTeamMemberRole({ variables: { userID: member.id, teamID, roleCode } });
|
||||
}}
|
||||
onRemoveFromTeam={
|
||||
member.role && member.role.code === 'owner'
|
||||
? undefined
|
||||
|
@ -3,7 +3,7 @@ import styled, { css } from 'styled-components/macro';
|
||||
import { MENU_TYPES } from 'shared/components/TopNavbar';
|
||||
import GlobalTopNavbar from 'App/TopNavbar';
|
||||
import updateApolloCache from 'shared/utils/cache';
|
||||
import { Route, Switch, useRouteMatch } from 'react-router';
|
||||
import { Route, Switch, useRouteMatch, Redirect } from 'react-router';
|
||||
import Members from './Members';
|
||||
import Projects from './Projects';
|
||||
|
||||
@ -18,6 +18,7 @@ import { usePopup, Popup } from 'shared/components/PopupMenu';
|
||||
import { History } from 'history';
|
||||
import produce from 'immer';
|
||||
import { TeamSettings, DeleteConfirm, DELETE_INFO } from 'shared/components/ProjectSettings';
|
||||
import UserContext, { PermissionObjectType, PermissionLevel, useCurrentUser } from 'App/context';
|
||||
|
||||
const OuterWrapper = styled.div`
|
||||
display: flex;
|
||||
@ -87,6 +88,7 @@ const Teams = () => {
|
||||
const { teamID } = useParams<TeamsRouteProps>();
|
||||
const history = useHistory();
|
||||
const { loading, data } = useGetTeamQuery({ variables: { teamID } });
|
||||
const { user } = useCurrentUser();
|
||||
const [currentTab, setCurrentTab] = useState(0);
|
||||
const match = useRouteMatch();
|
||||
useEffect(() => {
|
||||
@ -99,7 +101,10 @@ const Teams = () => {
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (data) {
|
||||
if (data && user) {
|
||||
if (!user.isVisible(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID)) {
|
||||
return <Redirect to="/" />;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<GlobalTopNavbar
|
||||
|
@ -4,14 +4,16 @@ import axios from 'axios';
|
||||
import createAuthRefreshInterceptor from 'axios-auth-refresh';
|
||||
import { ApolloProvider } from '@apollo/react-hooks';
|
||||
import { ApolloClient } from 'apollo-client';
|
||||
import { InMemoryCache } from 'apollo-cache-inmemory';
|
||||
import { HttpLink } from 'apollo-link-http';
|
||||
import { onError } from 'apollo-link-error';
|
||||
import { enableMapSet } from 'immer';
|
||||
import { ApolloLink, Observable, fromPromise } from 'apollo-link';
|
||||
import { getAccessToken, getNewToken, setAccessToken } from 'shared/utils/accessToken';
|
||||
import cache from './App/cache';
|
||||
import App from './App';
|
||||
|
||||
// https://able.bio/AnasT/apollo-graphql-async-access-token-refresh--470t1c8
|
||||
enableMapSet();
|
||||
|
||||
let forward$;
|
||||
let isRefreshing = false;
|
||||
@ -135,7 +137,7 @@ const client = new ApolloClient({
|
||||
credentials: 'same-origin',
|
||||
}),
|
||||
]),
|
||||
cache: new InMemoryCache(),
|
||||
cache,
|
||||
});
|
||||
|
||||
ReactDOM.render(
|
||||
|
@ -25,6 +25,7 @@ export const Default = () => {
|
||||
<ThemeProvider theme={theme}>
|
||||
<Admin
|
||||
onInviteUser={action('invite user')}
|
||||
canInviteUser
|
||||
initialTab={1}
|
||||
onUpdateUserPassword={action('update user password')}
|
||||
onDeleteUser={action('delete user')}
|
||||
|
@ -55,7 +55,7 @@ export const MiniProfileActionItem = styled.span<{ disabled?: boolean }>`
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
|
||||
${(props) =>
|
||||
${props =>
|
||||
props.disabled
|
||||
? css`
|
||||
user-select: none;
|
||||
@ -75,7 +75,7 @@ export const Content = styled.div`
|
||||
|
||||
export const CurrentPermission = styled.span`
|
||||
margin-left: 4px;
|
||||
color: rgba(${(props) => props.theme.colors.text.secondary}, 0.4);
|
||||
color: rgba(${props => props.theme.colors.text.secondary}, 0.4);
|
||||
`;
|
||||
|
||||
export const Separator = styled.div`
|
||||
@ -86,13 +86,13 @@ export const Separator = styled.div`
|
||||
|
||||
export const WarningText = styled.span`
|
||||
display: flex;
|
||||
color: rgba(${(props) => props.theme.colors.text.primary}, 0.4);
|
||||
color: rgba(${props => props.theme.colors.text.primary}, 0.4);
|
||||
padding: 6px;
|
||||
`;
|
||||
|
||||
export const DeleteDescription = styled.div`
|
||||
font-size: 14px;
|
||||
color: rgba(${(props) => props.theme.colors.text.primary});
|
||||
color: rgba(${props => props.theme.colors.text.primary});
|
||||
`;
|
||||
|
||||
export const RemoveMemberButton = styled(Button)`
|
||||
@ -159,8 +159,8 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
|
||||
<MiniProfileActions>
|
||||
<MiniProfileActionWrapper>
|
||||
{permissions
|
||||
.filter((p) => (user.role && user.role.code === 'owner') || p.code !== 'owner')
|
||||
.map((perm) => (
|
||||
.filter(p => (user.role && user.role.code === 'owner') || p.code !== 'owner')
|
||||
.map(perm => (
|
||||
<MiniProfileActionItem
|
||||
disabled={user.role && perm.code !== user.role.code && !canChangeRole}
|
||||
key={perm.code}
|
||||
@ -211,9 +211,9 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
|
||||
Choose a new user to take over ownership of this user's teams & projects.
|
||||
</DeleteDescription>
|
||||
<UserSelect
|
||||
onChange={(v) => setDeleteUser(v)}
|
||||
onChange={v => setDeleteUser(v)}
|
||||
value={deleteUser}
|
||||
options={users.map((u) => ({ label: u.fullName, value: u.id }))}
|
||||
options={users.map(u => ({ label: u.fullName, value: u.id }))}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@ -239,11 +239,7 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
|
||||
Removing this user from the organzation will remove them from assigned tasks, projects, and teams.
|
||||
</DeleteDescription>
|
||||
<DeleteDescription>{`The user is the owner of ${user.owned.projects.length} projects & ${user.owned.teams.length} teams.`}</DeleteDescription>
|
||||
<UserSelect
|
||||
onChange={() => {}}
|
||||
value={null}
|
||||
options={users.map((u) => ({ label: u.fullName, value: u.id }))}
|
||||
/>
|
||||
<UserSelect onChange={() => {}} value={null} options={users.map(u => ({ label: u.fullName, value: u.id }))} />
|
||||
<UserPassConfirmButton
|
||||
onClick={() => {
|
||||
// onDeleteUser();
|
||||
@ -334,14 +330,14 @@ const MemberItemOption = styled(Button)`
|
||||
`;
|
||||
|
||||
const MemberList = styled.div`
|
||||
border-top: 1px solid rgba(${(props) => props.theme.colors.border});
|
||||
border-top: 1px solid rgba(${props => props.theme.colors.border});
|
||||
`;
|
||||
|
||||
const MemberListItem = styled.div`
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid rgba(${(props) => props.theme.colors.border});
|
||||
border-bottom: 1px solid rgba(${props => props.theme.colors.border});
|
||||
min-height: 40px;
|
||||
padding: 12px 0 12px 40px;
|
||||
position: relative;
|
||||
@ -365,11 +361,11 @@ const MemberProfile = styled(TaskAssignee)`
|
||||
`;
|
||||
|
||||
const MemberItemName = styled.p`
|
||||
color: rgba(${(props) => props.theme.colors.text.secondary});
|
||||
color: rgba(${props => props.theme.colors.text.secondary});
|
||||
`;
|
||||
|
||||
const MemberItemUsername = styled.p`
|
||||
color: rgba(${(props) => props.theme.colors.text.primary});
|
||||
color: rgba(${props => props.theme.colors.text.primary});
|
||||
`;
|
||||
|
||||
const MemberListHeader = styled.div`
|
||||
@ -378,12 +374,12 @@ const MemberListHeader = styled.div`
|
||||
`;
|
||||
const ListTitle = styled.h3`
|
||||
font-size: 18px;
|
||||
color: rgba(${(props) => props.theme.colors.text.secondary});
|
||||
color: rgba(${props => props.theme.colors.text.secondary});
|
||||
margin-bottom: 12px;
|
||||
`;
|
||||
const ListDesc = styled.span`
|
||||
font-size: 16px;
|
||||
color: rgba(${(props) => props.theme.colors.text.primary});
|
||||
color: rgba(${props => props.theme.colors.text.primary});
|
||||
`;
|
||||
const FilterSearch = styled(Input)`
|
||||
margin: 0;
|
||||
@ -484,7 +480,7 @@ const ActionButtons = (params: any) => {
|
||||
<ActionButton onClick={() => {}}>
|
||||
<EditUserIcon width={16} height={16} />
|
||||
</ActionButton>
|
||||
<ActionButton onClick={($target) => params.onDeleteUser($target, params.value)}>
|
||||
<ActionButton onClick={$target => params.onDeleteUser($target, params.value)}>
|
||||
<DeleteUserIcon width={16} height={16} />
|
||||
</ActionButton>
|
||||
</>
|
||||
@ -541,7 +537,7 @@ const TabNavItemButton = styled.button<{ active: boolean }>`
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
color: ${(props) => (props.active ? 'rgba(115, 103, 240)' : '#c2c6dc')};
|
||||
color: ${props => (props.active ? 'rgba(115, 103, 240)' : '#c2c6dc')};
|
||||
&:hover {
|
||||
color: rgba(115, 103, 240);
|
||||
}
|
||||
@ -562,7 +558,7 @@ const TabNavLine = styled.span<{ top: number }>`
|
||||
width: 2px;
|
||||
height: 48px;
|
||||
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));
|
||||
box-shadow: 0 0 8px 0 rgba(115, 103, 240);
|
||||
@ -624,6 +620,7 @@ type AdminProps = {
|
||||
onDeleteUser: (userID: string, newOwnerID: string | null) => void;
|
||||
onInviteUser: ($target: React.RefObject<HTMLElement>) => void;
|
||||
users: Array<User>;
|
||||
canInviteUser: boolean;
|
||||
onUpdateUserPassword: (user: TaskUser, password: string) => void;
|
||||
};
|
||||
|
||||
@ -631,6 +628,7 @@ const Admin: React.FC<AdminProps> = ({
|
||||
initialTab,
|
||||
onAddUser,
|
||||
onUpdateUserPassword,
|
||||
canInviteUser,
|
||||
onDeleteUser,
|
||||
onInviteUser,
|
||||
users,
|
||||
@ -675,18 +673,20 @@ const Admin: React.FC<AdminProps> = ({
|
||||
</ListDesc>
|
||||
<ListActions>
|
||||
<FilterSearch width="250px" variant="alternate" placeholder="Filter by name" />
|
||||
<InviteMemberButton
|
||||
onClick={($target) => {
|
||||
onAddUser($target);
|
||||
}}
|
||||
>
|
||||
<InviteIcon width={16} height={16} />
|
||||
New Member
|
||||
</InviteMemberButton>
|
||||
{canInviteUser && (
|
||||
<InviteMemberButton
|
||||
onClick={$target => {
|
||||
onAddUser($target);
|
||||
}}
|
||||
>
|
||||
<InviteIcon width={16} height={16} />
|
||||
New Member
|
||||
</InviteMemberButton>
|
||||
)}
|
||||
</ListActions>
|
||||
</MemberListHeader>
|
||||
<MemberList>
|
||||
{users.map((member) => {
|
||||
{users.map(member => {
|
||||
const projectTotal = member.owned.projects.length + member.member.projects.length;
|
||||
return (
|
||||
<MemberListItem>
|
||||
@ -699,7 +699,7 @@ const Admin: React.FC<AdminProps> = ({
|
||||
<MemberItemOption variant="flat">{`On ${projectTotal} projects`}</MemberItemOption>
|
||||
<MemberItemOption
|
||||
variant="outline"
|
||||
onClick={($target) => {
|
||||
onClick={$target => {
|
||||
showPopup(
|
||||
$target,
|
||||
<TeamRoleManagerPopup
|
||||
@ -710,7 +710,7 @@ const Admin: React.FC<AdminProps> = ({
|
||||
onUpdateUserPassword(user, password);
|
||||
}}
|
||||
canChangeRole={(member.role && member.role.code !== 'owner') ?? false}
|
||||
onChangeRole={(roleCode) => {
|
||||
onChangeRole={roleCode => {
|
||||
updateUserRole({ variables: { userID: member.id, roleCode } });
|
||||
}}
|
||||
onDeleteUser={onDeleteUser}
|
||||
|
@ -37,17 +37,22 @@ const DropdownMenu: React.FC<DropdownMenuProps> = ({ left, top, onLogout, onClos
|
||||
type ProfileMenuProps = {
|
||||
onProfile: () => void;
|
||||
onLogout: () => void;
|
||||
showAdminConsole: boolean;
|
||||
onAdminConsole: () => void;
|
||||
};
|
||||
|
||||
const ProfileMenu: React.FC<ProfileMenuProps> = ({ onAdminConsole, onProfile, onLogout }) => {
|
||||
const ProfileMenu: React.FC<ProfileMenuProps> = ({ showAdminConsole, onAdminConsole, onProfile, onLogout }) => {
|
||||
return (
|
||||
<>
|
||||
<ActionItem onClick={onAdminConsole}>
|
||||
<Cog size={16} color="#c2c6dc" />
|
||||
<ActionTitle>Admin Console</ActionTitle>
|
||||
</ActionItem>
|
||||
<Separator />
|
||||
{showAdminConsole && (
|
||||
<>
|
||||
<ActionItem onClick={onAdminConsole}>
|
||||
<Cog size={16} color="#c2c6dc" />
|
||||
<ActionTitle>Admin Console</ActionTitle>
|
||||
</ActionItem>
|
||||
<Separator />
|
||||
</>
|
||||
)}
|
||||
<ActionItem onClick={onProfile}>
|
||||
<User size={16} color="#c2c6dc" />
|
||||
<ActionTitle>Profile</ActionTitle>
|
||||
|
@ -50,7 +50,6 @@ type MiniProfileProps = {
|
||||
onRemoveFromTask?: () => void;
|
||||
onChangeRole?: (roleCode: RoleCode) => void;
|
||||
onRemoveFromBoard?: () => void;
|
||||
onChangeProjectOwner?: (userID: string) => void;
|
||||
warning?: string | null;
|
||||
canChangeRole?: boolean;
|
||||
};
|
||||
@ -58,7 +57,6 @@ const MiniProfile: React.FC<MiniProfileProps> = ({
|
||||
user,
|
||||
bio,
|
||||
canChangeRole,
|
||||
onChangeProjectOwner,
|
||||
onRemoveFromTask,
|
||||
onChangeRole,
|
||||
onRemoveFromBoard,
|
||||
@ -91,15 +89,6 @@ const MiniProfile: React.FC<MiniProfileProps> = ({
|
||||
Remove from card
|
||||
</MiniProfileActionItem>
|
||||
)}
|
||||
{onChangeProjectOwner && (
|
||||
<MiniProfileActionItem
|
||||
onClick={() => {
|
||||
setTab(3);
|
||||
}}
|
||||
>
|
||||
Set as project owner
|
||||
</MiniProfileActionItem>
|
||||
)}
|
||||
{onChangeRole && user.role && (
|
||||
<MiniProfileActionItem
|
||||
onClick={() => {
|
||||
@ -193,24 +182,6 @@ const MiniProfile: React.FC<MiniProfileProps> = ({
|
||||
</RemoveMemberButton>
|
||||
</Content>
|
||||
</Popup>
|
||||
<Popup title="Set as Project Owner?" onClose={() => hidePopup()} tab={3}>
|
||||
<Content>
|
||||
<DeleteDescription>
|
||||
This will change the project owner from you to this user. They will be able to view and edit cards, remove
|
||||
members, and change all settings for the project. They will also be able to delete the project.
|
||||
</DeleteDescription>
|
||||
<RemoveMemberButton
|
||||
color="warning"
|
||||
onClick={() => {
|
||||
if (onChangeProjectOwner) {
|
||||
onChangeProjectOwner(user.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Set as Project Owner
|
||||
</RemoveMemberButton>
|
||||
</Content>
|
||||
</Popup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -38,6 +38,7 @@ const HomeDashboard = styled(Home)``;
|
||||
type ProjectHeadingProps = {
|
||||
onFavorite?: () => void;
|
||||
name: string;
|
||||
canEditProjectName: boolean;
|
||||
onSaveProjectName?: (projectName: string) => void;
|
||||
onOpenSettings: ($target: React.RefObject<HTMLElement>) => void;
|
||||
};
|
||||
@ -46,6 +47,7 @@ const ProjectHeading: React.FC<ProjectHeadingProps> = ({
|
||||
onFavorite,
|
||||
name: initialProjectName,
|
||||
onSaveProjectName,
|
||||
canEditProjectName,
|
||||
onOpenSettings,
|
||||
}) => {
|
||||
const [isEditProjectName, setEditProjectName] = useState(false);
|
||||
@ -94,7 +96,9 @@ const ProjectHeading: React.FC<ProjectHeadingProps> = ({
|
||||
) : (
|
||||
<ProjectName
|
||||
onClick={() => {
|
||||
setEditProjectName(true);
|
||||
if (canEditProjectName) {
|
||||
setEditProjectName(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{projectName}
|
||||
@ -142,19 +146,25 @@ type NavBarProps = {
|
||||
onProfileClick: ($target: React.RefObject<HTMLElement>) => void;
|
||||
onSaveName?: (name: string) => void;
|
||||
onNotificationClick: () => void;
|
||||
canEditProjectName?: boolean;
|
||||
canInviteUser?: boolean;
|
||||
onInviteUser?: ($target: React.RefObject<HTMLElement>) => void;
|
||||
onDashboardClick: () => void;
|
||||
user: TaskUser | null;
|
||||
onOpenSettings: ($target: React.RefObject<HTMLElement>) => void;
|
||||
projectMembers?: Array<TaskUser> | null;
|
||||
onRemoveFromBoard?: (userID: string) => void;
|
||||
onMemberProfile?: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
|
||||
};
|
||||
|
||||
const NavBar: React.FC<NavBarProps> = ({
|
||||
menuType,
|
||||
canInviteUser = false,
|
||||
onInviteUser,
|
||||
onChangeProjectOwner,
|
||||
currentTab,
|
||||
onMemberProfile,
|
||||
canEditProjectName = false,
|
||||
onOpenProjectFinder,
|
||||
onFavorite,
|
||||
onSetTab,
|
||||
@ -175,47 +185,6 @@ const NavBar: React.FC<NavBarProps> = ({
|
||||
}
|
||||
};
|
||||
const { showPopup } = usePopup();
|
||||
const onMemberProfile = ($targetRef: React.RefObject<HTMLElement>, memberID: string) => {
|
||||
const member = projectMembers ? projectMembers.find(u => u.id === memberID) : null;
|
||||
const warning =
|
||||
'You can’t leave because you are the only admin. To make another user an admin, click their avatar, select “Change permissions…”, and select “Admin”.';
|
||||
if (member) {
|
||||
console.log(member);
|
||||
showPopup(
|
||||
$targetRef,
|
||||
<MiniProfile
|
||||
warning={member.role && member.role.code === 'owner' ? warning : null}
|
||||
onChangeProjectOwner={
|
||||
member.role && member.role.code !== 'owner'
|
||||
? (userID: string) => {
|
||||
if (user && onChangeProjectOwner) {
|
||||
onChangeProjectOwner(userID);
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
canChangeRole={member.role && member.role.code !== 'owner'}
|
||||
onChangeRole={roleCode => {
|
||||
if (onChangeRole) {
|
||||
onChangeRole(member.id, roleCode);
|
||||
}
|
||||
}}
|
||||
onRemoveFromBoard={
|
||||
member.role && member.role.code === 'owner'
|
||||
? undefined
|
||||
: () => {
|
||||
if (onRemoveFromBoard) {
|
||||
onRemoveFromBoard(member.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
user={member}
|
||||
bio=""
|
||||
/>,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<NavbarWrapper>
|
||||
<NavbarHeader>
|
||||
@ -226,6 +195,7 @@ const NavBar: React.FC<NavBarProps> = ({
|
||||
onFavorite={onFavorite}
|
||||
onOpenSettings={onOpenSettings}
|
||||
name={name}
|
||||
canEditProjectName={canEditProjectName}
|
||||
onSaveProjectName={onSaveName}
|
||||
/>
|
||||
)}
|
||||
@ -255,7 +225,7 @@ const NavBar: React.FC<NavBarProps> = ({
|
||||
<TaskcafeTitle>Taskcafé</TaskcafeTitle>
|
||||
</LogoContainer>
|
||||
<GlobalActions>
|
||||
{projectMembers && (
|
||||
{projectMembers && onMemberProfile && (
|
||||
<>
|
||||
<ProjectMembers>
|
||||
{projectMembers.map((member, idx) => (
|
||||
@ -268,16 +238,18 @@ const NavBar: React.FC<NavBarProps> = ({
|
||||
onMemberProfile={onMemberProfile}
|
||||
/>
|
||||
))}
|
||||
<InviteButton
|
||||
onClick={$target => {
|
||||
if (onInviteUser) {
|
||||
onInviteUser($target);
|
||||
}
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
Invite
|
||||
</InviteButton>
|
||||
{canInviteUser && (
|
||||
<InviteButton
|
||||
onClick={$target => {
|
||||
if (onInviteUser) {
|
||||
onInviteUser($target);
|
||||
}
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
Invite
|
||||
</InviteButton>
|
||||
)}
|
||||
</ProjectMembers>
|
||||
<NavSeparator />
|
||||
</>
|
||||
|
@ -17,6 +17,7 @@ export type Scalars = {
|
||||
|
||||
|
||||
|
||||
|
||||
export enum RoleCode {
|
||||
Owner = 'owner',
|
||||
Admin = 'admin',
|
||||
@ -125,7 +126,6 @@ export type Project = {
|
||||
createdAt: Scalars['Time'];
|
||||
name: Scalars['String'];
|
||||
team: Team;
|
||||
owner: Member;
|
||||
taskGroups: Array<TaskGroup>;
|
||||
members: Array<Member>;
|
||||
labels: Array<ProjectLabel>;
|
||||
@ -192,6 +192,24 @@ export type TaskChecklist = {
|
||||
items: Array<TaskChecklistItem>;
|
||||
};
|
||||
|
||||
export enum RoleLevel {
|
||||
Admin = 'ADMIN',
|
||||
Member = 'MEMBER'
|
||||
}
|
||||
|
||||
export enum ActionLevel {
|
||||
Org = 'ORG',
|
||||
Team = 'TEAM',
|
||||
Project = 'PROJECT'
|
||||
}
|
||||
|
||||
export enum ObjectType {
|
||||
Org = 'ORG',
|
||||
Team = 'TEAM',
|
||||
Project = 'PROJECT',
|
||||
Task = 'TASK'
|
||||
}
|
||||
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
organizations: Array<Organization>;
|
||||
@ -204,7 +222,7 @@ export type Query = {
|
||||
teams: Array<Team>;
|
||||
labelColors: Array<LabelColor>;
|
||||
taskGroups: Array<TaskGroup>;
|
||||
me: UserAccount;
|
||||
me: MePayload;
|
||||
};
|
||||
|
||||
|
||||
@ -260,10 +278,8 @@ export type Mutation = {
|
||||
deleteUserAccount: DeleteUserAccountPayload;
|
||||
logoutUser: Scalars['Boolean'];
|
||||
removeTaskLabel: Task;
|
||||
setProjectOwner: SetProjectOwnerPayload;
|
||||
setTaskChecklistItemComplete: TaskChecklistItem;
|
||||
setTaskComplete: Task;
|
||||
setTeamOwner: SetTeamOwnerPayload;
|
||||
toggleTaskLabel: ToggleTaskLabelPayload;
|
||||
unassignTask: Task;
|
||||
updateProjectLabel: ProjectLabel;
|
||||
@ -412,11 +428,6 @@ export type MutationRemoveTaskLabelArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationSetProjectOwnerArgs = {
|
||||
input: SetProjectOwner;
|
||||
};
|
||||
|
||||
|
||||
export type MutationSetTaskChecklistItemCompleteArgs = {
|
||||
input: SetTaskChecklistItemComplete;
|
||||
};
|
||||
@ -427,11 +438,6 @@ export type MutationSetTaskCompleteArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationSetTeamOwnerArgs = {
|
||||
input: SetTeamOwner;
|
||||
};
|
||||
|
||||
|
||||
export type MutationToggleTaskLabelArgs = {
|
||||
input: ToggleTaskLabelInput;
|
||||
};
|
||||
@ -531,6 +537,25 @@ export type MutationUpdateUserRoleArgs = {
|
||||
input: UpdateUserRole;
|
||||
};
|
||||
|
||||
export type TeamRole = {
|
||||
__typename?: 'TeamRole';
|
||||
teamID: Scalars['UUID'];
|
||||
roleCode: RoleCode;
|
||||
};
|
||||
|
||||
export type ProjectRole = {
|
||||
__typename?: 'ProjectRole';
|
||||
projectID: Scalars['UUID'];
|
||||
roleCode: RoleCode;
|
||||
};
|
||||
|
||||
export type MePayload = {
|
||||
__typename?: 'MePayload';
|
||||
user: UserAccount;
|
||||
teamRoles: Array<TeamRole>;
|
||||
projectRoles: Array<ProjectRole>;
|
||||
};
|
||||
|
||||
export type ProjectsFilter = {
|
||||
teamID?: Maybe<Scalars['UUID']>;
|
||||
};
|
||||
@ -540,7 +565,7 @@ export type FindUser = {
|
||||
};
|
||||
|
||||
export type FindProject = {
|
||||
projectId: Scalars['String'];
|
||||
projectID: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type FindTask = {
|
||||
@ -633,18 +658,6 @@ export type UpdateProjectMemberRolePayload = {
|
||||
member: Member;
|
||||
};
|
||||
|
||||
export type SetProjectOwner = {
|
||||
projectID: Scalars['UUID'];
|
||||
ownerID: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type SetProjectOwnerPayload = {
|
||||
__typename?: 'SetProjectOwnerPayload';
|
||||
ok: Scalars['Boolean'];
|
||||
prevOwner: Member;
|
||||
newOwner: Member;
|
||||
};
|
||||
|
||||
export type NewTask = {
|
||||
taskGroupID: Scalars['String'];
|
||||
name: Scalars['String'];
|
||||
@ -868,19 +881,8 @@ export type UpdateTeamMemberRole = {
|
||||
export type UpdateTeamMemberRolePayload = {
|
||||
__typename?: 'UpdateTeamMemberRolePayload';
|
||||
ok: Scalars['Boolean'];
|
||||
member: Member;
|
||||
};
|
||||
|
||||
export type SetTeamOwner = {
|
||||
teamID: Scalars['UUID'];
|
||||
userID: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type SetTeamOwnerPayload = {
|
||||
__typename?: 'SetTeamOwnerPayload';
|
||||
ok: Scalars['Boolean'];
|
||||
prevOwner: Member;
|
||||
newOwner: Member;
|
||||
member: Member;
|
||||
};
|
||||
|
||||
export type UpdateUserPassword = {
|
||||
@ -1066,7 +1068,7 @@ export type DeleteTaskGroupMutation = (
|
||||
);
|
||||
|
||||
export type FindProjectQueryVariables = {
|
||||
projectId: Scalars['String'];
|
||||
projectID: Scalars['UUID'];
|
||||
};
|
||||
|
||||
|
||||
@ -1075,7 +1077,10 @@ export type FindProjectQuery = (
|
||||
& { findProject: (
|
||||
{ __typename?: 'Project' }
|
||||
& Pick<Project, 'name'>
|
||||
& { members: Array<(
|
||||
& { team: (
|
||||
{ __typename?: 'Team' }
|
||||
& Pick<Team, 'id'>
|
||||
), members: Array<(
|
||||
{ __typename?: 'Member' }
|
||||
& Pick<Member, 'id' | 'fullName' | 'username'>
|
||||
& { role: (
|
||||
@ -1242,12 +1247,21 @@ export type MeQueryVariables = {};
|
||||
export type MeQuery = (
|
||||
{ __typename?: 'Query' }
|
||||
& { me: (
|
||||
{ __typename?: 'UserAccount' }
|
||||
& Pick<UserAccount, 'id' | 'fullName'>
|
||||
& { profileIcon: (
|
||||
{ __typename?: 'ProfileIcon' }
|
||||
& Pick<ProfileIcon, 'initials' | 'bgColor' | 'url'>
|
||||
) }
|
||||
{ __typename?: 'MePayload' }
|
||||
& { user: (
|
||||
{ __typename?: 'UserAccount' }
|
||||
& Pick<UserAccount, 'id' | 'fullName'>
|
||||
& { profileIcon: (
|
||||
{ __typename?: 'ProfileIcon' }
|
||||
& Pick<ProfileIcon, 'initials' | 'bgColor' | 'url'>
|
||||
) }
|
||||
), teamRoles: Array<(
|
||||
{ __typename?: 'TeamRole' }
|
||||
& Pick<TeamRole, 'teamID' | 'roleCode'>
|
||||
)>, projectRoles: Array<(
|
||||
{ __typename?: 'ProjectRole' }
|
||||
& Pick<ProjectRole, 'projectID' | 'roleCode'>
|
||||
)> }
|
||||
) }
|
||||
);
|
||||
|
||||
@ -1311,35 +1325,6 @@ export type DeleteProjectMemberMutation = (
|
||||
) }
|
||||
);
|
||||
|
||||
export type SetProjectOwnerMutationVariables = {
|
||||
projectID: Scalars['UUID'];
|
||||
ownerID: Scalars['UUID'];
|
||||
};
|
||||
|
||||
|
||||
export type SetProjectOwnerMutation = (
|
||||
{ __typename?: 'Mutation' }
|
||||
& { setProjectOwner: (
|
||||
{ __typename?: 'SetProjectOwnerPayload' }
|
||||
& Pick<SetProjectOwnerPayload, 'ok'>
|
||||
& { newOwner: (
|
||||
{ __typename?: 'Member' }
|
||||
& Pick<Member, 'id'>
|
||||
& { role: (
|
||||
{ __typename?: 'Role' }
|
||||
& Pick<Role, 'code' | 'name'>
|
||||
) }
|
||||
), prevOwner: (
|
||||
{ __typename?: 'Member' }
|
||||
& Pick<Member, 'id'>
|
||||
& { role: (
|
||||
{ __typename?: 'Role' }
|
||||
& Pick<Role, 'code' | 'name'>
|
||||
) }
|
||||
) }
|
||||
) }
|
||||
);
|
||||
|
||||
export type UpdateProjectMemberRoleMutationVariables = {
|
||||
projectID: Scalars['UUID'];
|
||||
userID: Scalars['UUID'];
|
||||
@ -1706,6 +1691,29 @@ export type GetTeamQuery = (
|
||||
)> }
|
||||
);
|
||||
|
||||
export type UpdateTeamMemberRoleMutationVariables = {
|
||||
teamID: Scalars['UUID'];
|
||||
userID: Scalars['UUID'];
|
||||
roleCode: RoleCode;
|
||||
};
|
||||
|
||||
|
||||
export type UpdateTeamMemberRoleMutation = (
|
||||
{ __typename?: 'Mutation' }
|
||||
& { updateTeamMemberRole: (
|
||||
{ __typename?: 'UpdateTeamMemberRolePayload' }
|
||||
& Pick<UpdateTeamMemberRolePayload, 'teamID'>
|
||||
& { member: (
|
||||
{ __typename?: 'Member' }
|
||||
& Pick<Member, 'id'>
|
||||
& { role: (
|
||||
{ __typename?: 'Role' }
|
||||
& Pick<Role, 'code' | 'name'>
|
||||
) }
|
||||
) }
|
||||
) }
|
||||
);
|
||||
|
||||
export type ToggleTaskLabelMutationVariables = {
|
||||
taskID: Scalars['UUID'];
|
||||
projectLabelID: Scalars['UUID'];
|
||||
@ -2325,9 +2333,12 @@ export type DeleteTaskGroupMutationHookResult = ReturnType<typeof useDeleteTaskG
|
||||
export type DeleteTaskGroupMutationResult = ApolloReactCommon.MutationResult<DeleteTaskGroupMutation>;
|
||||
export type DeleteTaskGroupMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteTaskGroupMutation, DeleteTaskGroupMutationVariables>;
|
||||
export const FindProjectDocument = gql`
|
||||
query findProject($projectId: String!) {
|
||||
findProject(input: {projectId: $projectId}) {
|
||||
query findProject($projectID: UUID!) {
|
||||
findProject(input: {projectID: $projectID}) {
|
||||
name
|
||||
team {
|
||||
id
|
||||
}
|
||||
members {
|
||||
id
|
||||
fullName
|
||||
@ -2418,7 +2429,7 @@ export const FindProjectDocument = gql`
|
||||
* @example
|
||||
* const { data, loading, error } = useFindProjectQuery({
|
||||
* variables: {
|
||||
* projectId: // value for 'projectId'
|
||||
* projectID: // value for 'projectID'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
@ -2563,12 +2574,22 @@ export type GetProjectsQueryResult = ApolloReactCommon.QueryResult<GetProjectsQu
|
||||
export const MeDocument = gql`
|
||||
query me {
|
||||
me {
|
||||
id
|
||||
fullName
|
||||
profileIcon {
|
||||
initials
|
||||
bgColor
|
||||
url
|
||||
user {
|
||||
id
|
||||
fullName
|
||||
profileIcon {
|
||||
initials
|
||||
bgColor
|
||||
url
|
||||
}
|
||||
}
|
||||
teamRoles {
|
||||
teamID
|
||||
roleCode
|
||||
}
|
||||
projectRoles {
|
||||
projectID
|
||||
roleCode
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2717,53 +2738,6 @@ export function useDeleteProjectMemberMutation(baseOptions?: ApolloReactHooks.Mu
|
||||
export type DeleteProjectMemberMutationHookResult = ReturnType<typeof useDeleteProjectMemberMutation>;
|
||||
export type DeleteProjectMemberMutationResult = ApolloReactCommon.MutationResult<DeleteProjectMemberMutation>;
|
||||
export type DeleteProjectMemberMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteProjectMemberMutation, DeleteProjectMemberMutationVariables>;
|
||||
export const SetProjectOwnerDocument = gql`
|
||||
mutation setProjectOwner($projectID: UUID!, $ownerID: UUID!) {
|
||||
setProjectOwner(input: {projectID: $projectID, ownerID: $ownerID}) {
|
||||
ok
|
||||
newOwner {
|
||||
id
|
||||
role {
|
||||
code
|
||||
name
|
||||
}
|
||||
}
|
||||
prevOwner {
|
||||
id
|
||||
role {
|
||||
code
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type SetProjectOwnerMutationFn = ApolloReactCommon.MutationFunction<SetProjectOwnerMutation, SetProjectOwnerMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useSetProjectOwnerMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useSetProjectOwnerMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useSetProjectOwnerMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [setProjectOwnerMutation, { data, loading, error }] = useSetProjectOwnerMutation({
|
||||
* variables: {
|
||||
* projectID: // value for 'projectID'
|
||||
* ownerID: // value for 'ownerID'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useSetProjectOwnerMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<SetProjectOwnerMutation, SetProjectOwnerMutationVariables>) {
|
||||
return ApolloReactHooks.useMutation<SetProjectOwnerMutation, SetProjectOwnerMutationVariables>(SetProjectOwnerDocument, baseOptions);
|
||||
}
|
||||
export type SetProjectOwnerMutationHookResult = ReturnType<typeof useSetProjectOwnerMutation>;
|
||||
export type SetProjectOwnerMutationResult = ApolloReactCommon.MutationResult<SetProjectOwnerMutation>;
|
||||
export type SetProjectOwnerMutationOptions = ApolloReactCommon.BaseMutationOptions<SetProjectOwnerMutation, SetProjectOwnerMutationVariables>;
|
||||
export const UpdateProjectMemberRoleDocument = gql`
|
||||
mutation updateProjectMemberRole($projectID: UUID!, $userID: UUID!, $roleCode: RoleCode!) {
|
||||
updateProjectMemberRole(input: {projectID: $projectID, userID: $userID, roleCode: $roleCode}) {
|
||||
@ -3510,6 +3484,47 @@ export function useGetTeamLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHook
|
||||
export type GetTeamQueryHookResult = ReturnType<typeof useGetTeamQuery>;
|
||||
export type GetTeamLazyQueryHookResult = ReturnType<typeof useGetTeamLazyQuery>;
|
||||
export type GetTeamQueryResult = ApolloReactCommon.QueryResult<GetTeamQuery, GetTeamQueryVariables>;
|
||||
export const UpdateTeamMemberRoleDocument = gql`
|
||||
mutation updateTeamMemberRole($teamID: UUID!, $userID: UUID!, $roleCode: RoleCode!) {
|
||||
updateTeamMemberRole(input: {teamID: $teamID, userID: $userID, roleCode: $roleCode}) {
|
||||
member {
|
||||
id
|
||||
role {
|
||||
code
|
||||
name
|
||||
}
|
||||
}
|
||||
teamID
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type UpdateTeamMemberRoleMutationFn = ApolloReactCommon.MutationFunction<UpdateTeamMemberRoleMutation, UpdateTeamMemberRoleMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useUpdateTeamMemberRoleMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useUpdateTeamMemberRoleMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useUpdateTeamMemberRoleMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [updateTeamMemberRoleMutation, { data, loading, error }] = useUpdateTeamMemberRoleMutation({
|
||||
* variables: {
|
||||
* teamID: // value for 'teamID'
|
||||
* userID: // value for 'userID'
|
||||
* roleCode: // value for 'roleCode'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useUpdateTeamMemberRoleMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<UpdateTeamMemberRoleMutation, UpdateTeamMemberRoleMutationVariables>) {
|
||||
return ApolloReactHooks.useMutation<UpdateTeamMemberRoleMutation, UpdateTeamMemberRoleMutationVariables>(UpdateTeamMemberRoleDocument, baseOptions);
|
||||
}
|
||||
export type UpdateTeamMemberRoleMutationHookResult = ReturnType<typeof useUpdateTeamMemberRoleMutation>;
|
||||
export type UpdateTeamMemberRoleMutationResult = ApolloReactCommon.MutationResult<UpdateTeamMemberRoleMutation>;
|
||||
export type UpdateTeamMemberRoleMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateTeamMemberRoleMutation, UpdateTeamMemberRoleMutationVariables>;
|
||||
export const ToggleTaskLabelDocument = gql`
|
||||
mutation toggleTaskLabel($taskID: UUID!, $projectLabelID: UUID!) {
|
||||
toggleTaskLabel(input: {taskID: $taskID, projectLabelID: $projectLabelID}) {
|
||||
|
@ -2,9 +2,12 @@ import gql from 'graphql-tag';
|
||||
import TASK_FRAGMENT from './fragments/task';
|
||||
|
||||
const FIND_PROJECT_QUERY = gql`
|
||||
query findProject($projectId: String!) {
|
||||
findProject(input: { projectId: $projectId }) {
|
||||
query findProject($projectID: UUID!) {
|
||||
findProject(input: { projectID: $projectID }) {
|
||||
name
|
||||
team {
|
||||
id
|
||||
}
|
||||
members {
|
||||
id
|
||||
fullName
|
||||
|
@ -1,11 +1,21 @@
|
||||
query me {
|
||||
me {
|
||||
id
|
||||
fullName
|
||||
profileIcon {
|
||||
initials
|
||||
bgColor
|
||||
url
|
||||
user {
|
||||
id
|
||||
fullName
|
||||
profileIcon {
|
||||
initials
|
||||
bgColor
|
||||
url
|
||||
}
|
||||
}
|
||||
teamRoles {
|
||||
teamID
|
||||
roleCode
|
||||
}
|
||||
projectRoles {
|
||||
projectID
|
||||
roleCode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,25 +0,0 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const SET_PROJECT_OWNER_MUTATION = gql`
|
||||
mutation setProjectOwner($projectID: UUID!, $ownerID: UUID!) {
|
||||
setProjectOwner(input: { projectID: $projectID, ownerID: $ownerID }) {
|
||||
ok
|
||||
newOwner {
|
||||
id
|
||||
role {
|
||||
code
|
||||
name
|
||||
}
|
||||
}
|
||||
prevOwner {
|
||||
id
|
||||
role {
|
||||
code
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default SET_PROJECT_OWNER_MUTATION;
|
18
frontend/src/shared/graphql/team/updateTeamMemberRole.ts
Normal file
18
frontend/src/shared/graphql/team/updateTeamMemberRole.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const UPDATE_TEAM_MEMBER_ROLE_MUTATION = gql`
|
||||
mutation updateTeamMemberRole($teamID: UUID!, $userID: UUID!, $roleCode: RoleCode!) {
|
||||
updateTeamMemberRole(input: { teamID: $teamID, userID: $userID, roleCode: $roleCode }) {
|
||||
member {
|
||||
id
|
||||
role {
|
||||
code
|
||||
name
|
||||
}
|
||||
}
|
||||
teamID
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default UPDATE_TEAM_MEMBER_ROLE_MUTATION;
|
5
frontend/src/shared/utils/user.ts
Normal file
5
frontend/src/shared/utils/user.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { PermissionObjectType, PermissionLevel } from 'App/context';
|
||||
|
||||
export default function userCan(level: PermissionLevel, objectType: PermissionObjectType) {
|
||||
return false;
|
||||
}
|
1
frontend/src/taskcafe.d.ts
vendored
1
frontend/src/taskcafe.d.ts
vendored
@ -1,5 +1,6 @@
|
||||
interface JWTToken {
|
||||
userId: string;
|
||||
orgRole: string;
|
||||
iat: string;
|
||||
exp: string;
|
||||
}
|
||||
|
Reference in New Issue
Block a user