Skip to content

Commit

Permalink
Add threads to host applications (#10580)
Browse files Browse the repository at this point in the history
* Add small variant to comment threads

* Fix typing of canComment function

* Update collapsable html component style

* Add withdraw application action

* Redesign and add comment threads to host application pages

* Apply new comment thread variant to dashboard expense drawers

* Fix lint issues

* OCF banner

* Click pending tab in e2e test

* Add missing pending click in e2e

* Add private notes to host applications

* fix prettier

* Adjust tailwind classes and style

* update graphql

* Update langs

* fix lint issues
  • Loading branch information
hdiniz committed Sep 9, 2024
1 parent f111a25 commit bf3d097
Show file tree
Hide file tree
Showing 51 changed files with 2,548 additions and 1,256 deletions.
22 changes: 13 additions & 9 deletions components/HTMLContent.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import React, { useEffect, useRef } from 'react';
import React, { useLayoutEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { CaretDown } from '@styled-icons/fa-solid/CaretDown';
import { CaretUp } from '@styled-icons/fa-solid/CaretUp';
import { Markup } from 'interweave';
import { ChevronDown, ChevronUp } from 'lucide-react';
import { getLuminance } from 'polished';
import { FormattedMessage } from 'react-intl';
import styled, { css } from 'styled-components';
Expand Down Expand Up @@ -31,6 +30,10 @@ export const isEmptyHTMLValue = value => {
};

const ReadFullLink = styled.a`
margin-top: 4px;
display: flex;
align-items: center;
gap: 4px;
cursor: pointer;
font-size: 12px;
> svg {
Expand All @@ -48,7 +51,7 @@ const InlineDisplayBox = styled.div`

const CollapsedDisplayBox = styled.div`
overflow-y: hidden;
${props => props.maxHeight && `max-height: ${props.maxCollapsedHeight + 20}px;`}
${props => props.maxCollapsedHeight && `max-height: ${props.maxCollapsedHeight + 20}px;`}
-webkit-mask-image: linear-gradient(to bottom, black 50%, transparent 100%);
mask-image: linear-gradient(to bottom, black 50%, transparent 100%);
`;
Expand All @@ -71,6 +74,7 @@ const HTMLContent = styled(
collapsePadding = 1,
hideViewMoreLink = false,
openLinksInNewTab = false,
readMoreMessage,
...props
}) => {
const [isOpen, setOpen] = React.useState(false);
Expand All @@ -79,8 +83,8 @@ const HTMLContent = styled(

const DisplayBox = !isCollapsed || isOpen ? InlineDisplayBox : CollapsedDisplayBox;

useEffect(() => {
if (collapsable && contentRef?.current?.clientHeight > maxCollapsedHeight + collapsePadding) {
useLayoutEffect(() => {
if (collapsable && contentRef?.current?.scrollHeight > maxCollapsedHeight + collapsePadding) {
setIsCollapsed(true);
}
}, [content]);
Expand Down Expand Up @@ -137,8 +141,8 @@ const HTMLContent = styled(
}
}}
>
<FormattedMessage id="ExpandDescription" defaultMessage="Read full description" />
<CaretDown size="10px" />
{readMoreMessage || <FormattedMessage id="ExpandDescription" defaultMessage="Read full description" />}
<ChevronDown size={10} />
</ReadFullLink>
)}
{isOpen && isCollapsed && (
Expand All @@ -155,7 +159,7 @@ const HTMLContent = styled(
}}
>
<FormattedMessage defaultMessage="Collapse" id="W/V6+Y" />
<CaretUp size="10px" />
<ChevronUp size={10} />
</ReadFullLink>
)}
</div>
Expand Down
32 changes: 15 additions & 17 deletions components/conversations/Comment.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';

import { API_V2_CONTEXT, gql } from '../../lib/graphql/helpers';

import Container from '../Container';
import { Box, Flex } from '../Grid';
import HTMLContent from '../HTMLContent';
Expand All @@ -13,19 +11,8 @@ import CommentActions from './CommentActions';
import { CommentMetadata } from './CommentMetadata';
import EmojiReactionPicker from './EmojiReactionPicker';
import CommentReactions from './EmojiReactions';
import { commentFieldsFragment } from './graphql';

const editCommentMutation = gql`
mutation EditComment($comment: CommentUpdateInput!) {
editComment(comment: $comment) {
id
...CommentFields
}
}
${commentFieldsFragment}
`;

const mutationOptions = { context: API_V2_CONTEXT };
import { editCommentMutation, mutationOptions } from './graphql';
import SmallComment from './SmallComment';

/**
* Render a comment.
Expand Down Expand Up @@ -62,7 +49,7 @@ const Comment = ({
onDelete={onDelete}
onEditClick={() => setEditing(true)}
onReplyClick={() => {
onReplyClick(comment);
onReplyClick?.(comment);
}}
/>
)}
Expand Down Expand Up @@ -138,4 +125,15 @@ Comment.propTypes = {
onReplyClick: PropTypes.func,
};

export default Comment;
/**
*
* @param {import('./types').CommentPropsWithVariant} props
*/
export default function CommentComponent(props) {
// eslint-disable-next-line react/prop-types
if (props.variant === 'small') {
return <SmallComment {...props} />;
}

return <Comment {...props} />;
}
8 changes: 5 additions & 3 deletions components/conversations/CommentActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import MessageBox from '../MessageBox';
import StyledButton from '../StyledButton';
import StyledHr from '../StyledHr';
import { P } from '../Text';
import { Button } from '../ui/Button';
import { useToast } from '../ui/useToast';

import { CommentMetadata } from './CommentMetadata';
Expand Down Expand Up @@ -197,14 +198,15 @@ const CommentActions = ({
return (
<React.Fragment>
<div>
<StyledButton
<Button
ref={setRefElement}
buttonSize="tiny"
variant="outline"
size="xs"
data-cy="commnent-actions-trigger"
onClick={() => setShowAdminActions(!showAdminActions)}
>
<DotsHorizontalRounded size="16" />
</StyledButton>
</Button>
</div>

{showAdminActions && (
Expand Down
29 changes: 20 additions & 9 deletions components/conversations/CommentForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ import LoadingPlaceholder from '../LoadingPlaceholder';
import MessageBox from '../MessageBox';
import RichTextEditor from '../RichTextEditor';
import SignInOrJoinFree, { SignInOverlayBackground } from '../SignInOrJoinFree';
import StyledButton from '../StyledButton';
import StyledCheckbox from '../StyledCheckbox';
import { P } from '../Text';
import { Button } from '../ui/Button';
import { withUser } from '../UserProvider';

import { commentFieldsFragment } from './graphql';
Expand Down Expand Up @@ -66,7 +66,7 @@ const isAutoFocused = id => {
const mutationOptions = { context: API_V2_CONTEXT };

/** A small helper to make the form work with params from both API V1 & V2 */
const prepareCommentParams = (html, conversationId, expenseId, updateId) => {
const prepareCommentParams = (html, conversationId, expenseId, updateId, hostApplicationId) => {
const comment = { html };
if (conversationId) {
comment.ConversationId = conversationId;
Expand All @@ -84,6 +84,8 @@ const prepareCommentParams = (html, conversationId, expenseId, updateId) => {
} else {
comment.update.legacyId = updateId;
}
} else if (hostApplicationId) {
comment.hostApplication = { id: hostApplicationId };
}
return comment;
};
Expand All @@ -97,6 +99,7 @@ const CommentForm = ({
ConversationId,
ExpenseId,
UpdateId,
HostApplicationId,
onSuccess,
router,
loadingLoggedInUser,
Expand All @@ -105,6 +108,9 @@ const CommentForm = ({
canUsePrivateNote,
defaultType = commentTypes.COMMENT,
replyingToComment,
minHeight = 250,
submitButtonJustify,
submitButtonVariant,
}) => {
const [createComment, { loading, error }] = useMutation(createCommentMutation, mutationOptions);
const intl = useIntl();
Expand All @@ -123,7 +129,7 @@ const CommentForm = ({
if (!html) {
setValidationError(createError(ERROR.FORM_FIELD_REQUIRED));
} else {
const comment = prepareCommentParams(html, ConversationId, ExpenseId, UpdateId);
const comment = prepareCommentParams(html, ConversationId, ExpenseId, UpdateId, HostApplicationId);
if (type) {
comment.type = type;
}
Expand Down Expand Up @@ -161,7 +167,7 @@ const CommentForm = ({
)}
<form onSubmit={postComment} data-cy="comment-form">
{loadingLoggedInUser ? (
<LoadingPlaceholder height={232} />
<LoadingPlaceholder height={minHeight} />
) : (
// When Key is updated the text editor default value will be updated too
<div key={replyingToComment?.id}>
Expand All @@ -170,7 +176,7 @@ const CommentForm = ({
kind="COMMENT"
withBorders
inputName="html"
editorMinHeight={250}
editorMinHeight={minHeight}
placeholder={formatMessage(messages.placeholder)}
autoFocus={Boolean(!isRichTextDisabled && isAutoFocused(id))}
disabled={isRichTextDisabled}
Expand Down Expand Up @@ -212,18 +218,18 @@ const CommentForm = ({
/>
</Box>
)}
<Flex mt={3} alignItems="center" gap={12}>
<StyledButton
<Flex mt={3} alignItems="center" justifyContent={submitButtonJustify} gap={12}>
<Button
minWidth={150}
buttonStyle="primary"
variant={submitButtonVariant}
disabled={isDisabled || !LoggedInUser || uploading}
loading={loading}
data-cy="submit-comment-btn"
type="submit"
name="submit-comment"
>
{formatMessage(uploading ? messages.uploadingImage : messages.postReply)}
</StyledButton>
</Button>
</Flex>
</form>
</Container>
Expand All @@ -239,6 +245,8 @@ CommentForm.propTypes = {
ExpenseId: PropTypes.string,
/** If commenting on an update */
UpdateId: PropTypes.string,
/** If commenting on a host application */
HostApplicationId: PropTypes.string,
/** Called when the comment is created successfully */
onSuccess: PropTypes.func,
/** disable the inputs */
Expand All @@ -256,6 +264,9 @@ CommentForm.propTypes = {
router: PropTypes.object,
/** Called when comment gets selected*/
getClickedComment: PropTypes.func,
minHeight: PropTypes.number,
submitButtonJustify: PropTypes.string,
submitButtonVariant: PropTypes.string,
};

export default withUser(withRouter(CommentForm));
119 changes: 119 additions & 0 deletions components/conversations/SmallComment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import React from 'react';
import clsx from 'clsx';
import { Lock } from 'lucide-react';
import { FormattedMessage } from 'react-intl';

import commentTypes from '../../lib/constants/commentTypes';

import { AccountHoverCard } from '../AccountHoverCard';
import Avatar from '../Avatar';
import DateTime from '../DateTime';
import HTMLContent from '../HTMLContent';
import InlineEditField from '../InlineEditField';
import RichTextEditor from '../RichTextEditor';

import CommentActions from './CommentActions';
import { editCommentMutation, mutationOptions } from './graphql';
import type { CommentProps } from './types';

export default function SmallComment(props: CommentProps) {
const [isEditing, setEditing] = React.useState(false);
const hasActions = !isEditing;
const comment = props.comment;
const anchorHash = `comment-${new Date(comment.createdAt).getTime()}`;
const isPrivateNote = comment.type === commentTypes.PRIVATE_NOTE;

return (
<div
className="relative w-full border-slate-200 py-4 first:border-none first:pt-0 [&:last-child_.timeline-indicator]:-bottom-4 [&:last-child_.timeline-separator]:hidden"
data-cy="comment"
id={anchorHash}
>
<div className="timeline-separator absolute bottom-0 left-[20px] right-0 border-b" />
<div className="flex justify-between">
<div className="flex gap-4">
<div className="relative">
<div
className={clsx('timeline-indicator absolute bottom-[-16px] left-[20px] top-[-16px] border-l', {
'border-blue-400': isPrivateNote,
})}
/>
<AccountHoverCard
account={comment.fromAccount}
trigger={
<div className="relative">
<Avatar collective={comment.fromAccount} radius={40} />
{isPrivateNote && (
<div className="absolute bottom-[-4px] right-[-4px] flex h-[20px] w-[20px] items-center justify-center rounded-full bg-white shadow">
<Lock size={16} className="text-blue-400" />
</div>
)}
</div>
}
/>
</div>
<div>
<div className="mb-1 text-sm font-medium leading-5">{comment.fromAccount.name}</div>
<div className="text-sm leading-4 text-[#75777A]">
<DateTime dateStyle="medium" value={comment.createdAt} />
</div>
<div className="mt-4">
<InlineEditField
mutation={editCommentMutation}
mutationOptions={mutationOptions}
values={comment}
field="html"
canEdit={props.canEdit}
canDelete={props.canDelete}
isEditing={isEditing}
showEditIcon={false}
prepareVariables={(comment, html) => ({ comment: { id: comment.id, html } })}
disableEditor={() => setEditing(false)}
warnIfUnsavedChanges
required
>
{({ isEditing, setValue, setUploading }) =>
!isEditing ? (
<HTMLContent
fontSize="14px"
maxCollapsedHeight={140}
collapsable
collapsePadding={22}
content={comment.html}
data-cy="comment-body"
readMoreMessage={<FormattedMessage defaultMessage="Read more" id="ContributeCard.ReadMore" />}
/>
) : (
<RichTextEditor
kind="COMMENT"
defaultValue={comment.html}
onChange={e => setValue(e.target.value)}
fontSize="14px"
autoFocus
setUploading={setUploading}
/>
)
}
</InlineEditField>
</div>
</div>
</div>
{hasActions && (
<CommentActions
comment={comment}
anchorHash={anchorHash}
isConversationRoot={props.isConversationRoot}
canEdit={props.canEdit}
canDelete={props.canDelete}
canReply={props.canReply}
onDelete={props.onDelete}
onEditClick={() => setEditing(true)}
onReplyClick={() => {
props.onReplyClick(comment);
}}
/>
)}
</div>
</div>
);
}
Loading

0 comments on commit bf3d097

Please sign in to comment.