feature: add more to project pane

This commit is contained in:
Jordan Knott
2020-05-26 19:53:31 -05:00
parent 7e78ee36b4
commit fba4de631f
64 changed files with 1845 additions and 582 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 295 KiB

View File

@ -24,7 +24,7 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
<title>Citadel</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@ -5,7 +5,10 @@ import { useHistory } from 'react-router';
import UserIDContext from 'App/context';
import { useMeQuery } from 'shared/generated/graphql';
const GlobalTopNavbar: React.FC = () => {
type GlobalTopNavbarProps = {
name: string;
};
const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({ name }) => {
const { loading, data } = useMeQuery();
const history = useHistory();
const { userID, setUserID } = useContext(UserIDContext);
@ -41,6 +44,7 @@ const GlobalTopNavbar: React.FC = () => {
return (
<>
<TopNavbar
projectName={name}
bgColor={data ? data.me.profileIcon.bgColor ?? '#7367F0' : '#7367F0'}
firstName={data ? data.me.firstName : ''}
lastName={data ? data.me.lastName : ''}

View File

@ -2,7 +2,6 @@ import React, { useState, useEffect } from 'react';
import jwtDecode from 'jwt-decode';
import { createBrowserHistory } from 'history';
import { setAccessToken } from 'shared/utils/accessToken';
import GlobalTopNavbar from 'App/TopNavbar';
import styled from 'styled-components';
import NormalizeStyles from './NormalizeStyles';
import BaseStyles from './BaseStyles';
@ -10,6 +9,7 @@ import Routes from './Routes';
import { UserIDContext } from './context';
import Navbar from './Navbar';
import { Router } from 'react-router';
import { PopupProvider } from 'shared/components/PopupMenu';
const history = createBrowserHistory();
@ -45,21 +45,22 @@ const App = () => {
return (
<>
<UserIDContext.Provider value={{ userID, setUserID }}>
<NormalizeStyles />
<BaseStyles />
<Router history={history}>
{loading ? (
<div>loading</div>
) : (
<>
<Navbar />
<MainContent>
<GlobalTopNavbar />
<Routes history={history} />
</MainContent>
</>
)}
</Router>
<PopupProvider>
<NormalizeStyles />
<BaseStyles />
<Router history={history}>
{loading ? (
<div>loading</div>
) : (
<>
<Navbar />
<MainContent>
<Routes history={history} />
</MainContent>
</>
)}
</Router>
</PopupProvider>
</UserIDContext.Provider>
</>
);

View File

@ -1,11 +1,12 @@
import React, { useState, useContext } from 'react';
import Modal from 'shared/components/Modal';
import TaskDetails from 'shared/components/TaskDetails';
import PopupMenu from 'shared/components/PopupMenu';
import PopupMenu, { Popup, usePopup } from 'shared/components/PopupMenu';
import MemberManager from 'shared/components/MemberManager';
import { useRouteMatch, useHistory } from 'react-router';
import { useFindTaskQuery, useAssignTaskMutation, useUnassignTaskMutation } from 'shared/generated/graphql';
import UserIDContext from 'App/context';
import MiniProfile from 'shared/components/MiniProfile';
type DetailsProps = {
taskID: string;
@ -13,7 +14,7 @@ type DetailsProps = {
onTaskNameChange: (task: Task, newName: string) => void;
onTaskDescriptionChange: (task: Task, newDescription: string) => void;
onDeleteTask: (task: Task) => void;
onOpenAddLabelPopup: (task: Task, bounds: ElementBounds) => void;
onOpenAddLabelPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
availableMembers: Array<TaskUser>;
refreshCache: () => void;
};
@ -31,8 +32,10 @@ const Details: React.FC<DetailsProps> = ({
refreshCache,
}) => {
const { userID } = useContext(UserIDContext);
const { showPopup } = usePopup();
const history = useHistory();
const match = useRouteMatch();
const [currentMemberTask, setCurrentMemberTask] = useState('');
const [memberPopupData, setMemberPopupData] = useState(initialMemberPopupState);
const { loading, data, refetch } = useFindTaskQuery({ variables: { taskID } });
const [assignTask] = useAssignTaskMutation({
@ -55,7 +58,7 @@ const Details: React.FC<DetailsProps> = ({
}
const taskMembers = data.findTask.assigned.map(assigned => {
return {
userID: assigned.userID,
userID: assigned.id,
displayName: `${assigned.firstName} ${assigned.lastName}`,
profileIcon: {
url: null,
@ -76,6 +79,8 @@ const Details: React.FC<DetailsProps> = ({
<TaskDetails
task={{
...data.findTask,
taskID: data.findTask.id,
taskGroup: { taskGroupID: data.findTask.taskGroup.id },
members: taskMembers,
description: data.findTask.description ?? '',
labels: [],
@ -84,42 +89,48 @@ const Details: React.FC<DetailsProps> = ({
onTaskDescriptionChange={onTaskDescriptionChange}
onDeleteTask={onDeleteTask}
onCloseModal={() => history.push(projectURL)}
onOpenAddMemberPopup={(task, bounds) => {
console.log(task, bounds);
setMemberPopupData({
isOpen: true,
taskID: task.taskID,
top: bounds.position.top + bounds.size.height + 10,
left: bounds.position.left,
});
onMemberProfile={($targetRef, memberID) => {
showPopup(
$targetRef,
<Popup title={null} onClose={() => {}} tab={0}>
<MiniProfile
profileIcon={taskMembers[0].profileIcon}
displayName="Jordan Knott"
username="@jordanthedev"
bio="None"
onRemoveFromTask={() => {
unassignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } });
}}
/>
</Popup>,
);
}}
onOpenAddMemberPopup={(task, $targetRef) => {
console.log(`task: ${task.taskID}`);
showPopup(
$targetRef,
<Popup title="Members" tab={0} onClose={() => {}}>
<MemberManager
availableMembers={availableMembers}
activeMembers={taskMembers}
onMemberChange={(member, isActive) => {
console.log(`is active ${member.userID} - ${isActive}`);
if (isActive) {
assignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } });
} else {
unassignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } });
}
console.log(member, isActive);
}}
/>
</Popup>,
);
}}
onOpenAddLabelPopup={onOpenAddLabelPopup}
/>
);
}}
/>
{memberPopupData.isOpen && (
<PopupMenu
title="Members"
top={memberPopupData.top}
onClose={() => setMemberPopupData(initialMemberPopupState)}
left={memberPopupData.left}
>
<MemberManager
availableMembers={availableMembers}
activeMembers={taskMembers}
onMemberChange={(member, isActive) => {
console.log(`is active ${member.userID} - ${isActive}`);
if (isActive) {
assignTask({ variables: { taskID: data.findTask.taskID, userID: userID ?? '' } });
} else {
unassignTask({ variables: { taskID: data.findTask.taskID, userID: userID ?? '' } });
}
console.log(member, isActive);
}}
/>
</PopupMenu>
)}
</>
);
};

View File

@ -6,7 +6,7 @@ import { Board } from './Styles';
type KanbanBoardProps = {
listsData: BoardState;
onOpenListActionsPopup: (isOpen: boolean, left: number, top: number, taskGroupID: string) => void;
onOpenListActionsPopup: ($targetRef: React.RefObject<HTMLElement>, taskGroupID: string) => void;
onCardDrop: (task: Task) => void;
onListDrop: (taskGroup: TaskGroup) => void;
onCardCreate: (taskGroupID: string, name: string) => void;
@ -31,8 +31,8 @@ const KanbanBoard: React.FC<KanbanBoardProps> = ({
onCardClick={task => {
history.push(`${match.url}/c/${task.taskID}`);
}}
onExtraMenuOpen={(taskGroupID, pos, size) => {
onOpenListActionsPopup(true, pos.left, pos.top + size.height + 5, taskGroupID);
onExtraMenuOpen={(taskGroupID, $targetRef) => {
onOpenListActionsPopup($targetRef, taskGroupID);
}}
onQuickEditorOpen={onQuickEditorOpen}
onCardCreate={onCardCreate}

View File

@ -1,6 +1,9 @@
import React, { useState } from 'react';
import React, { useState, useRef } from 'react';
import * as BoardStateUtils from 'shared/utils/boardState';
import GlobalTopNavbar from 'App/TopNavbar';
import styled from 'styled-components/macro';
import { Bolt, ToggleOn, Tags } from 'shared/icons';
import { usePopup, Popup } from 'shared/components/PopupMenu';
import { useParams, Route, useRouteMatch, useHistory, RouteComponentProps } from 'react-router-dom';
import {
useFindProjectQuery,
@ -13,14 +16,19 @@ import {
useDeleteTaskGroupMutation,
useUpdateTaskDescriptionMutation,
useAssignTaskMutation,
DeleteTaskDocument,
FindProjectDocument,
} from 'shared/generated/graphql';
import QuickCardEditor from 'shared/components/QuickCardEditor';
import PopupMenu from 'shared/components/PopupMenu';
import ListActions from 'shared/components/ListActions';
import MemberManager from 'shared/components/MemberManager';
import { LabelsPopup } from 'shared/components/PopupMenu/PopupMenu.stories';
import KanbanBoard from 'Projects/Project/KanbanBoard';
import { mixin } from 'shared/utils/styles';
import LabelManager from 'shared/components/PopupMenu/LabelManager';
import LabelEditor from 'shared/components/PopupMenu/LabelEditor';
import produce from 'immer';
import Details from './Details';
type TaskRouteProps = {
@ -45,6 +53,67 @@ const Title = styled.span`
color: #fff;
`;
type LabelManagerEditorProps = {
labels: Array<Label>;
};
const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({ labels: initialLabels }) => {
const [labels, setLabels] = useState<Array<Label>>(initialLabels);
const [currentLabel, setCurrentLabel] = useState('');
const { setTab } = usePopup();
return (
<>
<Popup title="Labels" tab={0} onClose={() => {}}>
<LabelManager
labels={labels}
onLabelCreate={() => {
setTab(2);
}}
onLabelEdit={labelId => {
setCurrentLabel(labelId);
setTab(1);
}}
onLabelToggle={labelId => {
setLabels(
produce(labels, draftState => {
const idx = labels.findIndex(label => label.labelId === labelId);
if (idx !== -1) {
draftState[idx] = { ...draftState[idx], active: !labels[idx].active };
}
}),
);
}}
/>
</Popup>
<Popup onClose={() => {}} title="Edit label" tab={1}>
<LabelEditor
label={labels.find(label => label.labelId === currentLabel) ?? null}
onLabelEdit={(_labelId, name, color) => {
setLabels(
produce(labels, draftState => {
const idx = labels.findIndex(label => label.labelId === currentLabel);
if (idx !== -1) {
draftState[idx] = { ...draftState[idx], name, color };
}
}),
);
setTab(0);
}}
/>
</Popup>
<Popup onClose={() => {}} title="Create new label" tab={2}>
<LabelEditor
label={null}
onLabelEdit={(_labelId, name, color) => {
setLabels([...labels, { labelId: name, name, color, active: false }]);
setTab(0);
}}
/>
</Popup>
</>
);
};
interface ProjectParams {
projectId: string;
}
@ -55,6 +124,34 @@ const initialQuickCardEditorState: QuickCardEditorState = { isOpen: false, top:
const initialLabelsPopupState = { taskID: '', isOpen: false, top: 0, left: 0 };
const initialTaskDetailsState = { isOpen: false, taskID: '' };
const ProjectActions = styled.div`
display: flex;
align-items: center;
justify-content: flex-end;
height: 40px;
padding: 0 12px;
`;
const ProjectAction = styled.div`
cursor: pointer;
display: flex;
align-items: center;
font-size: 15px;
color: #c2c6dc;
&:not(:last-child) {
margin-right: 16px;
}
&:hover {
color: ${mixin.lighten('#c2c6dc', 0.25)};
}
`;
const ProjectActionText = styled.span`
padding-left: 4px;
`;
const Project = () => {
const { projectId } = useParams<ProjectParams>();
const match = useRouteMatch();
@ -70,17 +167,16 @@ const Project = () => {
const [deleteTaskGroup] = useDeleteTaskGroupMutation({
onCompleted: deletedTaskGroupData => {
setListsData(
BoardStateUtils.deleteTaskGroup(listsData, deletedTaskGroupData.deleteTaskGroup.taskGroup.taskGroupID),
);
setListsData(BoardStateUtils.deleteTaskGroup(listsData, deletedTaskGroupData.deleteTaskGroup.taskGroup.id));
},
});
const [createTaskGroup] = useCreateTaskGroupMutation({
onCompleted: newTaskGroupData => {
const newTaskGroup = {
...newTaskGroupData.createTaskGroup,
taskGroupID: newTaskGroupData.createTaskGroup.id,
tasks: [],
...newTaskGroupData.createTaskGroup,
};
setListsData(BoardStateUtils.addTaskGroup(listsData, newTaskGroup));
},
@ -90,10 +186,43 @@ const Project = () => {
onCompleted: newTaskData => {
const newTask = {
...newTaskData.createTask,
taskID: newTaskData.createTask.id,
taskGroup: { taskGroupID: newTaskData.createTask.taskGroup.id },
labels: [],
};
setListsData(BoardStateUtils.addTask(listsData, newTask));
},
update: (client, newTaskData) => {
const cacheData: any = client.readQuery({
query: FindProjectDocument,
variables: {
projectId: projectId,
},
});
console.log(cacheData);
console.log(newTaskData);
const newTaskGroups = produce(cacheData.findProject.taskGroups, (draftState: any) => {
const targetIndex = draftState.findIndex(
(taskGroup: any) => taskGroup.id === newTaskData.data.createTask.taskGroup.id,
);
draftState[targetIndex] = {
...draftState[targetIndex],
tasks: [...draftState[targetIndex].tasks, { ...newTaskData.data.createTask }],
};
});
console.log(newTaskGroups);
const newData = {
...cacheData.findProject,
taskGroups: newTaskGroups,
};
client.writeQuery({
query: FindProjectDocument,
variables: {
projectId: projectId,
},
data: { findProject: newData },
});
},
});
const [deleteTask] = useDeleteTaskMutation({
@ -105,50 +234,14 @@ const Project = () => {
const [updateTaskName] = useUpdateTaskNameMutation({
onCompleted: newTaskData => {
setListsData(
BoardStateUtils.updateTaskName(listsData, newTaskData.updateTaskName.taskID, newTaskData.updateTaskName.name),
BoardStateUtils.updateTaskName(listsData, newTaskData.updateTaskName.id, newTaskData.updateTaskName.name),
);
},
});
const { loading, data, refetch } = useFindProjectQuery({
variables: { projectId },
onCompleted: newData => {
console.log('beep!');
const newListsData: BoardState = { tasks: {}, columns: {} };
newData.findProject.taskGroups.forEach(taskGroup => {
newListsData.columns[taskGroup.taskGroupID] = {
taskGroupID: taskGroup.taskGroupID,
name: taskGroup.name,
position: taskGroup.position,
tasks: [],
};
taskGroup.tasks.forEach(task => {
const taskMembers = task.assigned.map(assigned => {
return {
userID: assigned.userID,
displayName: `${assigned.firstName} ${assigned.lastName}`,
profileIcon: {
url: null,
initials: assigned.profileIcon.initials ?? '',
bgColor: assigned.profileIcon.bgColor ?? '#7367F0',
},
};
});
newListsData.tasks[task.taskID] = {
taskID: task.taskID,
taskGroup: {
taskGroupID: taskGroup.taskGroupID,
},
name: task.name,
labels: [],
position: task.position,
description: task.description ?? undefined,
members: taskMembers,
};
});
});
setListsData(newListsData);
},
});
console.log(`loading ${loading} - ${data}`);
const onCardCreate = (taskGroupID: string, name: string) => {
const taskGroupTasks = Object.values(listsData.tasks).filter(
@ -163,15 +256,6 @@ const Project = () => {
createTask({ variables: { taskGroupID, name, position } });
};
const onQuickEditorOpen = (e: ContextMenuEvent) => {
const currentTask = Object.values(listsData.tasks).find(task => task.taskID === e.taskID);
setQuickCardEditor({
top: e.top,
left: e.left,
isOpen: true,
task: currentTask,
});
};
const onCardDrop = (droppedTask: Task) => {
updateTaskLocation({
variables: {
@ -202,10 +286,50 @@ const Project = () => {
const [assignTask] = useAssignTaskMutation();
const { showPopup } = usePopup();
const $labelsRef = useRef<HTMLDivElement>(null);
if (loading) {
return <Title>Error Loading</Title>;
return (
<>
<GlobalTopNavbar name="Project" />
<Title>Error Loading</Title>
</>
);
}
if (data) {
const currentListsData: BoardState = { tasks: {}, columns: {} };
data.findProject.taskGroups.forEach(taskGroup => {
currentListsData.columns[taskGroup.id] = {
taskGroupID: taskGroup.id,
name: taskGroup.name,
position: taskGroup.position,
tasks: [],
};
taskGroup.tasks.forEach(task => {
const taskMembers = task.assigned.map(assigned => {
return {
userID: assigned.id,
displayName: `${assigned.firstName} ${assigned.lastName}`,
profileIcon: {
url: null,
initials: assigned.profileIcon.initials ?? '',
bgColor: assigned.profileIcon.bgColor ?? '#7367F0',
},
};
});
currentListsData.tasks[task.id] = {
taskID: task.id,
taskGroup: {
taskGroupID: taskGroup.id,
},
name: task.name,
labels: [],
position: task.position,
description: task.description ?? undefined,
members: taskMembers,
};
});
});
const availableMembers = data.findProject.members.map(member => {
return {
displayName: `${member.firstName} ${member.lastName}`,
@ -214,38 +338,75 @@ const Project = () => {
initials: member.profileIcon.initials ?? null,
bgColor: member.profileIcon.bgColor ?? null,
},
userID: member.userID,
userID: member.id,
};
});
const onQuickEditorOpen = (e: ContextMenuEvent) => {
const currentTask = Object.values(currentListsData.tasks).find(task => task.taskID === e.taskID);
console.log(`currentTask: ${currentTask?.taskID}`);
setQuickCardEditor({
top: e.top,
left: e.left,
isOpen: true,
task: currentTask,
});
};
return (
<>
<GlobalTopNavbar name={data.findProject.name} />
<ProjectActions>
<ProjectAction
ref={$labelsRef}
onClick={() => {
showPopup(
$labelsRef,
<LabelManagerEditor
labels={data.findProject.labels.map(label => {
return {
labelId: label.id,
name: label.name ?? '',
color: label.colorHex,
active: false,
};
})}
/>,
);
}}
>
<Tags size={13} color="#c2c6dc" />
<ProjectActionText>Labels</ProjectActionText>
</ProjectAction>
<ProjectAction>
<ToggleOn size={13} color="#c2c6dc" />
<ProjectActionText>Fields</ProjectActionText>
</ProjectAction>
<ProjectAction>
<Bolt size={13} color="#c2c6dc" />
<ProjectActionText>Rules</ProjectActionText>
</ProjectAction>
</ProjectActions>
<KanbanBoard
listsData={listsData}
listsData={currentListsData}
onCardDrop={onCardDrop}
onListDrop={onListDrop}
onCardCreate={onCardCreate}
onCreateList={onCreateList}
onQuickEditorOpen={onQuickEditorOpen}
onOpenListActionsPopup={(isOpen, left, top, taskGroupID) => {
setPopupData({ isOpen, top, left, taskGroupID });
onOpenListActionsPopup={($targetRef, taskGroupID) => {
showPopup(
$targetRef,
<Popup title="List actions" tab={0} onClose={() => {}}>
<ListActions
taskGroupID={taskGroupID}
onArchiveTaskGroup={tgID => {
deleteTaskGroup({ variables: { taskGroupID: tgID } });
setPopupData(initialPopupState);
}}
/>
</Popup>,
);
}}
/>
{popupData.isOpen && (
<PopupMenu
title="List Actions"
top={popupData.top}
onClose={() => setPopupData(initialPopupState)}
left={popupData.left}
>
<ListActions
taskGroupID={popupData.taskGroupID}
onArchiveTaskGroup={taskGroupID => {
deleteTaskGroup({ variables: { taskGroupID } });
setPopupData(initialPopupState);
}}
/>
</PopupMenu>
)}
{quickCardEditor.isOpen && (
<QuickCardEditor
isOpen
@ -257,7 +418,35 @@ const Project = () => {
updateTaskName({ variables: { taskID: cardId, name: cardName } });
}}
onOpenPopup={() => console.log()}
onArchiveCard={(_listId: string, cardId: string) => deleteTask({ variables: { taskID: cardId } })}
onArchiveCard={(_listId: string, cardId: string) =>
deleteTask({
variables: { taskID: cardId },
update: client => {
const cacheData: any = client.readQuery({
query: FindProjectDocument,
variables: {
projectId: projectId,
},
});
const newData = {
...cacheData.findProject,
taskGroups: cacheData.findProject.taskGroups.map((taskGroup: any) => {
return {
...taskGroup,
tasks: taskGroup.tasks.filter((t: any) => t.id !== cardId),
};
}),
};
client.writeQuery({
query: FindProjectDocument,
variables: {
projectId: projectId,
},
data: { findProject: newData },
});
},
})
}
labels={[]}
top={quickCardEditor.top}
left={quickCardEditor.left}
@ -269,7 +458,7 @@ const Project = () => {
<Details
refreshCache={() => {
console.log('beep 2!');
refetch();
// refetch();
}}
availableMembers={availableMembers}
projectURL={match.url}
@ -284,7 +473,7 @@ const Project = () => {
setTaskDetails(initialTaskDetailsState);
deleteTask({ variables: { taskID: deletedTask.taskID } });
}}
onOpenAddLabelPopup={(task, bounds) => {}}
onOpenAddLabelPopup={(task, $targetRef) => {}}
/>
)}
/>

View File

@ -1,5 +1,6 @@
import React, { useState } from 'react';
import styled from 'styled-components/macro';
import GlobalTopNavbar from 'App/TopNavbar';
import { useGetProjectsQuery } from 'shared/generated/graphql';
import ProjectGridItem from 'shared/components/ProjectGridItem';
@ -40,13 +41,18 @@ const Projects = () => {
if (data) {
const { projects } = data;
return (
<ProjectGrid>
{projects.map(project => (
<ProjectLink key={project.projectID} to={`/projects/${project.projectID}`}>
<ProjectGridItem project={{ ...project, teamTitle: project.team.name, taskGroups: [] }} />
</ProjectLink>
))}
</ProjectGrid>
<>
<GlobalTopNavbar name="Projects" />
<ProjectGrid>
{projects.map(project => (
<ProjectLink key={project.id} to={`/projects/${project.id}`}>
<ProjectGridItem
project={{ ...project, projectID: project.id, teamTitle: project.team.name, taskGroups: [] }}
/>
</ProjectLink>
))}
</ProjectGrid>
</>
);
}
return <div>Error!</div>;

View File

@ -52,7 +52,7 @@ type Task = {
name: string;
position: number;
labels: Label[];
description?: string;
description?: string | null;
members?: Array<TaskUser>;
};

View File

@ -16,7 +16,7 @@ export const CardComposerWrapper = styled.div<{ isOpen: boolean }>`
`;
export const ListCard = styled.div`
background-color: #fff;
background-color: ${props => mixin.lighten('#262c49', 0.05)};
border-radius: 3px;
${mixin.boxShadowCard}
cursor: pointer;
@ -55,9 +55,10 @@ export const ListCardEditor = styled(TextareaAutosize)`
padding: 0;
font-size: 14px;
line-height: 20px;
&:focus {
border: none;
outline: none;
color: #c2c6dc;
l &:focus {
background-color: ${props => mixin.lighten('#262c49', 0.05)};
}
`;

View File

@ -4,8 +4,8 @@ export const Container = styled.div<{ left: number; top: number }>`
position: absolute;
left: ${props => props.left}px;
top: ${props => props.top}px;
padding-top: 10px;
position: absolute;
padding-top: 10px;
height: auto;
width: auto;
transform: translate(-100%);
@ -18,12 +18,12 @@ export const Wrapper = styled.div`
padding-top: 8px;
border-radius: 5px;
box-shadow: 0 5px 25px 0 rgba(0, 0, 0, 0.1);
border: 1px solid rgba(0, 0, 0, 0.1);
position: relative;
margin: 0;
color: #c2c6dc;
background: #262c49;
border: 1px solid rgba(0, 0, 0, 0.1);
border-color: #414561;
`;

View File

@ -21,7 +21,7 @@ export const AddCardContainer = styled.div`
export const AddCardButton = styled.a`
border-radius: 3px;
color: #5e6c84;
color: #c2c6dc;
display: flex;
align-items: center;
cursor: pointer;
@ -32,9 +32,9 @@ export const AddCardButton = styled.a`
text-decoration: none;
user-select: none;
&:hover {
background-color: rgba(9, 30, 66, 0.08);
color: #172b4d;
color: #c2c6dc;
text-decoration: none;
background: rgb(115, 103, 240);
}
`;
export const Wrapper = styled.div`
@ -125,4 +125,5 @@ export const ListExtraMenuButtonWrapper = styled.div`
top: 4px;
z-index: 1;
padding: 6px;
padding-bottom: 0;
`;

View File

@ -26,7 +26,7 @@ type Props = {
wrapperProps?: any;
headerProps?: any;
index?: number;
onExtraMenuOpen: (taskGroupID: string, pos: ElementPosition, size: ElementSize) => void;
onExtraMenuOpen: (taskGroupID: string, $targetRef: React.RefObject<HTMLElement>) => void;
};
const List = React.forwardRef(
@ -78,20 +78,7 @@ const List = React.forwardRef(
const handleExtraMenuOpen = () => {
if ($extraActionsRef && $extraActionsRef.current) {
const pos = $extraActionsRef.current.getBoundingClientRect();
onExtraMenuOpen(
id,
{
top: pos.top,
left: pos.left,
right: pos.right,
bottom: pos.bottom,
},
{
width: pos.width,
height: pos.height,
},
);
onExtraMenuOpen(id, $extraActionsRef);
}
};
useOnEscapeKeyDown(isEditingTitle, onEscape);
@ -116,7 +103,7 @@ const List = React.forwardRef(
{children && children}
<AddCardContainer hidden={isComposerOpen}>
<AddCardButton onClick={() => onOpenComposer(id)}>
<Plus size={12} color="#42526e" />
<Plus size={12} color="#c2c6dc" />
<AddCardButtonText>Add another card</AddCardButtonText>
</AddCardButton>
</AddCardContainer>

View File

@ -14,19 +14,19 @@ export const ListActionItem = styled.span`
cursor: pointer;
display: block;
font-size: 14px;
color: #172b4d;
color: #c2c6dc;
font-weight: 400;
padding: 6px 12px;
position: relative;
margin: 0 -12px;
text-decoration: none;
&:hover {
background-color: rgba(9, 30, 66, 0.04);
background: rgb(115, 103, 240);
}
`;
export const ListSeparator = styled.hr`
background-color: rgba(9, 30, 66, 0.13);
background-color: #414561;
border: 0;
height: 1px;
margin: 8px 0;

View File

@ -29,7 +29,7 @@ type Props = {
onCardCreate: (taskGroupID: string, name: string) => void;
onQuickEditorOpen: (e: ContextMenuEvent) => void;
onCreateList: (listName: string) => void;
onExtraMenuOpen: (taskGroupID: string, pos: ElementPosition, size: ElementSize) => void;
onExtraMenuOpen: (taskGroupID: string, $targetRef: React.RefObject<HTMLElement>) => void;
};
const Lists: React.FC<Props> = ({

View File

@ -1,5 +1,6 @@
import styled from 'styled-components';
import TextareaAutosize from 'react-autosize-textarea/lib';
import { mixin } from 'shared/utils/styles';
export const MemberManagerWrapper = styled.div``;
@ -11,17 +12,27 @@ export const MemberManagerSearchWrapper = styled.div`
export const MemberManagerSearch = styled(TextareaAutosize)`
margin: 4px 0 12px;
width: 100%;
background-color: #ebecf0;
border: none;
box-shadow: inset 0 0 0 2px #dfe1e6;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 3px;
line-height: 20px;
padding: 8px 12px;
font-size: 14px;
color: #172b4d;
font-family: 'Droid Sans';
font-weight: 400;
background: #262c49;
outline: none;
color: #c2c6dc;
border-color: #414561;
&:focus {
box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px;
background: ${mixin.darken('#262c49', 0.15)};
}
`;
export const BoardMembersLabel = styled.h4`
color: #5e6c84;
color: #c2c6dc;
font-size: 12px;
font-weight: 500;
letter-spacing: 0.04em;
@ -52,7 +63,7 @@ export const BoardMemberListItemContent = styled.div`
white-space: nowrap;
padding: 4px;
margin-bottom: 2px;
color: #172b4d;
color: #c2c6dc;
`;
export const ProfileIcon = styled.div`
@ -62,7 +73,7 @@ export const ProfileIcon = styled.div`
display: flex;
align-items: center;
justify-content: center;
color: #fff;
color: #c2c6dc;
font-weight: 700;
background: rgb(115, 103, 240);
cursor: pointer;

View File

@ -43,7 +43,7 @@ const MemberManager: React.FC<MemberManagerProps> = ({
)
.map(member => {
return (
<BoardMembersListItem>
<BoardMembersListItem key={member.userID}>
<BoardMemberListItemContent
onClick={() => {
const isActive = activeMembers.findIndex(m => m.userID === member.userID) !== -1;

View File

@ -11,7 +11,11 @@ export const ProfileIcon = styled.div<{ bgColor: string }>`
margin: 2px;
background-color: ${props => props.bgColor};
border-radius: 25em;
display: block;
font-size: 16px;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
height: 50px;
overflow: hidden;
position: relative;
@ -20,6 +24,7 @@ export const ProfileIcon = styled.div<{ bgColor: string }>`
`;
export const ProfileInfo = styled.div`
color: #c2c6dc;
margin: 0 0 0 64px;
word-wrap: break-word;
`;
@ -29,11 +34,11 @@ export const InfoTitle = styled.h3`
font-size: 16px;
font-weight: 600;
line-height: 20px;
color: #172b4d;
color: #c2c6dc;
`;
export const InfoUsername = styled.p`
color: #5e6c84;
color: #c2c6dc;
font-size: 14px;
line-height: 20px;
`;
@ -41,10 +46,29 @@ export const InfoUsername = styled.p`
export const InfoBio = styled.p`
font-size: 14px;
line-height: 20px;
color: #5e6c84;
color: #c2c6dc;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin: 0;
padding: 0;
`;
export const MiniProfileActions = styled.ul`
list-style-type: none;
`;
export const MiniProfileActionWrapper = styled.li``;
export const MiniProfileActionItem = styled.span`
color: #c2c6dc;
cursor: pointer;
display: block;
font-weight: 400;
padding: 6px 12px;
position: relative;
text-decoration: none;
&:hover {
background: rgb(115, 103, 240);
}
`;

View File

@ -1,14 +1,25 @@
import React from 'react';
import { Profile, ProfileIcon, ProfileInfo, InfoTitle, InfoUsername, InfoBio } from './Styles';
import {
Profile,
ProfileIcon,
ProfileInfo,
InfoTitle,
InfoUsername,
InfoBio,
MiniProfileActions,
MiniProfileActionWrapper,
MiniProfileActionItem,
} from './Styles';
type MiniProfileProps = {
displayName: string;
username: string;
bio: string;
profileIcon: ProfileIcon;
onRemoveFromTask: () => void;
};
const MiniProfile: React.FC<MiniProfileProps> = ({ displayName, username, bio, profileIcon }) => {
const MiniProfile: React.FC<MiniProfileProps> = ({ displayName, username, bio, profileIcon, onRemoveFromTask }) => {
return (
<>
<Profile>
@ -19,6 +30,17 @@ const MiniProfile: React.FC<MiniProfileProps> = ({ displayName, username, bio, p
<InfoBio>{bio}</InfoBio>
</ProfileInfo>
</Profile>
<MiniProfileActions>
<MiniProfileActionWrapper>
<MiniProfileActionItem
onClick={() => {
onRemoveFromTask();
}}
>
Remove from card
</MiniProfileActionItem>
</MiniProfileActionWrapper>
</MiniProfileActions>
</>
);
};

View File

@ -2,7 +2,7 @@ import styled from 'styled-components';
import { mixin } from 'shared/utils/styles';
export const ScrollOverlay = styled.div`
z-index: 1000000;
z-index: 3000;
position: fixed;
top: 0;
left: 0;

View File

@ -4,25 +4,51 @@ import { Checkmark } from 'shared/icons';
import { SaveButton, DeleteButton, LabelBox, EditLabelForm, FieldLabel, FieldName } from './Styles';
type Props = {
label: Label;
onLabelEdit: (labelId: string, labelName: string, color: string) => void;
label: Label | null;
onLabelEdit: (labelId: string | null, labelName: string, color: string) => void;
};
const LabelManager = ({ label, onLabelEdit }: Props) => {
const [currentLabel, setCurrentLabel] = useState('');
console.log(label);
const [currentLabel, setCurrentLabel] = useState(label ? label.name : '');
const [currentColor, setCurrentColor] = useState<string | null>(label ? label.color : null);
return (
<EditLabelForm>
<FieldLabel>Name</FieldLabel>
<FieldName id="labelName" type="text" name="name" value={currentLabel} />
<FieldName
id="labelName"
type="text"
name="name"
onChange={e => {
setCurrentLabel(e.currentTarget.value);
}}
value={currentLabel}
/>
<FieldLabel>Select a color</FieldLabel>
<div>
{Object.values(LabelColors).map(labelColor => (
<LabelBox color={labelColor}>
<Checkmark color="#fff" size={12} />
<LabelBox
color={labelColor}
onClick={() => {
setCurrentColor(labelColor);
}}
>
{labelColor === currentColor && <Checkmark color="#fff" size={12} />}
</LabelBox>
))}
</div>
<div>
<SaveButton type="submit" value="Save" />
<SaveButton
onClick={e => {
e.preventDefault();
console.log(currentColor);
if (currentColor) {
onLabelEdit(label ? label.labelId : null, currentLabel, currentColor);
}
}}
type="submit"
value="Save"
/>
<DeleteButton type="submit" value="Delete" />
</div>
</EditLabelForm>

View File

@ -1,46 +1,78 @@
import React, { useState } from 'react';
import { Pencil, Checkmark } from 'shared/icons';
import { LabelSearch, ActiveIcon, Labels, Label, CardLabel, Section, SectionTitle, LabelIcon } from './Styles';
import {
LabelSearch,
ActiveIcon,
Labels,
Label,
CardLabel,
Section,
SectionTitle,
LabelIcon,
CreateLabelButton,
} from './Styles';
type Props = {
labels?: Label[];
onLabelToggle: (labelId: string) => void;
onLabelEdit: (labelId: string, labelName: string, color: string) => void;
onLabelEdit: (labelId: string) => void;
onLabelCreate: () => void;
};
const LabelManager: React.FC<Props> = ({ labels, onLabelToggle, onLabelEdit }) => {
const LabelManager: React.FC<Props> = ({ labels, onLabelToggle, onLabelEdit, onLabelCreate }) => {
const [currentLabel, setCurrentLabel] = useState('');
const [currentSearch, setCurrentSearch] = useState('');
return (
<>
<LabelSearch type="text" />
<LabelSearch
type="text"
placeholder="search labels..."
onChange={e => {
setCurrentSearch(e.currentTarget.value);
}}
value={currentSearch}
/>
<Section>
<SectionTitle>Labels</SectionTitle>
<Labels>
{labels &&
labels.map(label => (
<Label>
<LabelIcon>
<Pencil />
</LabelIcon>
<CardLabel
key={label.labelId}
color={label.color}
active={currentLabel === label.labelId}
onMouseEnter={() => {
setCurrentLabel(label.labelId);
}}
onClick={() => onLabelToggle(label.labelId)}
>
{label.name}
{label.active && (
<ActiveIcon>
<Checkmark color="#fff" />
</ActiveIcon>
)}
</CardLabel>
</Label>
))}
labels
.filter(label => currentSearch === '' || label.name.toLowerCase().startsWith(currentSearch.toLowerCase()))
.map(label => (
<Label key={label.labelId}>
<LabelIcon
onClick={() => {
onLabelEdit(label.labelId);
}}
>
<Pencil color="#c2c6dc" />
</LabelIcon>
<CardLabel
key={label.labelId}
color={label.color}
active={currentLabel === label.labelId}
onMouseEnter={() => {
setCurrentLabel(label.labelId);
}}
onClick={() => onLabelToggle(label.labelId)}
>
{label.name}
{label.active && (
<ActiveIcon>
<Checkmark color="#fff" />
</ActiveIcon>
)}
</CardLabel>
</Label>
))}
</Labels>
<CreateLabelButton
onClick={() => {
onLabelCreate();
}}
>
Create a new label
</CreateLabelButton>
</Section>
</>
);

View File

@ -1,4 +1,4 @@
import React, { useState, useRef } from 'react';
import React, { useState, useRef, createRef } from 'react';
import { action } from '@storybook/addon-actions';
import LabelColors from 'shared/constants/labelColors';
import LabelManager from 'shared/components/PopupMenu/LabelManager';
@ -7,10 +7,12 @@ import ListActions from 'shared/components/ListActions';
import MemberManager from 'shared/components/MemberManager';
import DueDateManager from 'shared/components/DueDateManager';
import MiniProfile from 'shared/components/MiniProfile';
import styled from 'styled-components';
import PopupMenu from '.';
import PopupMenu, { PopupProvider, usePopup, Popup } from '.';
import NormalizeStyles from 'App/NormalizeStyles';
import BaseStyles from 'App/BaseStyles';
import produce from 'immer';
export default {
component: PopupMenu,
@ -37,19 +39,93 @@ const labelData = [
},
];
const OpenLabelBtn = styled.span``;
type TabProps = {
tab: number;
};
const LabelManagerEditor = () => {
const [labels, setLabels] = useState(labelData);
const [currentLabel, setCurrentLabel] = useState('');
const { setTab } = usePopup();
return (
<>
<Popup title="Labels" tab={0} onClose={action('on close')}>
<LabelManager
labels={labels}
onLabelCreate={() => {
setTab(2);
}}
onLabelEdit={labelId => {
setCurrentLabel(labelId);
setTab(1);
}}
onLabelToggle={labelId => {
setLabels(
produce(labels, draftState => {
const idx = labels.findIndex(label => label.labelId === labelId);
if (idx !== -1) {
draftState[idx] = { ...draftState[idx], active: !labels[idx].active };
}
}),
);
}}
/>
</Popup>
<Popup onClose={action('on close')} title="Edit label" tab={1}>
<LabelEditor
label={labels.find(label => label.labelId === currentLabel) ?? null}
onLabelEdit={(_labelId, name, color) => {
setLabels(
produce(labels, draftState => {
const idx = labels.findIndex(label => label.labelId === currentLabel);
if (idx !== -1) {
draftState[idx] = { ...draftState[idx], name, color };
}
}),
);
setTab(0);
}}
/>
</Popup>
<Popup onClose={action('on close')} title="Create new label" tab={2}>
<LabelEditor
label={null}
onLabelEdit={(_labelId, name, color) => {
setLabels([...labels, { labelId: name, name, color, active: false }]);
setTab(0);
}}
/>
</Popup>
</>
);
};
const OpenLabelsButton = () => {
const $buttonRef = createRef<HTMLButtonElement>();
const [currentLabel, setCurrentLabel] = useState('');
const [labels, setLabels] = useState(labelData);
const { showPopup, setTab } = usePopup();
console.log(labels);
return (
<OpenLabelBtn
ref={$buttonRef}
onClick={() => {
showPopup($buttonRef, <LabelManagerEditor />);
}}
>
Open
</OpenLabelBtn>
);
};
export const LabelsPopup = () => {
const [isPopupOpen, setPopupOpen] = useState(false);
return (
<>
{isPopupOpen && (
<PopupMenu title="Label" top={10} onClose={() => setPopupOpen(false)} left={10}>
<LabelManager labels={labelData} onLabelToggle={action('label toggle')} onLabelEdit={action('label edit')} />
</PopupMenu>
)}
<button type="submit" onClick={() => setPopupOpen(true)}>
Open
</button>
</>
<PopupProvider>
<OpenLabelsButton />
</PopupProvider>
);
};
@ -58,7 +134,13 @@ export const LabelsLabelEditor = () => {
return (
<>
{isPopupOpen && (
<PopupMenu title="Change Label" top={10} onClose={() => setPopupOpen(false)} left={10}>
<PopupMenu
onPrevious={action('on previous')}
title="Change Label"
top={10}
onClose={() => setPopupOpen(false)}
left={10}
>
<LabelEditor label={labelData[0]} onLabelEdit={action('label edit')} />
</PopupMenu>
)}
@ -201,12 +283,19 @@ export const MiniProfilePopup = () => {
<NormalizeStyles />
<BaseStyles />
{popupData.isOpen && (
<PopupMenu title="Due Date" top={popupData.top} onClose={() => setPopupData(initalState)} left={popupData.left}>
<PopupMenu
noHeader
title="Due Date"
top={popupData.top}
onClose={() => setPopupData(initalState)}
left={popupData.left}
>
<MiniProfile
displayName="Jordan Knott"
profileIcon={{ url: null, bgColor: '#000', initials: 'JK' }}
username="@jordanthedev"
bio="Stuff and things"
onRemoveFromTask={action('mini profile')}
/>
</PopupMenu>
)}
@ -236,3 +325,4 @@ export const MiniProfilePopup = () => {
</>
);
};

View File

@ -1,20 +1,34 @@
import styled, { css } from 'styled-components';
import { mixin } from 'shared/utils/styles';
export const Container = styled.div<{ top: number; left: number; ref: any }>`
export const Container = styled.div<{ invert: boolean; top: number; left: number; ref: any }>`
left: ${props => props.left}px;
top: ${props => props.top}px;
background: #fff;
border-radius: 3px;
box-shadow: 0 8px 16px -4px rgba(9, 30, 66, 0.25), 0 0 0 1px rgba(9, 30, 66, 0.08);
display: block;
position: absolute;
width: 304px;
z-index: 100000000000;
&:focus {
outline: none;
border: none;
}
width: 316px;
padding-top: 10px;
height: auto;
z-index: 40000;
${props =>
props.invert &&
css`
transform: translate(-100%);
`}
`;
export const Wrapper = styled.div`
padding: 5px;
padding-top: 8px;
border-radius: 5px;
box-shadow: 0 5px 25px 0 rgba(0, 0, 0, 0.1);
position: relative;
margin: 0;
color: #c2c6dc;
background: #262c49;
border: 1px solid rgba(0, 0, 0, 0.1);
border-color: #414561;
`;
export const Header = styled.div`
@ -26,10 +40,10 @@ export const Header = styled.div`
export const HeaderTitle = styled.span`
box-sizing: border-box;
color: #5e6c84;
color: #c2c6dc;
display: block;
line-height: 40px;
border-bottom: 1px solid rgba(9, 30, 66, 0.13);
border-bottom: 1px solid #414561;
margin: 0 12px;
overflow: hidden;
padding: 0 32px;
@ -46,23 +60,30 @@ export const Content = styled.div`
padding: 0 12px 12px;
`;
export const LabelSearch = styled.input`
box-sizing: border-box;
display: block;
transition-property: background-color, border-color, box-shadow;
transition-duration: 85ms;
transition-timing-function: ease;
margin: 4px 0 12px;
width: 100%;
background-color: #fafbfc;
border: none;
box-shadow: inset 0 0 0 2px #dfe1e6;
color: #172b4d;
box-sizing: border-box;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 3px;
display: block;
line-height: 20px;
padding: 8px 12px;
font-size: 14px;
font-family: 'Droid Sans';
font-weight: 400;
transition-property: background-color, border-color, box-shadow;
transition-duration: 85ms;
transition-timing-function: ease;
background: #262c49;
outline: none;
color: #c2c6dc;
border-color: #414561;
&:focus {
box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px;
background: ${mixin.darken('#262c49', 0.15)};
}
`;
export const Section = styled.div`
@ -70,7 +91,7 @@ export const Section = styled.div`
`;
export const SectionTitle = styled.h4`
color: #5e6c84;
color: #c2c6dc;
font-size: 12px;
font-weight: 500;
letter-spacing: 0.04em;
@ -95,7 +116,7 @@ export const CardLabel = styled.span<{ active: boolean; color: string }>`
props.active &&
css`
margin-left: 4px;
box-shadow: -8px 0 ${mixin.darken(props.color, 0.15)};
box-shadow: -8px 0 ${mixin.darken(props.color, 0.12)};
border-radius: 3px;
`}
@ -113,6 +134,7 @@ export const CardLabel = styled.span<{ active: boolean; color: string }>`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-height: 31px;
`;
export const CloseButton = styled.div`
@ -126,8 +148,6 @@ export const CloseButton = styled.div`
align-items: center;
justify-content: center;
z-index: 40;
height: 20px;
width: 20px;
cursor: pointer;
`;
@ -142,14 +162,14 @@ export const LabelIcon = styled.div`
align-items: center;
justify-content: center;
height: 20px;
height: 100%;
font-size: 16px;
line-height: 20px;
width: 20px;
width: auto;
cursor: pointer;
&:hover {
background: rgba(9, 30, 66, 0.08);
background: rgb(115, 103, 240);
}
`;
@ -186,19 +206,27 @@ export const FieldLabel = styled.label`
export const FieldName = styled.input`
margin: 4px 0 12px;
width: 100%;
background-color: #fafbfc;
border: none;
box-shadow: inset 0 0 0 2px #dfe1e6;
color: #172b4d;
box-sizing: border-box;
border-radius: 3px;
display: block;
line-height: 20px;
margin-bottom: 12px;
padding: 8px 12px;
background: #262c49;
border-width: 1px;
border-style: solid;
border-color: transparent;
border-image: initial;
font-size: 12px;
font-weight: 400;
color: #c2c6dc;
&:focus {
box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px;
background: ${mixin.darken('#262c49', 0.15)};
}
`;
export const LabelBox = styled.span<{ color: string }>`
@ -208,6 +236,7 @@ export const LabelBox = styled.span<{ color: string }>`
padding: 0;
width: 48px;
cursor: pointer;
background-color: ${props => props.color};
border-radius: 4px;
color: #fff;
@ -217,6 +246,7 @@ export const LabelBox = styled.span<{ color: string }>`
`;
export const SaveButton = styled.input`
cursor: pointer;
background-color: #5aac44;
box-shadow: none;
border: none;
@ -239,8 +269,7 @@ export const DeleteButton = styled.input`
border: none;
color: #fff;
cursor: pointer;
display: inline-block;
font-weight: 400;
type="submit"font-weight: 400;
line-height: 20px;
margin: 8px 4px 0 0;
padding: 6px 12px;
@ -248,3 +277,53 @@ export const DeleteButton = styled.input`
border-radius: 3px;
float: right;
`;
export const CreateLabelButton = styled.button`
outline: none;
border: none;
width: 100%;
border-radius: 3px;
line-height: 20px;
margin-bottom: 8px;
padding: 6px 12px;
background-color: none;
text-align: center;
color: #c2c6dc;
margin: 8px 4px 0 0;
font-size: 14px;
cursor: pointer;
&:hover {
background: rgb(115, 103, 240);
}
`;
export const PreviousButton = styled.div`
padding: 10px 12px 10px 8px;
position: absolute;
top: 0;
left: 0;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
z-index: 40;
cursor: pointer;
`;
export const ContainerDiamond = styled.div<{ invert: boolean }>`
top: 10px;
${props => (props.invert ? 'right: 10px; ' : 'left: 15px;')}
position: absolute;
width: 10px;
height: 10px;
display: block;
transform: rotate(45deg) translate(-7px);
border-top: 1px solid rgba(0, 0, 0, 0.1);
border-left: 1px solid rgba(0, 0, 0, 0.1);
z-index: 10;
background: #262c49;
border-color: #414561;
`;

View File

@ -1,30 +1,219 @@
import React, { useRef } from 'react';
import { Cross } from 'shared/icons';
import React, { useRef, createContext, RefObject, useState, useContext } from 'react';
import { Cross, AngleLeft } from 'shared/icons';
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import { Container, Header, HeaderTitle, Content, CloseButton } from './Styles';
import { createPortal } from 'react-dom';
import produce from 'immer';
import {
Container,
ContainerDiamond,
Header,
HeaderTitle,
Content,
CloseButton,
PreviousButton,
Wrapper,
} from './Styles';
type Props = {
title: string;
type PopupContextState = {
show: (target: RefObject<HTMLElement>, content: JSX.Element) => void;
setTab: (newTab: number) => void;
getCurrentTab: () => number;
};
type PopupProps = {
title: string | null;
onClose: () => void;
tab: number;
};
type PopupContainerProps = {
top: number;
left: number;
invert: boolean;
onClose: () => void;
};
const PopupMenu: React.FC<Props> = ({ title, top, left, onClose, children }) => {
const PopupContainer: React.FC<PopupContainerProps> = ({ top, left, onClose, children, invert }) => {
const $containerRef = useRef();
useOnOutsideClick($containerRef, true, onClose, null);
return (
<Container left={left} top={top} ref={$containerRef} invert={invert}>
{children}
</Container>
);
};
const PopupContext = createContext<PopupContextState>({
show: () => {},
setTab: () => {},
getCurrentTab: () => 0,
});
export const usePopup = () => {
const ctx = useContext<PopupContextState>(PopupContext);
return { showPopup: ctx.show, setTab: ctx.setTab, getCurrentTab: ctx.getCurrentTab };
};
type PopupState = {
isOpen: boolean;
left: number;
top: number;
invert: boolean;
currentTab: number;
previousTab: number;
content: JSX.Element | null;
};
const { Provider, Consumer } = PopupContext;
const canUseDOM = !!(typeof window !== 'undefined' && window.document && window.document.createElement);
const defaultState = {
isOpen: false,
left: 0,
top: 0,
invert: false,
currentTab: 0,
previousTab: 0,
content: null,
};
export const PopupProvider: React.FC = ({ children }) => {
const [currentState, setState] = useState<PopupState>(defaultState);
const show = (target: RefObject<HTMLElement>, content: JSX.Element) => {
console.log(target);
if (target && target.current) {
const bounds = target.current.getBoundingClientRect();
if (bounds.left + 304 + 30 > window.innerWidth) {
console.log('open!');
setState({
isOpen: true,
left: bounds.left + bounds.width,
top: bounds.top + bounds.height,
invert: true,
currentTab: 0,
previousTab: 0,
content,
});
} else {
console.log('open NOT INVERT!');
setState({
isOpen: true,
left: bounds.left,
top: bounds.top + bounds.height,
invert: false,
currentTab: 0,
previousTab: 0,
content,
});
}
}
};
const portalTarget = canUseDOM ? document.body : null; // appease flow
const setTab = (newTab: number) => {
setState((prevState: PopupState) => {
return {
...prevState,
previousTab: currentState.currentTab,
currentTab: newTab,
};
});
};
const getCurrentTab = () => {
return currentState.currentTab;
};
return (
<Provider value={{ show, setTab, getCurrentTab }}>
{portalTarget &&
currentState.isOpen &&
createPortal(
<PopupContainer
invert={currentState.invert}
top={currentState.top}
left={currentState.left}
onClose={() => setState(defaultState)}
>
{currentState.content}
<ContainerDiamond invert={currentState.invert} />
</PopupContainer>,
portalTarget,
)}
{children}
</Provider>
);
};
type Props = {
title: string | null;
top: number;
left: number;
onClose: () => void;
onPrevious?: () => void | null;
noHeader?: boolean | null;
};
const PopupMenu: React.FC<Props> = ({ title, top, left, onClose, noHeader, children, onPrevious }) => {
const $containerRef = useRef();
useOnOutsideClick($containerRef, true, onClose, null);
return (
<Container left={left} top={top} ref={$containerRef}>
<Header>
<HeaderTitle>{title}</HeaderTitle>
<CloseButton onClick={() => onClose()}>
<Cross />
</CloseButton>
</Header>
<Content>{children}</Content>
<Container invert={false} left={left} top={top} ref={$containerRef}>
<Wrapper>
{onPrevious && (
<PreviousButton onClick={onPrevious}>
<AngleLeft color="#c2c6dc" />
</PreviousButton>
)}
{noHeader ? (
<CloseButton onClick={() => onClose()}>
<Cross color="#c2c6dc" />
</CloseButton>
) : (
<Header>
<HeaderTitle>{title}</HeaderTitle>
<CloseButton onClick={() => onClose()}>
<Cross color="#c2c6dc" />
</CloseButton>
</Header>
)}
<Content>{children}</Content>
</Wrapper>
</Container>
);
};
export const Popup: React.FC<PopupProps> = ({ title, onClose, tab, children }) => {
const { getCurrentTab, setTab } = usePopup();
if (getCurrentTab() !== tab) {
return null;
}
return (
<>
<Wrapper>
{tab > 0 && (
<PreviousButton
onClick={() => {
setTab(0);
}}
>
<AngleLeft color="#c2c6dc" />
</PreviousButton>
)}
{title && (
<Header>
<HeaderTitle>{title}</HeaderTitle>
</Header>
)}
<CloseButton onClick={() => onClose()}>
<Cross color="#c2c6dc" />
</CloseButton>
<Content>{children}</Content>
</Wrapper>
</>
);
};
export default PopupMenu;

View File

@ -61,7 +61,7 @@ export const Default = () => {
onSaveName={action('on save name')}
onOpenComposer={action('on open composer')}
tasks={[]}
onExtraMenuOpen={(taskGroupID, pos, size) => console.log(taskGroupID, pos, size)}
onExtraMenuOpen={(taskGroupID, $targetRef) => console.log(taskGroupID, $targetRef)}
>
<ListCards>
<Card

View File

@ -2,7 +2,7 @@ import styled, { keyframes } from 'styled-components';
import TextareaAutosize from 'react-autosize-textarea';
export const Wrapper = styled.div<{ open: boolean }>`
background: rgba(0, 0, 0, 0.6);
background: rgba(0, 0, 0, 0.4);
bottom: 0;
color: #fff;
left: 0;

View File

@ -286,4 +286,7 @@ export const UnassignedLabel = styled.div`
color: rgb(137, 147, 164);
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
height: 32px;
`;

View File

@ -47,6 +47,7 @@ export const Default = () => {
onTaskDescriptionChange={(_task, desc) => setDescription(desc)}
onDeleteTask={action('delete task')}
onCloseModal={action('close modal')}
onMemberProfile={action('profile')}
onOpenAddMemberPopup={action('open add member popup')}
onOpenAddLabelPopup={action('open add label popup')}
/>

View File

@ -93,13 +93,27 @@ const DetailsEditor: React.FC<DetailsEditorProps> = ({
);
};
type TaskAssigneeProps = {
member: TaskUser;
onMemberProfile: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
};
const TaskAssignee: React.FC<TaskAssigneeProps> = ({ member, onMemberProfile }) => {
const $memberRef = useRef<HTMLDivElement>(null);
return (
<TaskDetailAssignee ref={$memberRef} onClick={() => onMemberProfile($memberRef, member.userID)} key={member.userID}>
<ProfileIcon>{member.profileIcon.initials ?? ''}</ProfileIcon>
</TaskDetailAssignee>
);
};
type TaskDetailsProps = {
task: Task;
onTaskNameChange: (task: Task, newName: string) => void;
onTaskDescriptionChange: (task: Task, newDescription: string) => void;
onDeleteTask: (task: Task) => void;
onOpenAddMemberPopup: (task: Task, bounds: ElementBounds) => void;
onOpenAddLabelPopup: (task: Task, bounds: ElementBounds) => void;
onOpenAddMemberPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
onOpenAddLabelPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
onMemberProfile: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
onCloseModal: () => void;
};
@ -111,6 +125,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
onCloseModal,
onOpenAddMemberPopup,
onOpenAddLabelPopup,
onMemberProfile,
}) => {
const [editorOpen, setEditorOpen] = useState(false);
const [description, setDescription] = useState(task.description ?? '');
@ -130,23 +145,14 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
const $unassignedRef = useRef<HTMLDivElement>(null);
const $addMemberRef = useRef<HTMLDivElement>(null);
const onUnassignedClick = () => {
const bounds = convertDivElementRefToBounds($unassignedRef);
if (bounds) {
onOpenAddMemberPopup(task, bounds);
}
onOpenAddMemberPopup(task, $unassignedRef);
};
const onAddMember = () => {
const bounds = convertDivElementRefToBounds($addMemberRef);
if (bounds) {
onOpenAddMemberPopup(task, bounds);
}
onOpenAddMemberPopup(task, $addMemberRef);
};
const $addLabelRef = useRef<HTMLDivElement>(null);
const onAddLabel = () => {
const bounds = convertDivElementRefToBounds($addLabelRef);
if (bounds) {
onOpenAddLabelPopup(task, bounds);
}
onOpenAddLabelPopup(task, $addLabelRef);
};
console.log(task);
return (
@ -204,14 +210,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
) : (
<>
{task.members &&
task.members.map(member => {
console.log(member);
return (
<TaskDetailAssignee key={member.userID}>
<ProfileIcon>{member.profileIcon.initials ?? ''}</ProfileIcon>
</TaskDetailAssignee>
);
})}
task.members.map(member => <TaskAssignee member={member} onMemberProfile={onMemberProfile} />)}
<TaskDetailsAddMember ref={$addMemberRef} onClick={onAddMember}>
<TaskDetailsAddMemberIcon>
<Plus size={16} color="#c2c6dc" />

View File

@ -1,4 +1,5 @@
import styled from 'styled-components';
import styled, { css } from 'styled-components';
import { mixin } from 'shared/utils/styles';
export const NavbarWrapper = styled.div`
width: 100%;
@ -76,8 +77,10 @@ export const ProfileIcon = styled.div<{ bgColor: string }>`
`;
export const ProjectMeta = styled.div`
align-items: center;
display: flex;
padding-top: 9px;
margin-left: -14px;
align-items: center;
max-width: 100%;
min-height: 51px;
`;
@ -91,11 +94,11 @@ export const ProjectTabs = styled.div`
max-width: 100%;
`;
export const ProjectTab = styled.span`
export const ProjectTab = styled.span<{ active?: boolean }>`
font-size: 80%;
color: #c2c6dc;
font-size: 15px;
cursor: default;
cursor: pointer;
display: flex;
line-height: normal;
min-width: 1px;
@ -103,16 +106,71 @@ export const ProjectTab = styled.span`
transition-property: box-shadow, color;
white-space: nowrap;
flex: 0 1 auto;
padding-bottom: 12px;
box-shadow: inset 0 -2px #d85dd8;
color: #d85dd8;
&:not(:last-child) {
margin-right: 20px;
}
${props =>
props.active
? css`
box-shadow: inset 0 -2px #d85dd8;
color: #d85dd8;
`
: css`
&:hover {
box-shadow: inset 0 -2px #cbd4db;
color: ${mixin.lighten('#c2c6dc', 0.25)};
}
`}
`;
export const ProjectName = styled.h1`
color: #c2c6dc;
margin-top: 9px;
font-weight: 600;
font-size: 20px;
padding: 6px 10px 6px 8px;
`;
export const ProjectSwitcher = styled.button`
font-size: 20px;
outline: none;
border: none;
width: 100px;
border-radius: 3px;
line-height: 20px;
padding: 6px 4px;
background-color: none;
text-align: center;
color: #c2c6dc;
cursor: pointer;
&:hover {
background: rgb(115, 103, 240);
}
`;
export const Separator = styled.div`
color: #c2c6dc;
font-size: 16px;
padding-left: 4px;
padding-right: 4px;
`;
export const ProjectSettingsButton = styled.button`
outline: none;
border: none;
border-radius: 3px;
line-height: 20px;
width: 28px;
height: 28px;
background-color: none;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&:hover {
background: rgb(115, 103, 240);
}
`;

View File

@ -38,6 +38,7 @@ export const Default = () => {
<NormalizeStyles />
<BaseStyles />
<TopNavbar
projectName="Projects"
bgColor="#7367F0"
firstName="Jordan"
lastName="Knott"

View File

@ -1,10 +1,12 @@
import React, { useRef } from 'react';
import { Bell } from 'shared/icons';
import { Star, Bell, Cog, AngleDown } from 'shared/icons';
import {
NotificationContainer,
GlobalActions,
ProjectActions,
ProjectSwitcher,
Separator,
ProjectMeta,
ProjectName,
ProjectTabs,
@ -13,6 +15,7 @@ import {
NavbarHeader,
Breadcrumbs,
BreadcrumpSeparator,
ProjectSettingsButton,
ProfileIcon,
ProfileContainer,
ProfileNameWrapper,
@ -21,6 +24,7 @@ import {
} from './Styles';
type NavBarProps = {
projectName: string;
onProfileClick: (bottom: number, right: number) => void;
onNotificationClick: () => void;
bgColor: string;
@ -29,6 +33,7 @@ type NavBarProps = {
initials: string;
};
const NavBar: React.FC<NavBarProps> = ({
projectName,
onProfileClick,
onNotificationClick,
firstName,
@ -47,10 +52,19 @@ const NavBar: React.FC<NavBarProps> = ({
<NavbarHeader>
<ProjectActions>
<ProjectMeta>
<ProjectName>Production Team</ProjectName>
<ProjectSwitcher>Projects</ProjectSwitcher>
<Separator>»</Separator>
<ProjectName>{projectName}</ProjectName>
<ProjectSettingsButton>
<AngleDown color="#c2c6dc" />
</ProjectSettingsButton>
<Star filled color="#c2c6dc" />
</ProjectMeta>
<ProjectTabs>
<ProjectTab>Board</ProjectTab>
<ProjectTab active>Board</ProjectTab>
<ProjectTab>Calender</ProjectTab>
<ProjectTab>Timeline</ProjectTab>
<ProjectTab>Wiki</ProjectTab>
</ProjectTabs>
</ProjectActions>
<GlobalActions>

View File

@ -17,7 +17,7 @@ export type Scalars = {
export type ProjectLabel = {
__typename?: 'ProjectLabel';
projectLabelID: Scalars['ID'];
id: Scalars['ID'];
createdDate: Scalars['Time'];
colorHex: Scalars['String'];
name?: Maybe<Scalars['String']>;
@ -25,7 +25,7 @@ export type ProjectLabel = {
export type TaskLabel = {
__typename?: 'TaskLabel';
taskLabelID: Scalars['ID'];
id: Scalars['ID'];
projectLabelID: Scalars['UUID'];
assignedDate: Scalars['Time'];
colorHex: Scalars['String'];
@ -41,7 +41,7 @@ export type ProfileIcon = {
export type ProjectMember = {
__typename?: 'ProjectMember';
userID: Scalars['ID'];
id: Scalars['ID'];
firstName: Scalars['String'];
lastName: Scalars['String'];
profileIcon: ProfileIcon;
@ -49,7 +49,7 @@ export type ProjectMember = {
export type RefreshToken = {
__typename?: 'RefreshToken';
tokenId: Scalars['ID'];
id: Scalars['ID'];
userId: Scalars['UUID'];
expiresAt: Scalars['Time'];
createdAt: Scalars['Time'];
@ -57,7 +57,7 @@ export type RefreshToken = {
export type UserAccount = {
__typename?: 'UserAccount';
userID: Scalars['ID'];
id: Scalars['ID'];
email: Scalars['String'];
createdAt: Scalars['Time'];
firstName: Scalars['String'];
@ -68,14 +68,14 @@ export type UserAccount = {
export type Team = {
__typename?: 'Team';
teamID: Scalars['ID'];
id: Scalars['ID'];
createdAt: Scalars['Time'];
name: Scalars['String'];
};
export type Project = {
__typename?: 'Project';
projectID: Scalars['ID'];
id: Scalars['ID'];
createdAt: Scalars['Time'];
name: Scalars['String'];
team: Team;
@ -87,7 +87,7 @@ export type Project = {
export type TaskGroup = {
__typename?: 'TaskGroup';
taskGroupID: Scalars['ID'];
id: Scalars['ID'];
projectID: Scalars['String'];
createdAt: Scalars['Time'];
name: Scalars['String'];
@ -97,7 +97,7 @@ export type TaskGroup = {
export type Task = {
__typename?: 'Task';
taskID: Scalars['ID'];
id: Scalars['ID'];
taskGroup: TaskGroup;
createdAt: Scalars['Time'];
name: Scalars['String'];
@ -382,14 +382,29 @@ export type AssignTaskMutation = (
{ __typename?: 'Mutation' }
& { assignTask: (
{ __typename?: 'Task' }
& Pick<Task, 'taskID'>
& Pick<Task, 'id'>
& { assigned: Array<(
{ __typename?: 'ProjectMember' }
& Pick<ProjectMember, 'userID' | 'firstName' | 'lastName'>
& Pick<ProjectMember, 'id' | 'firstName' | 'lastName'>
)> }
) }
);
export type CreateProjectLabelMutationVariables = {
projectID: Scalars['UUID'];
labelColorID: Scalars['UUID'];
name: Scalars['String'];
};
export type CreateProjectLabelMutation = (
{ __typename?: 'Mutation' }
& { createProjectLabel: (
{ __typename?: 'ProjectLabel' }
& Pick<ProjectLabel, 'id' | 'createdDate' | 'colorHex' | 'name'>
) }
);
export type CreateTaskMutationVariables = {
taskGroupID: Scalars['String'];
name: Scalars['String'];
@ -401,11 +416,18 @@ export type CreateTaskMutation = (
{ __typename?: 'Mutation' }
& { createTask: (
{ __typename?: 'Task' }
& Pick<Task, 'taskID' | 'name' | 'position'>
& Pick<Task, 'id' | 'name' | 'position' | 'description'>
& { taskGroup: (
{ __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'taskGroupID'>
) }
& Pick<TaskGroup, 'id'>
), assigned: Array<(
{ __typename?: 'ProjectMember' }
& Pick<ProjectMember, 'id' | 'firstName' | 'lastName'>
& { profileIcon: (
{ __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
) }
)> }
) }
);
@ -420,7 +442,7 @@ export type CreateTaskGroupMutation = (
{ __typename?: 'Mutation' }
& { createTaskGroup: (
{ __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'taskGroupID' | 'name' | 'position'>
& Pick<TaskGroup, 'id' | 'name' | 'position'>
) }
);
@ -449,10 +471,10 @@ export type DeleteTaskGroupMutation = (
& Pick<DeleteTaskGroupPayload, 'ok' | 'affectedRows'>
& { taskGroup: (
{ __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'taskGroupID'>
& Pick<TaskGroup, 'id'>
& { tasks: Array<(
{ __typename?: 'Task' }
& Pick<Task, 'taskID' | 'name'>
& Pick<Task, 'id' | 'name'>
)> }
) }
) }
@ -470,20 +492,23 @@ export type FindProjectQuery = (
& Pick<Project, 'name'>
& { members: Array<(
{ __typename?: 'ProjectMember' }
& Pick<ProjectMember, 'userID' | 'firstName' | 'lastName'>
& Pick<ProjectMember, 'id' | 'firstName' | 'lastName'>
& { profileIcon: (
{ __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
) }
)>, labels: Array<(
{ __typename?: 'ProjectLabel' }
& Pick<ProjectLabel, 'id' | 'createdDate' | 'colorHex' | 'name'>
)>, taskGroups: Array<(
{ __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'taskGroupID' | 'name' | 'position'>
& Pick<TaskGroup, 'id' | 'name' | 'position'>
& { tasks: Array<(
{ __typename?: 'Task' }
& Pick<Task, 'taskID' | 'name' | 'position' | 'description'>
& Pick<Task, 'id' | 'name' | 'position' | 'description'>
& { assigned: Array<(
{ __typename?: 'ProjectMember' }
& Pick<ProjectMember, 'userID' | 'firstName' | 'lastName'>
& Pick<ProjectMember, 'id' | 'firstName' | 'lastName'>
& { profileIcon: (
{ __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
@ -503,13 +528,13 @@ export type FindTaskQuery = (
{ __typename?: 'Query' }
& { findTask: (
{ __typename?: 'Task' }
& Pick<Task, 'taskID' | 'name' | 'description' | 'position'>
& Pick<Task, 'id' | 'name' | 'description' | 'position'>
& { taskGroup: (
{ __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'taskGroupID'>
& Pick<TaskGroup, 'id'>
), assigned: Array<(
{ __typename?: 'ProjectMember' }
& Pick<ProjectMember, 'userID' | 'firstName' | 'lastName'>
& Pick<ProjectMember, 'id' | 'firstName' | 'lastName'>
& { profileIcon: (
{ __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
@ -525,10 +550,10 @@ export type GetProjectsQuery = (
{ __typename?: 'Query' }
& { projects: Array<(
{ __typename?: 'Project' }
& Pick<Project, 'projectID' | 'name'>
& Pick<Project, 'id' | 'name'>
& { team: (
{ __typename?: 'Team' }
& Pick<Team, 'teamID' | 'name'>
& Pick<Team, 'id' | 'name'>
) }
)> }
);
@ -558,10 +583,10 @@ export type UnassignTaskMutation = (
{ __typename?: 'Mutation' }
& { unassignTask: (
{ __typename?: 'Task' }
& Pick<Task, 'taskID'>
& Pick<Task, 'id'>
& { assigned: Array<(
{ __typename?: 'ProjectMember' }
& Pick<ProjectMember, 'userID' | 'firstName' | 'lastName'>
& Pick<ProjectMember, 'id' | 'firstName' | 'lastName'>
)> }
) }
);
@ -576,7 +601,7 @@ export type UpdateTaskDescriptionMutation = (
{ __typename?: 'Mutation' }
& { updateTaskDescription: (
{ __typename?: 'Task' }
& Pick<Task, 'taskID'>
& Pick<Task, 'id'>
) }
);
@ -590,7 +615,7 @@ export type UpdateTaskGroupLocationMutation = (
{ __typename?: 'Mutation' }
& { updateTaskGroupLocation: (
{ __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'taskGroupID' | 'position'>
& Pick<TaskGroup, 'id' | 'position'>
) }
);
@ -605,7 +630,7 @@ export type UpdateTaskLocationMutation = (
{ __typename?: 'Mutation' }
& { updateTaskLocation: (
{ __typename?: 'Task' }
& Pick<Task, 'taskID' | 'createdAt' | 'name' | 'position'>
& Pick<Task, 'id' | 'createdAt' | 'name' | 'position'>
) }
);
@ -619,7 +644,7 @@ export type UpdateTaskNameMutation = (
{ __typename?: 'Mutation' }
& { updateTaskName: (
{ __typename?: 'Task' }
& Pick<Task, 'taskID' | 'name' | 'position'>
& Pick<Task, 'id' | 'name' | 'position'>
) }
);
@ -627,12 +652,12 @@ export type UpdateTaskNameMutation = (
export const AssignTaskDocument = gql`
mutation assignTask($taskID: UUID!, $userID: UUID!) {
assignTask(input: {taskID: $taskID, userID: $userID}) {
id
assigned {
userID
id
firstName
lastName
}
taskID
}
}
`;
@ -662,15 +687,63 @@ export function useAssignTaskMutation(baseOptions?: ApolloReactHooks.MutationHoo
export type AssignTaskMutationHookResult = ReturnType<typeof useAssignTaskMutation>;
export type AssignTaskMutationResult = ApolloReactCommon.MutationResult<AssignTaskMutation>;
export type AssignTaskMutationOptions = ApolloReactCommon.BaseMutationOptions<AssignTaskMutation, AssignTaskMutationVariables>;
export const CreateProjectLabelDocument = gql`
mutation createProjectLabel($projectID: UUID!, $labelColorID: UUID!, $name: String!) {
createProjectLabel(input: {projectID: $projectID, labelColorID: $labelColorID, name: $name}) {
id
createdDate
colorHex
name
}
}
`;
export type CreateProjectLabelMutationFn = ApolloReactCommon.MutationFunction<CreateProjectLabelMutation, CreateProjectLabelMutationVariables>;
/**
* __useCreateProjectLabelMutation__
*
* To run a mutation, you first call `useCreateProjectLabelMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useCreateProjectLabelMutation` 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 [createProjectLabelMutation, { data, loading, error }] = useCreateProjectLabelMutation({
* variables: {
* projectID: // value for 'projectID'
* labelColorID: // value for 'labelColorID'
* name: // value for 'name'
* },
* });
*/
export function useCreateProjectLabelMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<CreateProjectLabelMutation, CreateProjectLabelMutationVariables>) {
return ApolloReactHooks.useMutation<CreateProjectLabelMutation, CreateProjectLabelMutationVariables>(CreateProjectLabelDocument, baseOptions);
}
export type CreateProjectLabelMutationHookResult = ReturnType<typeof useCreateProjectLabelMutation>;
export type CreateProjectLabelMutationResult = ApolloReactCommon.MutationResult<CreateProjectLabelMutation>;
export type CreateProjectLabelMutationOptions = ApolloReactCommon.BaseMutationOptions<CreateProjectLabelMutation, CreateProjectLabelMutationVariables>;
export const CreateTaskDocument = gql`
mutation createTask($taskGroupID: String!, $name: String!, $position: Float!) {
createTask(input: {taskGroupID: $taskGroupID, name: $name, position: $position}) {
taskID
taskGroup {
taskGroupID
}
id
name
position
description
taskGroup {
id
}
assigned {
id
firstName
lastName
profileIcon {
url
initials
bgColor
}
}
}
}
`;
@ -704,7 +777,7 @@ export type CreateTaskMutationOptions = ApolloReactCommon.BaseMutationOptions<Cr
export const CreateTaskGroupDocument = gql`
mutation createTaskGroup($projectID: String!, $name: String!, $position: Float!) {
createTaskGroup(input: {projectID: $projectID, name: $name, position: $position}) {
taskGroupID
id
name
position
}
@ -775,9 +848,9 @@ export const DeleteTaskGroupDocument = gql`
ok
affectedRows
taskGroup {
taskGroupID
id
tasks {
taskID
id
name
}
}
@ -814,7 +887,7 @@ export const FindProjectDocument = gql`
findProject(input: {projectId: $projectId}) {
name
members {
userID
id
firstName
lastName
profileIcon {
@ -823,17 +896,23 @@ export const FindProjectDocument = gql`
bgColor
}
}
labels {
id
createdDate
colorHex
name
}
taskGroups {
taskGroupID
id
name
position
tasks {
taskID
id
name
position
description
assigned {
userID
id
firstName
lastName
profileIcon {
@ -876,15 +955,15 @@ export type FindProjectQueryResult = ApolloReactCommon.QueryResult<FindProjectQu
export const FindTaskDocument = gql`
query findTask($taskID: UUID!) {
findTask(input: {taskID: $taskID}) {
taskID
id
name
description
position
taskGroup {
taskGroupID
id
}
assigned {
userID
id
firstName
lastName
profileIcon {
@ -925,10 +1004,10 @@ export type FindTaskQueryResult = ApolloReactCommon.QueryResult<FindTaskQuery, F
export const GetProjectsDocument = gql`
query getProjects {
projects {
projectID
id
name
team {
teamID
id
name
}
}
@ -1000,11 +1079,11 @@ export const UnassignTaskDocument = gql`
mutation unassignTask($taskID: UUID!, $userID: UUID!) {
unassignTask(input: {taskID: $taskID, userID: $userID}) {
assigned {
userID
id
firstName
lastName
}
taskID
id
}
}
`;
@ -1037,7 +1116,7 @@ export type UnassignTaskMutationOptions = ApolloReactCommon.BaseMutationOptions<
export const UpdateTaskDescriptionDocument = gql`
mutation updateTaskDescription($taskID: UUID!, $description: String!) {
updateTaskDescription(input: {taskID: $taskID, description: $description}) {
taskID
id
}
}
`;
@ -1070,7 +1149,7 @@ export type UpdateTaskDescriptionMutationOptions = ApolloReactCommon.BaseMutatio
export const UpdateTaskGroupLocationDocument = gql`
mutation updateTaskGroupLocation($taskGroupID: UUID!, $position: Float!) {
updateTaskGroupLocation(input: {taskGroupID: $taskGroupID, position: $position}) {
taskGroupID
id
position
}
}
@ -1104,7 +1183,7 @@ export type UpdateTaskGroupLocationMutationOptions = ApolloReactCommon.BaseMutat
export const UpdateTaskLocationDocument = gql`
mutation updateTaskLocation($taskID: String!, $taskGroupID: String!, $position: Float!) {
updateTaskLocation(input: {taskID: $taskID, taskGroupID: $taskGroupID, position: $position}) {
taskID
id
createdAt
name
position
@ -1141,7 +1220,7 @@ export type UpdateTaskLocationMutationOptions = ApolloReactCommon.BaseMutationOp
export const UpdateTaskNameDocument = gql`
mutation updateTaskName($taskID: String!, $name: String!) {
updateTaskName(input: {taskID: $taskID, name: $name}) {
taskID
id
name
position
}

View File

@ -1,10 +1,10 @@
mutation assignTask($taskID: UUID!, $userID: UUID!) {
assignTask(input: {taskID: $taskID, userID: $userID}) {
id
assigned {
userID
id
firstName
lastName
}
taskID
}
}

View File

@ -0,0 +1,8 @@
mutation createProjectLabel($projectID: UUID!, $labelColorID: UUID!, $name: String!) {
createProjectLabel(input:{projectID:$projectID, labelColorID: $labelColorID, name: $name}) {
id
createdDate
colorHex
name
}
}

View File

@ -1,10 +1,21 @@
mutation createTask($taskGroupID: String!, $name: String!, $position: Float!) {
createTask(input: { taskGroupID: $taskGroupID, name: $name, position: $position }) {
taskID
taskGroup {
taskGroupID
}
id
name
position
description
taskGroup {
id
}
assigned {
id
firstName
lastName
profileIcon {
url
initials
bgColor
}
}
}
}

View File

@ -2,7 +2,7 @@ mutation createTaskGroup( $projectID: String!, $name: String!, $position: Float!
createTaskGroup(
input: { projectID: $projectID, name: $name, position: $position }
) {
taskGroupID
id
name
position
}

View File

@ -3,9 +3,9 @@ mutation deleteTaskGroup($taskGroupID: UUID!) {
ok
affectedRows
taskGroup {
taskGroupID
id
tasks {
taskID
id
name
}
}

View File

@ -2,7 +2,7 @@ query findProject($projectId: String!) {
findProject(input: { projectId: $projectId }) {
name
members {
userID
id
firstName
lastName
profileIcon {
@ -11,17 +11,23 @@ query findProject($projectId: String!) {
bgColor
}
}
labels {
id
createdDate
colorHex
name
}
taskGroups {
taskGroupID
id
name
position
tasks {
taskID
id
name
position
description
assigned {
userID
id
firstName
lastName
profileIcon {

View File

@ -1,14 +1,14 @@
query findTask($taskID: UUID!) {
findTask(input: {taskID: $taskID}) {
taskID
id
name
description
position
taskGroup {
taskGroupID
id
}
assigned {
userID
id
firstName
lastName
profileIcon {

View File

@ -1,9 +1,9 @@
query getProjects {
projects {
projectID
id
name
team {
teamID
id
name
}
}

View File

@ -1,10 +1,10 @@
mutation unassignTask($taskID: UUID!, $userID: UUID!) {
unassignTask(input: {taskID: $taskID, userID: $userID}) {
assigned {
userID
id
firstName
lastName
}
taskID
id
}
}

View File

@ -1,5 +1,5 @@
mutation updateTaskDescription($taskID: UUID!, $description: String!) {
updateTaskDescription(input: {taskID: $taskID, description: $description}) {
taskID
id
}
}

View File

@ -1,6 +1,6 @@
mutation updateTaskGroupLocation($taskGroupID: UUID!, $position: Float!) {
updateTaskGroupLocation(input:{taskGroupID:$taskGroupID, position: $position}) {
taskGroupID
id
position
}
}

View File

@ -1,6 +1,6 @@
mutation updateTaskLocation($taskID: String!, $taskGroupID: String!, $position: Float!) {
updateTaskLocation(input: { taskID: $taskID, taskGroupID: $taskGroupID, position: $position }) {
taskID
id
createdAt
name
position

View File

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

View File

@ -0,0 +1,27 @@
import React from 'react';
type Props = {
width: number | string;
height: number | string;
color: string;
};
const AngleDown = ({ width, height, color }: Props) => {
return (
<svg width={width} height={height} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
<path
fill={color}
d="M143 352.3L7 216.3c-9.4-9.4-9.4-24.6 0-33.9l22.6-22.6c9.4-9.4 24.6-9.4 33.9 0l96.4 96.4 96.4-96.4c9.4-9.4 24.6-9.4 33.9 0l22.6 22.6c9.4 9.4 9.4 24.6 0 33.9l-136 136c-9.2 9.4-24.4 9.4-33.8 0z"
/>
</svg>
);
};
AngleDown.defaultProps = {
width: 24,
height: 16,
color: '#000',
};
export default AngleDown;

View File

@ -0,0 +1,24 @@
import React from 'react';
type Props = {
size: number | string;
color: string;
};
const AngleLeft = ({ size, color }: Props) => {
return (
<svg width={size} height={size} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 512">
<path
fill={color}
d="M31.7 239l136-136c9.4-9.4 24.6-9.4 33.9 0l22.6 22.6c9.4 9.4 9.4 24.6 0 33.9L127.9 256l96.4 96.4c9.4 9.4 9.4 24.6 0 33.9L201.7 409c-9.4 9.4-24.6 9.4-33.9 0l-136-136c-9.5-9.4-9.5-24.6-.1-34z"
/>
</svg>
);
};
AngleLeft.defaultProps = {
size: 16,
color: '#000',
};
export default AngleLeft;

View File

@ -7,8 +7,8 @@ type Props = {
const Bell = ({ size, color }: Props) => {
return (
<svg fill={color} xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 16 16">
<path d="M16.023 12.5c0-4.5-4-3.5-4-7 0-0.29-0.028-0.538-0.079-0.749-0.263-1.766-1.44-3.183-2.965-3.615 0.014-0.062 0.021-0.125 0.021-0.191 0-0.52-0.45-0.945-1-0.945s-1 0.425-1 0.945c0 0.065 0.007 0.129 0.021 0.191-1.71 0.484-2.983 2.208-3.020 4.273-0.001 0.030-0.001 0.060-0.001 0.091 0 3.5-4 2.5-4 7 0 1.191 2.665 2.187 6.234 2.439 0.336 0.631 1.001 1.061 1.766 1.061s1.43-0.43 1.766-1.061c3.568-0.251 6.234-1.248 6.234-2.439 0-0.004-0-0.007-0-0.011l0.024 0.011zM12.91 13.345c-0.847 0.226-1.846 0.389-2.918 0.479-0.089-1.022-0.947-1.824-1.992-1.824s-1.903 0.802-1.992 1.824c-1.072-0.090-2.071-0.253-2.918-0.479-1.166-0.311-1.724-0.659-1.928-0.845 0.204-0.186 0.762-0.534 1.928-0.845 1.356-0.362 3.1-0.561 4.91-0.561s3.554 0.199 4.91 0.561c1.166 0.311 1.724 0.659 1.928 0.845-0.204 0.186-0.762 0.534-1.928 0.845z" />
<svg fill={color} xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 448 512">
<path d="M439.39 362.29c-19.32-20.76-55.47-51.99-55.47-154.29 0-77.7-54.48-139.9-127.94-155.16V32c0-17.67-14.32-32-31.98-32s-31.98 14.33-31.98 32v20.84C118.56 68.1 64.08 130.3 64.08 208c0 102.3-36.15 133.53-55.47 154.29-6 6.45-8.66 14.16-8.61 21.71.11 16.4 12.98 32 32.1 32h383.8c19.12 0 32-15.6 32.1-32 .05-7.55-2.61-15.27-8.61-21.71zM67.53 368c21.22-27.97 44.42-74.33 44.53-159.42 0-.2-.06-.38-.06-.58 0-61.86 50.14-112 112-112s112 50.14 112 112c0 .2-.06.38-.06.58.11 85.1 23.31 131.46 44.53 159.42H67.53zM224 512c35.32 0 63.97-28.65 63.97-64H160.03c0 35.35 28.65 64 63.97 64z" />
</svg>
);
};

View File

@ -0,0 +1,24 @@
import React from 'react';
type Props = {
size: number | string;
color: string;
};
const Bolt = ({ size, color }: Props) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" width={size} height={size}>
<path
fill={color}
d="M296 160H180.6l42.6-129.8C227.2 15 215.7 0 200 0H56C44 0 33.8 8.9 32.2 20.8l-32 240C-1.7 275.2 9.5 288 24 288h118.7L96.6 482.5c-3.6 15.2 8 29.5 23.3 29.5 8.4 0 16.4-4.4 20.8-12l176-304c9.3-15.9-2.2-36-20.7-36z"
/>
</svg>
);
};
Bolt.defaultProps = {
size: 16,
color: '#000',
};
export default Bolt;

View File

@ -0,0 +1,24 @@
import React from 'react';
type Props = {
size: number | string;
color: string;
};
const Cog = ({ size, color }: Props) => {
return (
<svg width={size} height={size} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path
fill={color}
d="M487.4 315.7l-42.6-24.6c4.3-23.2 4.3-47 0-70.2l42.6-24.6c4.9-2.8 7.1-8.6 5.5-14-11.1-35.6-30-67.8-54.7-94.6-3.8-4.1-10-5.1-14.8-2.3L380.8 110c-17.9-15.4-38.5-27.3-60.8-35.1V25.8c0-5.6-3.9-10.5-9.4-11.7-36.7-8.2-74.3-7.8-109.2 0-5.5 1.2-9.4 6.1-9.4 11.7V75c-22.2 7.9-42.8 19.8-60.8 35.1L88.7 85.5c-4.9-2.8-11-1.9-14.8 2.3-24.7 26.7-43.6 58.9-54.7 94.6-1.7 5.4.6 11.2 5.5 14L67.3 221c-4.3 23.2-4.3 47 0 70.2l-42.6 24.6c-4.9 2.8-7.1 8.6-5.5 14 11.1 35.6 30 67.8 54.7 94.6 3.8 4.1 10 5.1 14.8 2.3l42.6-24.6c17.9 15.4 38.5 27.3 60.8 35.1v49.2c0 5.6 3.9 10.5 9.4 11.7 36.7 8.2 74.3 7.8 109.2 0 5.5-1.2 9.4-6.1 9.4-11.7v-49.2c22.2-7.9 42.8-19.8 60.8-35.1l42.6 24.6c4.9 2.8 11 1.9 14.8-2.3 24.7-26.7 43.6-58.9 54.7-94.6 1.5-5.5-.7-11.3-5.6-14.1zM256 336c-44.1 0-80-35.9-80-80s35.9-80 80-80 80 35.9 80 80-35.9 80-80 80z"
/>
</svg>
);
};
Cog.defaultProps = {
size: 16,
color: '#000',
};
export default Cog;

View File

@ -7,8 +7,8 @@ type Props = {
const Cross = ({ size, color }: Props) => {
return (
<svg fill={color} xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 16 16">
<path d="M15.854 12.854c-0-0-0-0-0-0l-4.854-4.854 4.854-4.854c0-0 0-0 0-0 0.052-0.052 0.090-0.113 0.114-0.178 0.066-0.178 0.028-0.386-0.114-0.529l-2.293-2.293c-0.143-0.143-0.351-0.181-0.529-0.114-0.065 0.024-0.126 0.062-0.178 0.114 0 0-0 0-0 0l-4.854 4.854-4.854-4.854c-0-0-0-0-0-0-0.052-0.052-0.113-0.090-0.178-0.114-0.178-0.066-0.386-0.029-0.529 0.114l-2.293 2.293c-0.143 0.143-0.181 0.351-0.114 0.529 0.024 0.065 0.062 0.126 0.114 0.178 0 0 0 0 0 0l4.854 4.854-4.854 4.854c-0 0-0 0-0 0-0.052 0.052-0.090 0.113-0.114 0.178-0.066 0.178-0.029 0.386 0.114 0.529l2.293 2.293c0.143 0.143 0.351 0.181 0.529 0.114 0.065-0.024 0.126-0.062 0.178-0.114 0-0 0-0 0-0l4.854-4.854 4.854 4.854c0 0 0 0 0 0 0.052 0.052 0.113 0.090 0.178 0.114 0.178 0.066 0.386 0.029 0.529-0.114l2.293-2.293c0.143-0.143 0.181-0.351 0.114-0.529-0.024-0.065-0.062-0.126-0.114-0.178z" />
<svg fill={color} width={size} height={size} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512">
<path d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z" />
</svg>
);
};

View File

@ -0,0 +1,36 @@
import React from 'react';
type Props = {
width: number | string;
height: number | string;
color: string;
filled: boolean;
};
const Star = ({ width, height, color, filled }: Props) => {
return (
<svg width={width} height={height} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
{filled ? (
<path
fill={color}
d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"
/>
) : (
<path
fill={color}
d="M528.1 171.5L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6zM388.6 312.3l23.7 138.4L288 385.4l-124.3 65.3 23.7-138.4-100.6-98 139-20.2 62.2-126 62.2 126 139 20.2-100.6 98z"
/>
)}
</svg>
);
};
Star.defaultProps = {
width: 24,
height: 16,
color: '#000',
filled: false,
};
export default Star;

View File

@ -0,0 +1,24 @@
import React from 'react';
type Props = {
size: number | string;
color: string;
};
const Tags = ({ size, color }: Props) => {
return (
<svg width={size} height={size} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512">
<path
fill={color}
d="M497.941 225.941L286.059 14.059A48 48 0 0 0 252.118 0H48C21.49 0 0 21.49 0 48v204.118a48 48 0 0 0 14.059 33.941l211.882 211.882c18.744 18.745 49.136 18.746 67.882 0l204.118-204.118c18.745-18.745 18.745-49.137 0-67.882zM112 160c-26.51 0-48-21.49-48-48s21.49-48 48-48 48 21.49 48 48-21.49 48-48 48zm513.941 133.823L421.823 497.941c-18.745 18.745-49.137 18.745-67.882 0l-.36-.36L527.64 323.522c16.999-16.999 26.36-39.6 26.36-63.64s-9.362-46.641-26.36-63.64L331.397 0h48.721a48 48 0 0 1 33.941 14.059l211.882 211.882c18.745 18.745 18.745 49.137 0 67.882z"
/>
</svg>
);
};
Tags.defaultProps = {
size: 16,
color: '#000',
};
export default Tags;

View File

@ -0,0 +1,24 @@
import React from 'react';
type Props = {
size: number | string;
color: string;
};
const ToggleOn = ({ size, color }: Props) => {
return (
<svg width={size} height={size} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
<path
fill={color}
d="M384 64H192C86 64 0 150 0 256s86 192 192 192h192c106 0 192-86 192-192S490 64 384 64zm0 320c-70.8 0-128-57.3-128-128 0-70.8 57.3-128 128-128 70.8 0 128 57.3 128 128 0 70.8-57.3 128-128 128z"
/>
</svg>
);
};
ToggleOn.defaultProps = {
size: 16,
color: '#000',
};
export default ToggleOn;

View File

@ -1,6 +1,10 @@
import Cross from './Cross';
import Cog from './Cog';
import Bolt from './Bolt';
import Plus from './Plus';
import Bell from './Bell';
import AngleLeft from './AngleLeft';
import AngleDown from './AngleDown';
import Bin from './Bin';
import Pencil from './Pencil';
import Checkmark from './Checkmark';
@ -13,5 +17,31 @@ import Stack from './Stack';
import Question from './Question';
import Exit from './Exit';
import Ellipsis from './Ellipsis';
import ToggleOn from './ToggleOn';
import Tags from './Tags';
import Star from './Star';
export { Cross, Plus, Bell, Ellipsis, Bin, Exit, Pencil, Stack, Question, Home, Citadel, Checkmark, User, Users, Lock };
export {
Star,
AngleDown,
Cross,
Cog,
Bolt,
Plus,
Bell,
AngleLeft,
Tags,
Ellipsis,
Bin,
Exit,
Pencil,
Stack,
Question,
Home,
Citadel,
Checkmark,
User,
Users,
Lock,
ToggleOn,
};

View File

@ -2,7 +2,7 @@ import produce from 'immer';
export const addTask = (currentState: BoardState, newTask: Task) => {
return produce(currentState, (draftState: BoardState) => {
currentState.tasks[newTask.taskID] = newTask;
draftState.tasks[newTask.taskID] = newTask;
});
};