feat: add ui skeleton to Task Details while loading

This commit is contained in:
Jordan Knott 2020-12-30 19:12:02 -06:00
parent 90b92781d7
commit f16cceb0e1
4 changed files with 251 additions and 37 deletions

View File

@ -1,6 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import Modal from 'shared/components/Modal'; import Modal from 'shared/components/Modal';
import TaskDetails from 'shared/components/TaskDetails'; import TaskDetails from 'shared/components/TaskDetails';
import TaskDetailsLoading from 'shared/components/TaskDetails/Loading';
import { Popup, usePopup } from 'shared/components/PopupMenu'; import { Popup, usePopup } from 'shared/components/PopupMenu';
import MemberManager from 'shared/components/MemberManager'; import MemberManager from 'shared/components/MemberManager';
import { useRouteMatch, useHistory } from 'react-router'; import { useRouteMatch, useHistory } from 'react-router';
@ -407,9 +408,7 @@ const Details: React.FC<DetailsProps> = ({
}); });
const [updateTaskComment] = useUpdateTaskCommentMutation(); const [updateTaskComment] = useUpdateTaskCommentMutation();
const [editableComment, setEditableComment] = useState<null | string>(null); const [editableComment, setEditableComment] = useState<null | string>(null);
if (!data) { const isLoading = true;
return null;
}
return ( return (
<> <>
<Modal <Modal
@ -418,7 +417,7 @@ const Details: React.FC<DetailsProps> = ({
history.push(projectURL); history.push(projectURL);
}} }}
renderContent={() => { renderContent={() => {
return ( return data ? (
<TaskDetails <TaskDetails
onCancelCommentEdit={() => setEditableComment(null)} onCancelCommentEdit={() => setEditableComment(null)}
onUpdateComment={(commentID, message) => { onUpdateComment={(commentID, message) => {
@ -647,6 +646,8 @@ const Details: React.FC<DetailsProps> = ({
); );
}} }}
/> />
) : (
<TaskDetailsLoading />
); );
}} }}
/> />

View File

