PromucFlow_constructor/app/client/src/comments/CommentCard/CommentCard.tsx
Pawan Kumar 809a633306
feat: App Theming (#9714)
* fix style bugs

* fix select styles

* test: fix font size issue for cypress tests

* incorporate ashit feedback

* test: addresed review comments for cypress tests

* add analytics events

* height issue in view mode

* incorporate code review feedbacks

* incorporate code review feedbacks

* refactor: addressed review comments; removed border radius and box shadow for text widget; Updated migrations

* feat: Makes shadow and radius controls keyboard accessible (#11547)

* makes shadow and radius controls keyboard accessible

* removes unused imports

* moves options out of render method

* fix: changed the misnomer background property name to the relevant property name

* fix: border radius issue for the map widget

* address qa bugs

* address qa bugs

* fix ux of theming pane when widget is selected

* fix:
* added backgroundColor to the video widget
* restricted pop-over border radius to 0.375rem
* added box shadow for the input group for select widget

* fix: added delete icon in the delete theme modal

* address qa bugs

* change checkbox column size in config

* add js convertible to button color

* remove unused imports

* test: fixed jest tests

* fix primary color typo

* fix: migrations for the theming

* fix:
* Removed background color from MultiTreeSelect and TreeSelect component.
* grouped button's menu button pop over border radius restricting to 0.375rem.

* test: updated Dsl migration UT

* address qa bugs

* address qa bugs

* fix: address qa comments

* address qa bugs

* fix:
* migration issue;
* unit test cases;

* fix rating widget scroll issue

* fix youtube video border radius bug

* fix select widget

* fix select widgets styles

* address qa bugs

* merge conflicts

* makes the reset button keyboard accessible (#12134)

* -resolved merge conflicts

* address qa bugs

* fix: labelTextSize migration fixes

* refactor:
* made changes to the fontSizeUtils function
* fixed the issue related to unit tests

* fix button group widget

* remove unused imports

* fix: fixed the text size migration for the table widget

* refactor: addressed review comments for the table widget theming migration

* fix button group widget

* add init calls for view mode

* json form init theme changes

* fix: added migration for boxShadow, borderRadius and textSizes for table widget

* fix broken fields

* test: fixed unit tests

* wip

* inconsistancy fixes and schemaItem update in updateHook/fieldConfiguration

* feat: init json form migration theming

* json form primaryColor -> accentColor

* update table widget

* update table widget

* object field label styling

* fix: migration related to the JSON form

* fix: fixed labelTextSize migration for JSON form nested widgets

* property control nested stylesheet lookup

* JSONForm label styles form array items

* show label for checkbox field array item

* fix button group widget

* wip

* refactor: addressed table widget review comments

* refactor: addressed ashit review comments;
* added childStylesheet for widgets

* feat: Keyboard navigable Color Picker control (#11797)

* Makes ColorPicker keyboard accessible

* seperate out keyboard and mouse interactions

* fix issue with not focusing back to input

* Adds test for Color picker

* chore: added comment for the boxShadow property

* fix:
* added unit test cases for the widget and property utils
* resolved warning messages

* wip

* theme config update

* fix merge conflicts

* refactor: moved theming migration inside the migrations folder

* fix qa bugs

* fix jest test

* fix: unit test cases

* fix table column creation logic

* refactor: addressed review comments for migrations

* fix: Overriding margin and padding for custom render in the dropdown component (#12875)

* * fix for custom render padding and margin in ADS dropdown

* * fix for removing padding from normal render options

* refactor: moved the boxShadow condition to the variable

* fix qa bugs

* fix: migration QA callouts for audio recorder widget

* refactor: added updated comments for boxShadow migration for table widget

* fix theme binfings for JSONForm fields under Object

* fix table widget theming bug

* fix: addressed code review comments

* fix: unit test cases

* fix: qa migration callouts

* fix table widget theming bug

* fix JSONForm currency input dropdown not submit form

* Added new tests - AppThemingSpec

* fix qa bugs

* fix unit test

* fix JSONForm cellBorderWidth to have default value post migration

* fix unit test

* fix qa bugs

* remove unused imports

* fix qa bugs

* fix JSONForm input height issue

* fix qa bugs

* Updating Theming spec

* * dropdown color fixes (#13249)

* fix caching issue
;

* Fixed Theming tests

* fix tests

* fix tab widget tests

* fix: json form children level migration issue

* fix table widget tests

* Updated test

* updated tests

* updated test

* updated tests

* updated tests

* updated pageload

* fix cypress tests

* remove cypress created files

* fix color picker issues

* Failure fixes

* Fixed some more tests

* fix: cypress test failures

* fix tests

* remove consoles

* fix table tests

* fix qa bugs

* updating snapshots for AppPageLayout_spec as per new UI

* fix rating widget bug

* fix qa bugs

* fix:
* cypress failing tests
* Migration QA callouts
* Removed unused imports

* update constract check algo

* fix color contrast issue

* fix: cypress failure test cases

* update font sizes labels

* fix regression bugs

* fix:
* JSON form labelTextSize issue fix
* Updated comment for the fontSizeUtility function
* migrations issues related to table widget borderRadius and boxShadow

* fix: default labelTextSize issue for the Input and Select families

* fix regression bugs

* fix regression bugs

* PassingParams spec - added wait time

* fix: font family default value issue on JS toggle

* fix js toggle issue in text widget

* fix tests

* fix tests

* fix tests

* fix cypress tests

* fix regression bugs

* fix regression bugs

* fix:
* refactored table widget migration function as per review comments,
* added default value to the widget

* fix: failing unit test cases

* fix theming spec

* fix cypress tests

* test: fixed failed cypress test

* incorporate ashit feedback

* fix cypress tests

* fix: addressed review comments

* comment out table cypress test

* fix merge conflicts

* comment out color picker tests

Co-authored-by: Pawan Kumar <pawankumar@Pawans-MacBook-Pro.local>
Co-authored-by: keyurparalkar <keyur@appsmith.com>
Co-authored-by: Aswath K <aswath@appsmith.com>
Co-authored-by: Nayan <nayan@appsmith.com>
Co-authored-by: Ashit Rath <ashit@appsmith.com>
Co-authored-by: balajisoundar <balaji@appsmith.com>
Co-authored-by: albinAppsmith <87797149+albinAppsmith@users.noreply.github.com>
Co-authored-by: Aishwarya UR <aishwarya@appsmith.com>
Co-authored-by: apple <nandan@thinkify.io>
Co-authored-by: Parthvi Goswami <parthvigoswami@Parthvis-MacBook-Pro.local>
2022-05-04 15:15:57 +05:30

537 lines
15 KiB
TypeScript

import React, { useCallback, useEffect, useState } from "react";
import Editor from "@draft-js-plugins/editor";
import {
CompositeDecorator,
convertFromRaw,
DraftDecorator,
EditorState,
RawDraftContentState,
} from "draft-js";
import styled from "styled-components";
import ProfileImage, { Profile } from "pages/common/ProfileImage";
import { Comment, Reaction } from "entities/Comments/CommentsInterfaces";
import { getTypographyByKey } from "constants/DefaultTheme";
import CommentContextMenu from "./CommentContextMenu";
import ResolveCommentButton from "comments/CommentCard/ResolveCommentButton";
import { MentionComponent } from "components/ads/MentionsInput";
import Icon, { IconSize } from "components/ads/Icon";
import EmojiReactions, {
Reaction as ComponentReaction,
Reactions,
ReactionOperation,
} from "components/ads/EmojiReactions";
import { Toaster } from "components/ads/Toast";
import AddCommentInput from "comments/inlineComments/AddCommentInput";
import createMentionPlugin from "@draft-js-plugins/mention";
import { flattenDeep, noop } from "lodash";
import copy from "copy-to-clipboard";
import moment from "moment";
import history from "utils/history";
import { getAppMode } from "selectors/applicationSelectors";
import { widgetsMapWithParentModalId } from "selectors/entitiesSelector";
import { USER_PHOTO_ASSET_URL } from "constants/userConstants";
import { getCommentThreadURL } from "../utils";
import {
deleteCommentRequest,
markThreadAsReadRequest,
pinCommentThreadRequest,
editCommentRequest,
deleteCommentThreadRequest,
addCommentReaction,
removeCommentReaction,
} from "actions/commentActions";
import { useDispatch, useSelector } from "react-redux";
import { commentThreadsSelector } from "selectors/commentsSelectors";
import { getCurrentUser } from "selectors/usersSelectors";
import {
createMessage,
LINK_COPIED_SUCCESSFULLY,
} from "@appsmith/constants/messages";
import { Variant } from "components/ads/common";
import TourTooltipWrapper from "components/ads/tour/TourTooltipWrapper";
import { TourType } from "entities/Tour";
import useProceedToNextTourStep from "utils/hooks/useProceedToNextTourStep";
import { commentsTourStepsEditModeTypes } from "comments/tour/commentsTourSteps";
import { useNavigateToWidget } from "pages/Editor/Explorer/Widgets/useNavigateToWidget";
const StyledContainer = styled.div`
width: 100%;
padding: ${(props) =>
`${props.theme.spaces[6]}px ${props.theme.spaces[5]}px`};
border-radius: 0;
&:hover {
background-color: ${(props) =>
props.theme.colors.comments.cardHoverBackground};
}
`;
const CommentBodyContainer = styled.div`
padding-bottom: ${(props) => props.theme.spaces[4]}px;
color: ${(props) => props.theme.colors.comments.profileUserName};
`;
const CommentHeader = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: ${(props) => props.theme.spaces[4]}px;
`;
const UserName = styled.span`
${(props) => getTypographyByKey(props, "h5")}
color: ${(props) => props.theme.colors.comments.profileUserName};
margin-left: ${(props) => props.theme.spaces[4]}px;
display: flex;
align-items: center;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1; /* number of lines to show */
-webkit-box-orient: vertical;
word-break: break-word;
`;
const HeaderSection = styled.div`
display: flex;
align-items: center;
max-width: 100%;
& ${Profile} {
flex-shrink: 0;
}
`;
const CommentTime = styled.div`
color: ${(props) => props.theme.colors.comments.commentTime};
${(props) => getTypographyByKey(props, "p3")}
display: flex;
justify-content: space-between;
margin-bottom: ${(props) => props.theme.spaces[4]}px;
`;
const CommentSubheader = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: ${(props) => props.theme.spaces[4]}px;
white-space: nowrap;
${(props) => getTypographyByKey(props, "p3")}
color: ${(props) => props.theme.colors.comments.pinnedByText};
& .thread-id {
flex-shrink: 0;
max-width: 50px;
}
& .pin {
margin: 0 ${(props) => props.theme.spaces[3]}px;
}
strong {
white-space: pre;
margin-left: ${(props) => props.theme.spaces[0]}px;
text-overflow: ellipsis;
overflow: hidden;
}
`;
const CommentThreadId = styled.div`
color: ${(props) => props.theme.colors.comments.commentTime};
${(props) => getTypographyByKey(props, "p3")}
overflow: hidden;
text-overflow: ellipsis;
`;
const Section = styled.div`
display: flex;
align-items: center;
overflow: hidden;
text-overflow: ellipsis;
`;
const UnreadIndicator = styled.div`
width: 6px;
height: 6px;
border-radius: 50%;
background-color: ${(props) =>
props.theme.colors.comments.unreadIndicatorCommentCard};
margin-right: ${(props) => props.theme.spaces[2]}px;
flex-shrink: 0;
`;
const ReactionsRow = styled.div`
display: flex;
`;
const EmojiReactionsBtnContainer = styled.div``;
const mentionPlugin = createMentionPlugin({
mentionComponent: MentionComponent,
});
const plugins = [mentionPlugin];
const decorators = flattenDeep(plugins.map((plugin) => plugin.decorators));
const decorator = new CompositeDecorator(
decorators.filter((_decorator, index) => index !== 1) as DraftDecorator[],
);
function StopClickPropagation({ children }: { children: React.ReactNode }) {
return (
<div
// flex to unset height, so that align-items works as expected
onClick={(e: React.MouseEvent) => e.stopPropagation()}
style={{ display: "flex" }}
>
{children}
</div>
);
}
const replyText = (replies?: number) => {
if (!replies) return "";
return replies > 1 ? `${replies} Replies` : `1 Reply`;
};
enum CommentCardModes {
EDIT = "EDIT",
VIEW = "VIEW",
}
const reduceReactions = (
reactions: Array<Reaction> | undefined,
username?: string,
) => {
return (
(Array.isArray(reactions) &&
reactions.reduce(
(res: Record<string, ComponentReaction>, reaction: Reaction) => {
const { byName, byUsername, emoji } = reaction;
const sameAsCurrent = byUsername === username;
const name = byName || byUsername;
if (res[reaction.emoji]) {
res[reaction.emoji].count++;
if (!sameAsCurrent) {
res[reaction.emoji].users = [
...(res[reaction.emoji].users || []),
name,
];
}
} else {
const users = !sameAsCurrent ? [name] : [];
res[emoji] = {
count: 1,
reactionEmoji: emoji,
users,
} as ComponentReaction;
}
if (sameAsCurrent) {
res[reaction.emoji].active = true;
}
return res;
},
{},
)) ||
undefined
);
};
const ResolveButtonContainer = styled.div`
margin-left: ${(props) => props.theme.spaces[2]}px;
`;
function CommentCard({
comment,
commentThreadId,
inline,
isParentComment,
numberOfReplies,
resolved,
showReplies,
showSubheader,
toggleResolved,
unread = true,
visible,
}: {
comment: Comment;
isEditMode?: boolean;
isParentComment?: boolean;
resolved?: boolean;
toggleResolved?: () => void;
commentThreadId: string;
numberOfReplies?: number;
showReplies?: boolean;
showSubheader?: boolean;
unread?: boolean;
inline?: boolean;
visible?: boolean;
}) {
const proceedToNextTourStep = useProceedToNextTourStep({
[TourType.COMMENTS_TOUR_EDIT_MODE]: commentsTourStepsEditModeTypes.RESOLVE,
});
const [isHovered, setIsHovered] = useState(false);
const [cardMode, setCardMode] = useState(CommentCardModes.VIEW);
const dispatch = useDispatch();
const { authorName, authorPhotoId, body, id: commentId } = comment;
const contentState = convertFromRaw(body as RawDraftContentState);
const editorState = EditorState.createWithContent(contentState, decorator);
const commentThread = useSelector(commentThreadsSelector(commentThreadId));
const [reactions, setReactions] = useState<Reactions>();
const currentUser = useSelector(getCurrentUser);
const currentUserUsername = currentUser?.username;
const isPinned = commentThread.pinnedState?.active;
const pinnedByUsername = commentThread.pinnedState?.authorUsername;
let pinnedBy = commentThread.pinnedState?.authorName;
const appMode = useSelector(getAppMode);
if (currentUserUsername === pinnedByUsername) {
pinnedBy = "You";
}
const commentThreadURL = getCommentThreadURL({
commentThreadId,
isResolved: !!commentThread?.resolvedState?.active,
pageId: commentThread?.pageId,
mode: appMode,
});
const copyCommentLink = () => {
copy(commentThreadURL.toString());
Toaster.show({
text: createMessage(LINK_COPIED_SUCCESSFULLY),
variant: Variant.success,
});
};
const pin = useCallback(() => {
dispatch(
pinCommentThreadRequest({ threadId: commentThreadId, pin: !isPinned }),
);
}, [isPinned]);
const deleteComment = useCallback(() => {
dispatch(deleteCommentRequest({ threadId: commentThreadId, commentId }));
}, []);
const deleteThread = () => {
dispatch(deleteCommentThreadRequest(commentThreadId));
};
const isCreatedByMe = currentUserUsername === comment.authorUsername;
const switchToEditCommentMode = () => setCardMode(CommentCardModes.EDIT);
const switchToViewCommentMode = () => setCardMode(CommentCardModes.VIEW);
const onSaveComment = (body: RawDraftContentState) => {
dispatch(editCommentRequest({ commentId, commentThreadId, body }));
setCardMode(CommentCardModes.VIEW);
};
const widgetMap: Record<string, any> = useSelector(
widgetsMapWithParentModalId,
);
const contextMenuProps = {
switchToEditCommentMode,
pin,
copyCommentLink,
deleteComment,
deleteThread,
isParentComment,
isCreatedByMe,
isPinned,
};
// TODO enable when comments links are enabled
// useSelectCommentUsingQuery(comment.id);
const { navigateToWidget } = useNavigateToWidget();
// Dont make inline cards clickable
// TODO check if type === widget
const handleCardClick = () => {
if (inline) return;
if (commentThread.widgetType) {
// for the view mode we use canvas widgets instead of widgets by page
// since we don't have the dsl for all the pages currently
const widget = widgetMap[commentThread.refId];
// 1. This is only needed for the modal widgetMap
// 2. TODO check if we can do something similar for tabs
// 3. getAllWidgetsMap doesn't exist for the view mode, so these won't work for the view mode
if (widget?.parentModalId) {
navigateToWidget(
commentThread.refId,
commentThread.widgetType,
widget.pageId,
false,
widget.parentModalId,
);
}
}
history.push(
`${commentThreadURL.pathname}${commentThreadURL.search}${commentThreadURL.hash}`,
);
if (!commentThread?.isViewed) {
dispatch(markThreadAsReadRequest(commentThreadId));
}
};
useEffect(() => {
setReactions(reduceReactions(comment.reactions, currentUserUsername));
}, [comment.reactions]);
const handleReaction = (
_event: React.MouseEvent,
emojiData: string,
updatedReactions: Reactions,
addOrRemove: ReactionOperation,
) => {
setReactions(updatedReactions);
if (addOrRemove == ReactionOperation.ADD) {
dispatch(addCommentReaction({ emoji: emojiData, commentId }));
} else {
dispatch(removeCommentReaction({ emoji: emojiData, commentId }));
}
};
const showOptions = visible || isHovered;
const showResolveBtn =
(showOptions || !!resolved) && isParentComment && toggleResolved;
const hasReactions = !!reactions && Object.keys(reactions).length > 0;
const profilePhotoUrl = authorPhotoId
? `/api/${USER_PHOTO_ASSET_URL}/${authorPhotoId}`
: "";
return (
<StyledContainer
data-cy={`t--comment-card-${comment.id}`}
onClick={handleCardClick}
onMouseLeave={() => setIsHovered(false)}
onMouseOver={() => setIsHovered(true)}
>
{showSubheader && (
<CommentSubheader>
<Section className="thread-id">
{unread && <UnreadIndicator />}
<CommentThreadId>{commentThread.sequenceId}</CommentThreadId>
</Section>
<Section className="pinned-by" onClick={pin}>
{isPinned && (
<>
<Icon className="pin" name="pin-3" size={IconSize.XXL} />
<span>Pinned By</span>
<strong>{` ${pinnedBy}`}</strong>
</>
)}
</Section>
</CommentSubheader>
)}
<CommentHeader data-cy="comments-card-header">
<HeaderSection>
<ProfileImage
size={25}
source={profilePhotoUrl}
userName={authorName || ""}
/>
<UserName>{authorName}</UserName>
</HeaderSection>
<HeaderSection>
{showOptions && (
<StopClickPropagation>
<EmojiReactionsBtnContainer>
<EmojiReactions
hideReactions
iconSize={IconSize.XXL}
onSelectReaction={handleReaction}
reactions={reactions}
/>
</EmojiReactionsBtnContainer>
</StopClickPropagation>
)}
{showResolveBtn && (
<StopClickPropagation>
<ResolveButtonContainer>
{inline ? (
<TourTooltipWrapper
activeStepConfig={{
[TourType.COMMENTS_TOUR_EDIT_MODE]:
commentsTourStepsEditModeTypes.RESOLVE,
}}
>
<ResolveCommentButton
handleClick={() => {
toggleResolved && toggleResolved();
proceedToNextTourStep();
}}
resolved={!!resolved}
/>
</TourTooltipWrapper>
) : (
<ResolveCommentButton
handleClick={toggleResolved as () => void}
resolved={!!resolved}
/>
)}
</ResolveButtonContainer>
</StopClickPropagation>
)}
{showOptions && (
<StopClickPropagation>
<CommentContextMenu {...contextMenuProps} />
</StopClickPropagation>
)}
</HeaderSection>
</CommentHeader>
<CommentBodyContainer>
{cardMode === CommentCardModes.EDIT ? (
<AddCommentInput
initialEditorState={editorState}
onCancel={switchToViewCommentMode}
onSave={onSaveComment}
removePadding
/>
) : (
<Editor
editorState={editorState}
onChange={noop}
plugins={plugins}
readOnly
/>
)}
</CommentBodyContainer>
<CommentTime>
<span>{moment(comment.creationTime).fromNow()}</span>
<span>{showReplies && replyText(numberOfReplies)}</span>
</CommentTime>
{hasReactions && (
<ReactionsRow>
<StopClickPropagation>
<EmojiReactions
iconSize={IconSize.LARGE}
onSelectReaction={handleReaction}
reactions={reactions}
/>
</StopClickPropagation>
</ReactionsRow>
)}
</StyledContainer>
);
}
export default CommentCard;