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:
Jordan Knott
2020-07-31 20:01:14 -05:00
committed by Jordan Knott
parent 5dbdc20b36
commit e64f6f8569
63 changed files with 3017 additions and 1905 deletions

View File

@ -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);

View File

@ -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;

View File

@ -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 cant 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}

View File

@ -0,0 +1,5 @@
import { InMemoryCache } from 'apollo-cache-inmemory';
const cache = new InMemoryCache();
export default cache;

View File

@ -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;

View File

@ -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>
</>
);
};

View File

@ -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);

View File

@ -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');

View File

@ -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();

View File

@ -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 },
);
},
})

View File

@ -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 } });
}
}
}}
/>

View File

@ -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;

View File

@ -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`} />} />

View File

@ -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 });
}
}}

View File

@ -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 cant leave because you are the only admin. To make another user an admin, click their avatar, select Change permissions, and select Admin.';
const [createTeamMember] = useCreateTeamMemberMutation({
@ -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

View File

@ -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

View File

@ -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(

View File

@ -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')}

View File

@ -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}

View File

@ -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>

View File

@ -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>
</>
);
};

View File

@ -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 cant 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 />
</>

View File

@ -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}) {

View File

@ -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

View File

@ -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
}
}
}

View File

@ -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;

View 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;

View File

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

View File

@ -1,5 +1,6 @@
interface JWTToken {
userId: string;
orgRole: string;
iat: string;
exp: string;
}