@ -1,4 +1,9 @@
import React, { useRef, useState, useEffect } from 'react'; import React, { useRef, useState, useEffect } from 'react';
import { usePopup } from 'shared/components/PopupMenu';
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import { At, Paperclip, Smile } from 'shared/icons';
import { Picker, Emoji } from 'emoji-mart';
import Task from 'shared/icons/Task';
import { import {
CommentTextArea, CommentTextArea,
CommentEditorContainer, CommentEditorContainer,
@ -8,11 +13,6 @@ import {
CommentProfile, CommentProfile,
CommentInnerWrapper, CommentInnerWrapper,
} from './Styles'; } from './Styles';
import { usePopup } from 'shared/components/PopupMenu';
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import { At, Paperclip, Smile } from 'shared/icons';
import { Picker, Emoji } from 'emoji-mart';
import Task from 'shared/icons/Task';
type CommentCreatorProps = { type CommentCreatorProps = {
me?: TaskUser; me?: TaskUser;
@ -21,10 +21,12 @@ type CommentCreatorProps = {
message?: string | null; message?: string | null;
onCreateComment: (message: string) => void; onCreateComment: (message: string) => void;
onCancelEdit?: () => void; onCancelEdit?: () => void;
disabled?: boolean;
}; };
const CommentCreator: React.FC<CommentCreatorProps> = ({ const CommentCreator: React.FC<CommentCreatorProps> = ({
me, me,
disabled = false,
message, message,
onMemberProfile, onMemberProfile,
onCreateComment, onCreateComment,
@ -70,6 +72,7 @@ const CommentCreator: React.FC<CommentCreatorProps> = ({
showCommentActions={showCommentActions} showCommentActions={showCommentActions}
placeholder="Write a comment..." placeholder="Write a comment..."
ref={$comment} ref={$comment}
disabled={disabled}
value={comment} value={comment}
onChange={e => setComment(e.currentTarget.value)} onChange={e => setComment(e.currentTarget.value)}
onFocus={() => { onFocus={() => {
@ -91,12 +94,11 @@ const CommentCreator: React.FC<CommentCreatorProps> = ({
<div ref={$emojiCart}> <div ref={$emojiCart}>
<Picker <Picker
onClick={emoji => { onClick={emoji => {
console.log(emoji);
if ($comment && $comment.current) { if ($comment && $comment.current) {
let textToInsert = `${emoji.colons} `; const textToInsert = `${emoji.colons} `;
let cursorPosition = $comment.current.selectionStart; const cursorPosition = $comment.current.selectionStart;
let textBeforeCursorPosition = $comment.current.value.substring(0, cursorPosition); const textBeforeCursorPosition = $comment.current.value.substring(0, cursorPosition);
let textAfterCursorPosition = $comment.current.value.substring( const textAfterCursorPosition = $comment.current.value.substring(
cursorPosition, cursorPosition,
$comment.current.value.length, $comment.current.value.length,
); );

View File

@ -0,0 +1,162 @@
import React, { useState, useRef } from 'react';
import {
Plus,
User,
Trash,
Paperclip,
Clone,
Share,
Tags,
Checkmark,
CheckSquareOutline,
At,
Smile,
} from 'shared/icons';
import { toArray } from 'react-emoji-render';
import DOMPurify from 'dompurify';
import TaskAssignee from 'shared/components/TaskAssignee';
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import { usePopup } from 'shared/components/PopupMenu';
import CommentCreator from 'shared/components/TaskDetails/CommentCreator';
import { AngleDown } from 'shared/icons/AngleDown';
import Editor from 'rich-markdown-editor';
import dark from 'shared/utils/editorTheme';
import styled from 'styled-components';
import ReactMarkdown from 'react-markdown';
import { Picker, Emoji } from 'emoji-mart';
import 'emoji-mart/css/emoji-mart.css';
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
import dayjs from 'dayjs';
import Task from 'shared/icons/Task';
import {
ActivityItemHeader,
ActivityItemTimestamp,
ActivityItem,
ActivityItemCommentAction,
ActivityItemCommentActions,
TaskDetailLabel,
CommentContainer,
ActivityItemCommentContainer,
MetaDetailContent,
TaskDetailsAddLabelIcon,
ActionButton,
AssignUserIcon,
AssignUserLabel,
AssignUsersButton,
AssignedUsersSection,
ViewRawButton,
DueDateTitle,
Container,
LeftSidebar,
SidebarSkeleton,
ContentContainer,
LeftSidebarContent,
LeftSidebarSection,
SidebarTitle,
SidebarButton,
SidebarButtonText,
MarkCompleteButton,
HeaderContainer,
HeaderLeft,
HeaderInnerContainer,
TaskDetailsTitleWrapper,
TaskDetailsTitle,
ExtraActionsSection,
HeaderRight,
HeaderActionIcon,
EditorContainer,
InnerContentContainer,
DescriptionContainer,
Labels,
ChecklistSection,
MemberList,
TaskMember,
TabBarSection,
TabBarItem,
ActivitySection,
TaskDetailsEditor,
ActivityItemHeaderUser,
ActivityItemHeaderTitle,
ActivityItemHeaderTitleName,
ActivityItemComment,
} from './Styles';
type TaskDetailsProps = {};
const TaskDetailsLoading: React.FC<TaskDetailsProps> = () => {
return (
<Container>
<LeftSidebar>
<LeftSidebarContent>
<LeftSidebarSection>
<SidebarTitle>TASK GROUP</SidebarTitle>
<SidebarButton loading>
<SidebarSkeleton />
</SidebarButton>
<DueDateTitle>DUE DATE</DueDateTitle>
<SidebarButton loading>
<SidebarSkeleton />
</SidebarButton>
</LeftSidebarSection>
<AssignedUsersSection>
<DueDateTitle>MEMBERS</DueDateTitle>
<SidebarButton loading>
<SidebarSkeleton />
</SidebarButton>
</AssignedUsersSection>
<ExtraActionsSection>
<DueDateTitle>ACTIONS</DueDateTitle>
<ActionButton disabled icon={<Tags width={12} height={12} />}>
Labels
</ActionButton>
<ActionButton disabled icon={<CheckSquareOutline width={12} height={12} />}>
Checklist
</ActionButton>
<ActionButton disabled>Cover</ActionButton>
</ExtraActionsSection>
</LeftSidebarContent>
</LeftSidebar>
<ContentContainer>
<HeaderContainer>
<HeaderInnerContainer>
<HeaderLeft>
<MarkCompleteButton disabled invert={false}>
<Checkmark width={8} height={8} />
<span>Mark complete</span>
</MarkCompleteButton>
</HeaderLeft>
<HeaderRight>
<HeaderActionIcon>
<Paperclip width={16} height={16} />
</HeaderActionIcon>
<HeaderActionIcon>
<Clone width={16} height={16} />
</HeaderActionIcon>
<HeaderActionIcon>
<Share width={16} height={16} />
</HeaderActionIcon>
<HeaderActionIcon>
<Trash width={16} height={16} />
</HeaderActionIcon>
</HeaderRight>
</HeaderInnerContainer>
<TaskDetailsTitleWrapper loading>
<TaskDetailsTitle value="" disabled loading />
</TaskDetailsTitleWrapper>
</HeaderContainer>
<InnerContentContainer>
<DescriptionContainer />
<TabBarSection>
<TabBarItem>Activity</TabBarItem>
</TabBarSection>
<ActivitySection />
</InnerContentContainer>
<CommentContainer>
<CommentCreator disabled onCreateComment={() => null} onMemberProfile={() => null} />
</CommentContainer>
</ContentContainer>
</Container>
);
};
export default TaskDetailsLoading;

View File

@ -1,8 +1,9 @@
import styled, { css } from 'styled-components'; import styled, { css, keyframes } from 'styled-components';
import TextareaAutosize from 'react-autosize-textarea'; import TextareaAutosize from 'react-autosize-textarea';
import { mixin } from 'shared/utils/styles'; import { mixin } from 'shared/utils/styles';
import Button from 'shared/components/Button'; import Button from 'shared/components/Button';
import TaskAssignee from 'shared/components/TaskAssignee'; import TaskAssignee from 'shared/components/TaskAssignee';
import theme from 'App/ThemeStyles';
export const Container = styled.div` export const Container = styled.div`
display: flex; display: flex;
@ -16,7 +17,7 @@ export const LeftSidebar = styled.div`
background: #222740; background: #222740;
`; `;
export const MarkCompleteButton = styled.button<{ invert: boolean }>` export const MarkCompleteButton = styled.button<{ invert: boolean; disabled?: boolean }>`
padding: 4px 8px; padding: 4px 8px;
position: relative; position: relative;
border: none; border: none;
@ -62,6 +63,11 @@ export const MarkCompleteButton = styled.button<{ invert: boolean }>`
color: ${props.theme.colors.success}; color: ${props.theme.colors.success};
} }
`} `}
${props =>
props.invert &&
css`
opacity: 0.6;
`}
`; `;
export const LeftSidebarContent = styled.div` export const LeftSidebarContent = styled.div`
@ -89,24 +95,55 @@ export const SidebarTitle = styled.div`
text-transform: uppercase; text-transform: uppercase;
`; `;
export const SidebarButton = styled.div` export const defaultBaseColor = theme.colors.bg.primary;
export const defaultHighlightColor = mixin.lighten(theme.colors.bg.primary, 0.25);
export const skeletonKeyframes = keyframes`
0% {
background-position: -200px 0;
}
100% {
background-position: calc(200px + 100%) 0;
}
`;
export const SidebarButton = styled.div<{ loading?: boolean }>`
font-size: 14px; font-size: 14px;
color: ${props => props.theme.colors.text.primary}; color: ${props => props.theme.colors.text.primary};
min-height: 32px; min-height: 32px;
width: 100%; width: 100%;
padding: 9px 8px 7px 8px;
border-color: transparent;
border-radius: 6px; border-radius: 6px;
border-width: 1px;
border-style: solid; ${props =>
props.loading
? css`
background: ${props.theme.colors.bg.primary};
`
: css`
padding: 9px 8px 7px 8px;
cursor: pointer;
border-color: transparent;
border-width: 1px;
border-style: solid;
&:hover {
border-color: #414561;
}
`};
display: inline-block; display: inline-block;
outline: 0; outline: 0;
cursor: pointer; `;
&:hover {
border-color: #414561; export const SidebarSkeleton = styled.div`
} background-image: linear-gradient(90deg, ${defaultBaseColor}, ${defaultHighlightColor}, ${defaultBaseColor});
background-size: 200px 100%;
background-repeat: no-repeat;
border-radius: 6px;
padding: 1px;
animation: ${skeletonKeyframes} 1.2s ease-in-out infinite;
width: 100%;
height: 100%;
`; `;
export const SidebarButtonText = styled.span` export const SidebarButtonText = styled.span`
@ -141,18 +178,18 @@ export const HeaderLeft = styled.div`
justify-content: flex-start; justify-content: flex-start;
`; `;
export const TaskDetailsTitleWrapper = styled.div` export const TaskDetailsTitleWrapper = styled.div<{ loading?: boolean }>`
width: 100%; width: 100%;
margin: 8px 0 4px 0; margin: 8px 0 4px 0;
display: inline-block; display: flex;
border-radius: 6px;
${props => props.loading && `background: ${props.theme.colors.bg.primary};`}
`; `;
export const TaskDetailsTitle = styled(TextareaAutosize)` export const TaskDetailsTitle = styled(TextareaAutosize)<{ loading?: boolean }>`
padding: 9px 8px 7px 8px; padding: 9px 8px 7px 8px;
border-color: transparent; border-color: transparent;
border-radius: 6px; border-radius: 6px;
border-width: 1px;
border-style: solid;
width: 100%; width: 100%;
color: #c2c6dc; color: #c2c6dc;
display: inline-block; display: inline-block;
@ -161,13 +198,25 @@ export const TaskDetailsTitle = styled(TextareaAutosize)`
font-weight: 700; font-weight: 700;
background: none; background: none;
&:hover { ${props =>
border-color: #414561; props.loading
} ? css`
background-image: linear-gradient(90deg, ${defaultBaseColor}, ${defaultHighlightColor}, ${defaultBaseColor});
background-size: 200px 100%;
background-repeat: no-repeat;
animation: ${skeletonKeyframes} 1.2s ease-in-out infinite;
`
: css`
&:hover {
border-color: #414561;
border-width: 1px;
border-style: solid;
}
&:focus { &:focus {
border-color: ${props => props.theme.colors.primary}; border-color: ${props.theme.colors.primary};
} }
`}
`; `;
export const DueDateTitle = styled.div` export const DueDateTitle = styled.div`