feat: add ui skeleton to Task Details while loading
This commit is contained in:
parent
90b92781d7
commit
f16cceb0e1
@ -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 />
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
|
162
frontend/src/shared/components/TaskDetails/Loading.tsx
Normal file
162
frontend/src/shared/components/TaskDetails/Loading.tsx
Normal 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;
|
@ -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;
|
||||||
|
|
||||||
|
${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-width: 1px;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
|
|
||||||
display: inline-block;
|
|
||||||
outline: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: #414561;
|
border-color: #414561;
|
||||||
}
|
}
|
||||||
|
`};
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
outline: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
${props =>
|
||||||
|
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 {
|
&:hover {
|
||||||
border-color: #414561;
|
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`
|
||||||
|
Loading…
Reference in New Issue
Block a user