change: add loading state to project board

This commit is contained in:
Jordan Knott 2020-07-16 21:14:26 -05:00
parent a90ace7a06
commit 45a92636cb
5 changed files with 207 additions and 46 deletions

View File

@ -2,9 +2,9 @@ import React, { useState, useRef, useContext, useEffect } from 'react';
import { MENU_TYPES } from 'shared/components/TopNavbar'; import { MENU_TYPES } from 'shared/components/TopNavbar';
import updateApolloCache from 'shared/utils/cache'; import updateApolloCache from 'shared/utils/cache';
import GlobalTopNavbar, { ProjectPopup } from 'App/TopNavbar'; import GlobalTopNavbar, { ProjectPopup } from 'App/TopNavbar';
import LabelManagerEditor from '../LabelManagerEditor';
import styled, { css } from 'styled-components/macro'; import styled, { css } from 'styled-components/macro';
import { Bolt, ToggleOn, Tags, CheckCircle, Sort, Filter } from 'shared/icons'; import { Bolt, ToggleOn, Tags, CheckCircle, Sort, Filter } from 'shared/icons';
import LabelManagerEditor from '../LabelManagerEditor';
import { usePopup, Popup } from 'shared/components/PopupMenu'; import { usePopup, Popup } from 'shared/components/PopupMenu';
import { useParams, Route, useRouteMatch, useHistory, RouteComponentProps, useLocation } from 'react-router-dom'; import { useParams, Route, useRouteMatch, useHistory, RouteComponentProps, useLocation } from 'react-router-dom';
import { import {
@ -47,6 +47,7 @@ import DueDateManager from 'shared/components/DueDateManager';
import UserIDContext from 'App/context'; import UserIDContext from 'App/context';
import LabelManager from 'shared/components/PopupMenu/LabelManager'; import LabelManager from 'shared/components/PopupMenu/LabelManager';
import LabelEditor from 'shared/components/PopupMenu/LabelEditor'; import LabelEditor from 'shared/components/PopupMenu/LabelEditor';
import EmptyBoard from 'shared/components/EmptyBoard';
const ProjectBar = styled.div` const ProjectBar = styled.div`
display: flex; display: flex;
@ -103,12 +104,18 @@ const initialQuickCardEditorState: QuickCardEditorState = {
}; };
type ProjectBoardProps = { type ProjectBoardProps = {
onCardLabelClick: () => void; onCardLabelClick?: () => void;
cardLabelVariant: CardLabelVariant; cardLabelVariant?: CardLabelVariant;
projectID: string; projectID?: string;
loading?: boolean;
}; };
const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick, cardLabelVariant }) => { const ProjectBoard: React.FC<ProjectBoardProps> = ({
projectID,
onCardLabelClick,
cardLabelVariant,
loading: isLoading = false,
}) => {
const [assignTask] = useAssignTaskMutation(); const [assignTask] = useAssignTaskMutation();
const [unassignTask] = useUnassignTaskMutation(); const [unassignTask] = useUnassignTaskMutation();
const $labelsRef = useRef<HTMLDivElement>(null); const $labelsRef = useRef<HTMLDivElement>(null);
@ -170,7 +177,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
const [updateTaskGroupName] = useUpdateTaskGroupNameMutation({}); const [updateTaskGroupName] = useUpdateTaskGroupNameMutation({});
const { loading, data } = useFindProjectQuery({ const { loading, data } = useFindProjectQuery({
variables: { projectId: projectID }, variables: { projectId: projectID ?? '' },
}); });
const [updateTaskDueDate] = useUpdateTaskDueDateMutation(); const [updateTaskDueDate] = useUpdateTaskDueDateMutation();
@ -256,7 +263,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
}; };
const onCreateList = (listName: string) => { const onCreateList = (listName: string) => {
if (data) { if (data && projectID) {
const [lastColumn] = data.findProject.taskGroups.sort((a, b) => a.position - b.position).slice(-1); const [lastColumn] = data.findProject.taskGroups.sort((a, b) => a.position - b.position).slice(-1);
let position = 65535; let position = 65535;
if (lastColumn) { if (lastColumn) {
@ -266,8 +273,42 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
} }
}; };
if (loading) { if (loading || isLoading) {
return <span>loading</span>; 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 (data) { if (data) {
labelsRef.current = data.findProject.labels; labelsRef.current = data.findProject.labels;
@ -323,7 +364,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
taskLabels={null} taskLabels={null}
labelColors={data.labelColors} labelColors={data.labelColors}
labels={labelsRef} labels={labelsRef}
projectID={projectID} projectID={projectID ?? ''}
/>, />,
); );
}} }}
@ -345,8 +386,8 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
onTaskClick={task => { onTaskClick={task => {
history.push(`${match.url}/c/${task.id}`); history.push(`${match.url}/c/${task.id}`);
}} }}
onCardLabelClick={onCardLabelClick} onCardLabelClick={onCardLabelClick ?? (() => {})}
cardLabelVariant={cardLabelVariant} cardLabelVariant={cardLabelVariant ?? 'large'}
onTaskDrop={(droppedTask, previousTaskGroupID) => { onTaskDrop={(droppedTask, previousTaskGroupID) => {
updateTaskLocation({ updateTaskLocation({
variables: { variables: {
@ -475,7 +516,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
labelColors={data.labelColors} labelColors={data.labelColors}
labels={labelsRef} labels={labelsRef}
taskLabels={taskLabelsRef} taskLabels={taskLabelsRef}
projectID={projectID} projectID={projectID ?? ''}
/>, />,
); );
}} }}

View File

@ -39,6 +39,7 @@ import Input from 'shared/components/Input';
import Member from 'shared/components/Member'; import Member from 'shared/components/Member';
import Board from './Board'; import Board from './Board';
import Details from './Details'; import Details from './Details';
import EmptyBoard from 'shared/components/EmptyBoard';
const CARD_LABEL_VARIANT_STORAGE_KEY = 'card_label_variant'; const CARD_LABEL_VARIANT_STORAGE_KEY = 'card_label_variant';
@ -202,6 +203,7 @@ const Project = () => {
return ( return (
<> <>
<GlobalTopNavbar onSaveProjectName={projectName => {}} name="" projectID={null} /> <GlobalTopNavbar onSaveProjectName={projectName => {}} name="" projectID={null} />
<Board loading />
</> </>
); );
} }

View File

@ -1,11 +1,11 @@
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import styled, { css } from 'styled-components/macro'; import styled, { css } from 'styled-components/macro';
const Text = styled.span<{ fontSize: string }>` const Text = styled.span<{ fontSize: string; justifyTextContent: string }>`
position: relative; position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: ${props => props.justifyTextContent};
transition: all 0.2s ease; transition: all 0.2s ease;
font-size: ${props => props.fontSize}; font-size: ${props => props.fontSize};
color: rgba(${props => props.theme.colors.text.secondary}); color: rgba(${props => props.theme.colors.text.secondary});
@ -112,6 +112,7 @@ type ButtonProps = {
type?: 'button' | 'submit'; type?: 'button' | 'submit';
className?: string; className?: string;
onClick?: ($target: React.RefObject<HTMLButtonElement>) => void; onClick?: ($target: React.RefObject<HTMLButtonElement>) => void;
justifyTextContent?: string;
}; };
const Button: React.FC<ButtonProps> = ({ const Button: React.FC<ButtonProps> = ({
@ -120,6 +121,7 @@ const Button: React.FC<ButtonProps> = ({
color = 'primary', color = 'primary',
variant = 'filled', variant = 'filled',
type = 'button', type = 'button',
justifyTextContent = 'center',
onClick, onClick,
className, className,
children, children,
@ -134,7 +136,9 @@ const Button: React.FC<ButtonProps> = ({
case 'filled': case 'filled':
return ( return (
<Filled ref={$button} type={type} onClick={handleClick} className={className} disabled={disabled} color={color}> <Filled ref={$button} type={type} onClick={handleClick} className={className} disabled={disabled} color={color}>
<Text fontSize={fontSize}>{children}</Text> <Text justifyTextContent={justifyTextContent} fontSize={fontSize}>
{children}
</Text>
</Filled> </Filled>
); );
case 'outline': case 'outline':
@ -147,13 +151,17 @@ const Button: React.FC<ButtonProps> = ({
disabled={disabled} disabled={disabled}
color={color} color={color}
> >
<Text fontSize={fontSize}>{children}</Text> <Text justifyTextContent={justifyTextContent} fontSize={fontSize}>
{children}
</Text>
</Outline> </Outline>
); );
case 'flat': case 'flat':
return ( return (
<Flat ref={$button} type={type} onClick={handleClick} className={className} disabled={disabled} color={color}> <Flat ref={$button} type={type} onClick={handleClick} className={className} disabled={disabled} color={color}>
<Text fontSize={fontSize}>{children}</Text> <Text justifyTextContent={justifyTextContent} fontSize={fontSize}>
{children}
</Text>
</Flat> </Flat>
); );
case 'lineDown': case 'lineDown':
@ -166,7 +174,9 @@ const Button: React.FC<ButtonProps> = ({
disabled={disabled} disabled={disabled}
color={color} color={color}
> >
<Text fontSize={fontSize}>{children}</Text> <Text justifyTextContent={justifyTextContent} fontSize={fontSize}>
{children}
</Text>
<LineX color={color} /> <LineX color={color} />
</LineDown> </LineDown>
); );
@ -180,13 +190,17 @@ const Button: React.FC<ButtonProps> = ({
disabled={disabled} disabled={disabled}
color={color} color={color}
> >
<Text fontSize={fontSize}>{children}</Text> <Text justifyTextContent={justifyTextContent} fontSize={fontSize}>
{children}
</Text>
</Gradient> </Gradient>
); );
case 'relief': case 'relief':
return ( return (
<Relief ref={$button} type={type} onClick={handleClick} className={className} disabled={disabled} color={color}> <Relief ref={$button} type={type} onClick={handleClick} className={className} disabled={disabled} color={color}>
<Text fontSize={fontSize}>{children}</Text> <Text justifyTextContent={justifyTextContent} fontSize={fontSize}>
{children}
</Text>
</Relief> </Relief>
); );
default: default:

View File

@ -0,0 +1,94 @@
import React from 'react';
import styled, { keyframes } from 'styled-components/macro';
import { mixin } from 'shared/utils/styles';
export const BoardContainer = styled.div`
position: relative;
overflow-y: auto;
outline: none;
flex-grow: 1;
`;
export const BoardWrapper = styled.div`
display: flex;
user-select: none;
white-space: nowrap;
margin-bottom: 8px;
overflow-x: auto;
overflow-y: hidden;
padding-bottom: 8px;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
`;
export const Container = styled.div`
width: 272px;
margin: 0 4px;
height: 100%;
box-sizing: border-box;
display: inline-block;
vertical-align: top;
white-space: nowrap;
`;
export const defaultBaseColor = '#10163a';
export const defaultHighlightColor = mixin.lighten('#10163a', 0.25);
export const skeletonKeyframes = keyframes`
0% {
background-position: -200px 0;
}
100% {
background-position: calc(200px + 100%) 0;
}
`;
export const Wrapper = styled.div`
// background-color: #ebecf0;
// background: rgb(244, 245, 247);
min-height: 120px;
opacity: 0.8;
background: #10163a;
color: #c2c6dc;
border-radius: 5px;
box-sizing: border-box;
display: flex;
flex-direction: column;
max-height: 100%;
position: relative;
white-space: normal;
background-image: linear-gradient(90deg, ${defaultBaseColor}, ${defaultHighlightColor}, ${defaultBaseColor});
background-size: 200px 100%;
background-repeat: no-repeat;
animation: ${skeletonKeyframes} 1.2s ease-in-out infinite;
`;
const EmptyBoard: React.FC = () => {
return (
<BoardContainer>
<BoardWrapper>
<Container>
<Wrapper />
</Container>
<Container>
<Wrapper />
</Container>
<Container>
<Wrapper />
</Container>
<Container>
<Wrapper />
</Container>
</BoardWrapper>
</BoardContainer>
);
};
export default EmptyBoard;

View File

@ -1,5 +1,5 @@
import React, {useState, useRef, useEffect} from 'react'; import React, { useState, useRef, useEffect } from 'react';
import {Bin, Cross, Plus} from 'shared/icons'; import { Bin, Cross, Plus } from 'shared/icons';
import useOnOutsideClick from 'shared/hooks/onOutsideClick'; import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
@ -9,7 +9,7 @@ import {
getNewDraggablePosition, getNewDraggablePosition,
getAfterDropDraggableList, getAfterDropDraggableList,
} from 'shared/utils/draggables'; } from 'shared/utils/draggables';
import {DragDropContext, Droppable, Draggable, DropResult} from 'react-beautiful-dnd'; import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd';
import TaskAssignee from 'shared/components/TaskAssignee'; import TaskAssignee from 'shared/components/TaskAssignee';
import moment from 'moment'; import moment from 'moment';
@ -54,7 +54,7 @@ import {
MetaDetailTitle, MetaDetailTitle,
MetaDetailContent, MetaDetailContent,
} from './Styles'; } from './Styles';
import Checklist, {ChecklistItem, ChecklistItems} from '../Checklist'; import Checklist, { ChecklistItem, ChecklistItems } from '../Checklist';
import styled from 'styled-components'; import styled from 'styled-components';
const ChecklistContainer = styled.div``; const ChecklistContainer = styled.div``;
@ -69,7 +69,7 @@ type TaskLabelProps = {
onClick: ($target: React.RefObject<HTMLElement>) => void; onClick: ($target: React.RefObject<HTMLElement>) => void;
}; };
const TaskLabelItem: React.FC<TaskLabelProps> = ({label, onClick}) => { const TaskLabelItem: React.FC<TaskLabelProps> = ({ label, onClick }) => {
const $label = useRef<HTMLDivElement>(null); const $label = useRef<HTMLDivElement>(null);
return ( return (
<TaskDetailLabel <TaskDetailLabel
@ -84,7 +84,7 @@ const TaskLabelItem: React.FC<TaskLabelProps> = ({label, onClick}) => {
); );
}; };
const TaskContent: React.FC<TaskContentProps> = ({description, onEditContent}) => { const TaskContent: React.FC<TaskContentProps> = ({ description, onEditContent }) => {
return description === '' ? ( return description === '' ? (
<TaskDetailsAddDetailsButton onClick={onEditContent}>Add a more detailed description</TaskDetailsAddDetailsButton> <TaskDetailsAddDetailsButton onClick={onEditContent}>Add a more detailed description</TaskDetailsAddDetailsButton>
) : ( ) : (
@ -214,7 +214,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
onOpenAddLabelPopup(task, $target); onOpenAddLabelPopup(task, $target);
}; };
const onDragEnd = ({draggableId, source, destination, type}: DropResult) => { const onDragEnd = ({ draggableId, source, destination, type }: DropResult) => {
if (typeof destination === 'undefined') return; if (typeof destination === 'undefined') return;
if (!isPositionChanged(source, destination)) return; if (!isPositionChanged(source, destination)) return;
@ -233,7 +233,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
}; };
beforeDropDraggables = getSortedDraggables( beforeDropDraggables = getSortedDraggables(
task.checklists.map(checklist => { task.checklists.map(checklist => {
return {id: checklist.id, position: checklist.position}; return { id: checklist.id, position: checklist.position };
}), }),
); );
if (droppedDraggable === null || beforeDropDraggables === null) { if (droppedDraggable === null || beforeDropDraggables === null) {
@ -249,9 +249,9 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
const newPosition = getNewDraggablePosition(afterDropDraggables, destination.index); const newPosition = getNewDraggablePosition(afterDropDraggables, destination.index);
console.log(droppedGroup); console.log(droppedGroup);
console.log(`positiion: ${newPosition}`); console.log(`positiion: ${newPosition}`);
onChecklistDrop({...droppedGroup, position: newPosition}); onChecklistDrop({ ...droppedGroup, position: newPosition });
} else { } else {
throw {error: 'task group can not be found'}; throw { error: 'task group can not be found' };
} }
} else { } else {
const targetChecklist = task.checklists.findIndex( const targetChecklist = task.checklists.findIndex(
@ -266,7 +266,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
}; };
beforeDropDraggables = getSortedDraggables( beforeDropDraggables = getSortedDraggables(
task.checklists[targetChecklist].items.map(item => { task.checklists[targetChecklist].items.map(item => {
return {id: item.id, position: item.position}; return { id: item.id, position: item.position };
}), }),
); );
if (droppedDraggable === null || beforeDropDraggables === null) { if (droppedDraggable === null || beforeDropDraggables === null) {
@ -438,7 +438,9 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
complete={item.complete} complete={item.complete}
onDeleteItem={onDeleteItem} onDeleteItem={onDeleteItem}
onChangeName={onChangeItemName} onChangeName={onChangeItemName}
onToggleItem={(itemID, complete) => onToggleChecklistItem(item.id, complete)} onToggleItem={(itemID, complete) =>
onToggleChecklistItem(item.id, complete)
}
/> />
)} )}
</Draggable> </Draggable>
@ -461,15 +463,23 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
<TaskDetailsSidebar> <TaskDetailsSidebar>
<ActionButtons> <ActionButtons>
<ActionButtonsTitle>ADD TO CARD</ActionButtonsTitle> <ActionButtonsTitle>ADD TO CARD</ActionButtonsTitle>
<ActionButton onClick={() => onToggleTaskComplete(task)}> <ActionButton justifyTextContent="flex-start" onClick={() => onToggleTaskComplete(task)}>
{task.complete ? 'Mark Incomplete' : 'Mark Complete'} {task.complete ? 'Mark Incomplete' : 'Mark Complete'}
</ActionButton> </ActionButton>
<ActionButton onClick={$target => onAddMember($target)}>Members</ActionButton> <ActionButton justifyTextContent="flex-start" onClick={$target => onAddMember($target)}>
<ActionButton onClick={$target => onAddLabel($target)}>Labels</ActionButton> Members
<ActionButton onClick={$target => onAddChecklist($target)}>Checklist</ActionButton> </ActionButton>
<ActionButton onClick={$target => onOpenDueDatePopop(task, $target)}>Due Date</ActionButton> <ActionButton justifyTextContent="flex-start" onClick={$target => onAddLabel($target)}>
<ActionButton>Attachment</ActionButton> Labels
<ActionButton>Cover</ActionButton> </ActionButton>
<ActionButton justifyTextContent="flex-start" onClick={$target => onAddChecklist($target)}>
Checklist
</ActionButton>
<ActionButton justifyTextContent="flex-start" onClick={$target => onOpenDueDatePopop(task, $target)}>
Due Date
</ActionButton>
<ActionButton justifyTextContent="flex-start">Attachment</ActionButton>
<ActionButton justifyTextContent="flex-start">Cover</ActionButton>
</ActionButtons> </ActionButtons>
</TaskDetailsSidebar> </TaskDetailsSidebar>
</TaskDetailsWrapper> </TaskDetailsWrapper>