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 Modal from 'shared/components/Modal';
|
||||
import TaskDetails from 'shared/components/TaskDetails';
|
||||
import TaskDetailsLoading from 'shared/components/TaskDetails/Loading';
|
||||
import { Popup, usePopup } from 'shared/components/PopupMenu';
|
||||
import MemberManager from 'shared/components/MemberManager';
|
||||
import { useRouteMatch, useHistory } from 'react-router';
|
||||
@ -407,9 +408,7 @@ const Details: React.FC<DetailsProps> = ({
|
||||
});
|
||||
const [updateTaskComment] = useUpdateTaskCommentMutation();
|
||||
const [editableComment, setEditableComment] = useState<null | string>(null);
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
const isLoading = true;
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
@ -418,7 +417,7 @@ const Details: React.FC<DetailsProps> = ({
|
||||
history.push(projectURL);
|
||||
}}
|
||||
renderContent={() => {
|
||||
return (
|
||||
return data ? (
|
||||
<TaskDetails
|
||||
onCancelCommentEdit={() => setEditableComment(null)}
|
||||
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 { 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 {
|
||||
CommentTextArea,
|
||||
CommentEditorContainer,
|
||||
@ -8,11 +13,6 @@ import {
|
||||
CommentProfile,
|
||||
CommentInnerWrapper,
|
||||
} 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 = {
|
||||
me?: TaskUser;
|
||||
@ -21,10 +21,12 @@ type CommentCreatorProps = {
|
||||
message?: string | null;
|
||||
onCreateComment: (message: string) => void;
|
||||
onCancelEdit?: () => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const CommentCreator: React.FC<CommentCreatorProps> = ({
|
||||
me,
|
||||
disabled = false,
|
||||
message,
|
||||
onMemberProfile,
|
||||
onCreateComment,
|
||||
@ -70,6 +72,7 @@ const CommentCreator: React.FC<CommentCreatorProps> = ({
|
||||
showCommentActions={showCommentActions}
|
||||
placeholder="Write a comment..."
|
||||
ref={$comment}
|
||||
disabled={disabled}
|
||||
value={comment}
|
||||
onChange={e => setComment(e.currentTarget.value)}
|
||||
onFocus={() => {
|
||||
@ -91,12 +94,11 @@ const CommentCreator: React.FC<CommentCreatorProps> = ({
|
||||
<div ref={$emojiCart}>
|
||||
<Picker
|
||||
onClick={emoji => {
|
||||
console.log(emoji);
|
||||
if ($comment && $comment.current) {
|
||||
let textToInsert = `${emoji.colons} `;
|
||||
let cursorPosition = $comment.current.selectionStart;
|
||||
let textBeforeCursorPosition = $comment.current.value.substring(0, cursorPosition);
|
||||
let textAfterCursorPosition = $comment.current.value.substring(
|
||||
const textToInsert = `${emoji.colons} `;
|
||||
const cursorPosition = $comment.current.selectionStart;
|
||||
const textBeforeCursorPosition = $comment.current.value.substring(0, cursorPosition);
|
||||
const textAfterCursorPosition = $comment.current.value.substring(
|
||||
cursorPosition,
|
||||
$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 { mixin } from 'shared/utils/styles';
|
||||
import Button from 'shared/components/Button';
|
||||
import TaskAssignee from 'shared/components/TaskAssignee';
|
||||
import theme from 'App/ThemeStyles';
|
||||
|
||||
export const Container = styled.div`
|
||||
display: flex;
|
||||
@ -16,7 +17,7 @@ export const LeftSidebar = styled.div`
|
||||
background: #222740;
|
||||
`;
|
||||
|
||||
export const MarkCompleteButton = styled.button<{ invert: boolean }>`
|
||||
export const MarkCompleteButton = styled.button<{ invert: boolean; disabled?: boolean }>`
|
||||
padding: 4px 8px;
|
||||
position: relative;
|
||||
border: none;
|
||||
@ -62,6 +63,11 @@ export const MarkCompleteButton = styled.button<{ invert: boolean }>`
|
||||
color: ${props.theme.colors.success};
|
||||
}
|
||||
`}
|
||||
${props =>
|
||||
props.invert &&
|
||||
css`
|
||||
opacity: 0.6;
|
||||
`}
|
||||
`;
|
||||
|
||||
export const LeftSidebarContent = styled.div`
|
||||
@ -89,24 +95,55 @@ export const SidebarTitle = styled.div`
|
||||
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;
|
||||
color: ${props => props.theme.colors.text.primary};
|
||||
min-height: 32px;
|
||||
width: 100%;
|
||||
|
||||
padding: 9px 8px 7px 8px;
|
||||
border-color: transparent;
|
||||
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-style: solid;
|
||||
|
||||
display: inline-block;
|
||||
outline: 0;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
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`
|
||||
@ -141,18 +178,18 @@ export const HeaderLeft = styled.div`
|
||||
justify-content: flex-start;
|
||||
`;
|
||||
|
||||
export const TaskDetailsTitleWrapper = styled.div`
|
||||
export const TaskDetailsTitleWrapper = styled.div<{ loading?: boolean }>`
|
||||
width: 100%;
|
||||
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;
|
||||
border-color: transparent;
|
||||
border-radius: 6px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
width: 100%;
|
||||
color: #c2c6dc;
|
||||
display: inline-block;
|
||||
@ -161,13 +198,25 @@ export const TaskDetailsTitle = styled(TextareaAutosize)`
|
||||
font-weight: 700;
|
||||
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 {
|
||||
border-color: #414561;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: ${props => props.theme.colors.primary};
|
||||
border-color: ${props.theme.colors.primary};
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
export const DueDateTitle = styled.div`
|
||||
|
Loading…
Reference in New Issue
Block a user