From fed4a5aeb945cebdd8fc0a295712ff9524f4ee6e Mon Sep 17 00:00:00 2001 From: Arian <89781510+arian-garshi@users.noreply.github.com> Date: Mon, 18 Sep 2023 09:54:52 +0200 Subject: [PATCH] feat: basic tagging functionality --- .../Comments/Components/ClusteredMessages.tsx | 35 +++-- .../Comments/Components/CommentView.tsx | 96 ++++++++++++-- .../Comments/Components/InputController.tsx | 33 ++--- .../Comments/Components/InputField.tsx | 121 ++++++++++++++++++ .../Comments/Components/RenderComment.tsx | 3 +- .../Comments/Components/TagDropDown.tsx | 76 +++++++++++ src/api/environmentConfig.ts | 3 +- src/utils/helpers.tsx | 40 ++++++ 8 files changed, 360 insertions(+), 47 deletions(-) create mode 100644 src/Components/SideSheet/Comments/Components/InputField.tsx create mode 100644 src/Components/SideSheet/Comments/Components/TagDropDown.tsx diff --git a/src/Components/SideSheet/Comments/Components/ClusteredMessages.tsx b/src/Components/SideSheet/Comments/Components/ClusteredMessages.tsx index 626a38ce..0d230e1a 100644 --- a/src/Components/SideSheet/Comments/Components/ClusteredMessages.tsx +++ b/src/Components/SideSheet/Comments/Components/ClusteredMessages.tsx @@ -111,26 +111,23 @@ const ClusteredMessages: FC = () => {
- {cluster.messages.map((message, messageIndex) => { - console.log(message) - return ( - <> - {message.isEdited && ( - - Edited - {" "} - {formatDate(message.modifiedDate || "")} - + {cluster.messages.map((message, messageIndex) => ( + <> + {message.isEdited && ( + + Edited + {" "} + {formatDate(message.modifiedDate || "")} + )} - - - ) - })} + + + ))}
))} diff --git a/src/Components/SideSheet/Comments/Components/CommentView.tsx b/src/Components/SideSheet/Comments/Components/CommentView.tsx index 34d100e5..ec353cfd 100644 --- a/src/Components/SideSheet/Comments/Components/CommentView.tsx +++ b/src/Components/SideSheet/Comments/Components/CommentView.tsx @@ -9,7 +9,16 @@ import { Message } from "../../../../Models/Message" import InputController from "./InputController" import { ViewContext } from "../../../../Context/ViewContext" import ClusteredMessages from "./ClusteredMessages" +import TagDropDown from "./TagDropDown" +import { processMessageInput } from "../../../../utils/helpers" +const Controls = styled.div` + position: sticky; + bottom: 0; + width: 100%; + box-sizing: border-box; + +` const Container = styled.div` display: flex; flex-direction: column; @@ -35,14 +44,48 @@ const CommentView: React.FC = ({ currentProperty, }) => { const [newMessage, setNewMessage] = useState() + const [taggedUsers, setTaggedUsers] = useState([]) + const [searchTerm, setSearchTerm] = useState("") + const [showTagDropDown, setShowTagDropDown] = useState(false) const { - activeTagData, conversations, setConversations, activeConversation, setActiveConversation, + activeTagData, + conversations, + setConversations, + activeConversation, + setActiveConversation, } = useContext(ViewContext) const getConversationForProperty = (property: string) => ( conversations.find((conversation) => conversation.property?.toUpperCase() === property.toUpperCase()) ) + const dummyData = [ + { + id: "1", + displayName: "Henrik Hansen", + accountType: "Consultant", + status: "Active", + }, + { + id: "2", + displayName: "Peter Jensen", + accountType: "Consultant", + status: "Active", + }, + { + id: "3", + displayName: "Jesper Gudbransen", + accountType: "Consultant", + status: "inactive", + }, + { + id: "4", + displayName: "Mikkel Eriksen", + accountType: "Consultant", + status: "inactive", + }, + ] + useEffect(() => { (async () => { try { @@ -63,12 +106,18 @@ const CommentView: React.FC = ({ })() }, [currentProperty]) - const handleMessageChange = ( - event: React.ChangeEvent, - ) => { + const handleTagSelected = (displayName: string, userId: string) => { + const commentText = newMessage?.text ?? "" + const lastAtPos = commentText.lastIndexOf("@") + const beforeAt = commentText.substring(0, lastAtPos) + const afterAt = commentText.substring(lastAtPos + 1).replace(/^\S+/, "") // Removes the word right after the "@" + + const newCommentText = `${beforeAt}${displayName} ${afterAt}` const message = { ...newMessage } - message.text = event.target.value + message.text = newCommentText setNewMessage(message) + setShowTagDropDown(false) + setSearchTerm("") } const createConversation = async () => { @@ -92,6 +141,9 @@ const CommentView: React.FC = ({ const addMessage = async () => { const message = { ...newMessage } + const { processedString, mentions } = processMessageInput(newMessage?.text ?? "") + console.log("mentions: ", mentions) // to be used for tagging users in the future + message.text = processedString try { const service = await GetConversationService() const savedMessage = await service.addMessage(activeTagData?.review?.id ?? "", activeConversation?.id ?? "", message) @@ -106,6 +158,8 @@ const CommentView: React.FC = ({ console.error(`Error creating comment: ${error}`) } setNewMessage(undefined) + setTaggedUsers([]) + setSearchTerm("") } const handleSubmit = async () => { @@ -116,16 +170,38 @@ const CommentView: React.FC = ({ } } + useEffect(() => { + console.log("newMessage: ", newMessage) + }, [newMessage]) + + useEffect(() => { + console.log("taggedUsers: ", taggedUsers) + }, [taggedUsers]) + return ( - + + {showTagDropDown && ( + + )} + + + ) } diff --git a/src/Components/SideSheet/Comments/Components/InputController.tsx b/src/Components/SideSheet/Comments/Components/InputController.tsx index 46a2964b..96774f0c 100644 --- a/src/Components/SideSheet/Comments/Components/InputController.tsx +++ b/src/Components/SideSheet/Comments/Components/InputController.tsx @@ -1,15 +1,13 @@ import React, { FC } from "react" import { - Input, Button, Icon, Checkbox, + Button, Icon, Checkbox, } from "@equinor/eds-core-react" import styled from "styled-components" import { send } from "@equinor/eds-icons" +import InputField from "./InputField" const Controls = styled.div` - width: 100%; padding: 30px 15px 10px 15px; - position: sticky; - bottom: 0; background-color: white; border-top: 1px solid LightGray; display: flex; @@ -24,24 +22,29 @@ const InputButtonWrapper = styled.div` ` interface InputControllerProps { - value: string - handleCommentChange: (event: React.ChangeEvent) => void handleSubmit: () => void + setSearchTerm: React.Dispatch> + setShowTagDropDown: React.Dispatch> + newMessage: any + setNewMessage: React.Dispatch> + taggedUsers: string[] } const InputController: FC = ({ - value, - handleCommentChange, handleSubmit, + setShowTagDropDown, + setSearchTerm, + newMessage, + setNewMessage, + taggedUsers, }) => ( - diff --git a/src/Components/SideSheet/Comments/Components/InputField.tsx b/src/Components/SideSheet/Comments/Components/InputField.tsx new file mode 100644 index 00000000..eac040bf --- /dev/null +++ b/src/Components/SideSheet/Comments/Components/InputField.tsx @@ -0,0 +1,121 @@ +import React, { + useRef, useState, useEffect, MutableRefObject, +} from "react" +import styled from "styled-components" + +const StyledDiv = styled.div` + background-color: #F7F7F7; + border-bottom: 1px solid #6F6F6F; + cursor: text; + padding: 8px; +` + +const StyledP = styled.p<{ isPlaceholder: boolean }>` + color: ${({ isPlaceholder }) => (isPlaceholder ? "grey" : "black")}; + margin: 0; + min-height: 18px; + + &:focus { + outline: none; + } + + span { + color: #007079; + font-weight: 500; +` + +interface Props { + placeholder?: string + setSearchTerm: React.Dispatch> + setShowTagDropDown: React.Dispatch> + newReviewComment: any + setNewReviewComment: React.Dispatch> + taggedUsers: string[] +} + +const InputField: React.FC = ({ + placeholder = "Write a comment...", + setSearchTerm, + setShowTagDropDown, + newReviewComment, + setNewReviewComment, + taggedUsers, +}) => { + const pRef = useRef(null) + const [isPlaceholderShown, setIsPlaceholderShown] = useState(true) + +useEffect(() => { + if (pRef.current) { + console.log("re-rendering with the text: ", newReviewComment?.text) + pRef.current.innerHTML = newReviewComment?.text || "" + } +}, [taggedUsers]) + +useEffect(() => { + if (pRef.current && isPlaceholderShown) { + pRef.current.innerHTML = placeholder + } +}, [isPlaceholderShown, placeholder]) + + const handleCommentChange = (commentText: string) => { + const lastAtPos = commentText.lastIndexOf("@") + const shouldShowDropdown = lastAtPos !== -1 + + setShowTagDropDown(shouldShowDropdown) + + if (shouldShowDropdown) { + const termAfterAt = commentText.slice(lastAtPos + 1) + setSearchTerm(termAfterAt) + } else { + setSearchTerm("") + } + + const comment = { ...newReviewComment, text: commentText } + setNewReviewComment(comment) + } + + const handleFocus = () => { + if (pRef.current) { + pRef.current.contentEditable = "true" + pRef.current.focus() + if (isPlaceholderShown) { + pRef.current.innerText = "" + } + } + } + + const handleBlur = () => { + if (pRef.current) { + pRef.current.contentEditable = "false" + const content = pRef.current.innerHTML + handleCommentChange(content) + if (content.trim() === "") { + setIsPlaceholderShown(true) + pRef.current.innerHTML = placeholder + } else { + setIsPlaceholderShown(false) + } + } + } + const handleInput = () => { + if (pRef.current) { + const content = pRef.current.innerHTML + if (content !== placeholder) { + handleCommentChange(content) + } + } + } + + return ( + + + + ) +} + +export default InputField diff --git a/src/Components/SideSheet/Comments/Components/RenderComment.tsx b/src/Components/SideSheet/Comments/Components/RenderComment.tsx index 6352da55..5ec3b037 100644 --- a/src/Components/SideSheet/Comments/Components/RenderComment.tsx +++ b/src/Components/SideSheet/Comments/Components/RenderComment.tsx @@ -12,6 +12,7 @@ import { Message } from "../../../../Models/Message" import { GetConversationService } from "../../../../api/ConversationService" import { Conversation } from "../../../../Models/Conversation" import { ViewContext } from "../../../../Context/ViewContext" +import { unescapeHtmlEntities } from "../../../../utils/helpers" const CommentText = styled(Typography)` margin: 10px 0; @@ -160,7 +161,7 @@ const RenderComment: FC = ({ onMouseOut={handleClose} > { - comment.softDeleted ? "Message deleted by user" : comment.text + comment.softDeleted ? "Message deleted by user" : unescapeHtmlEntities(comment.text || "") } > + SearchTerm?: string + onTagSelected: (displayName: string, userId: string) => void + dummyData: { id: string; displayName: string; accountType: string; status: string }[] +} + +const TagDropDown: FC = ({ + SearchTerm, setTaggedUsers, onTagSelected, dummyData, +}) => { + const filteredNames = dummyData.filter(({ displayName }) => !SearchTerm || displayName.toLowerCase().includes(SearchTerm.toLowerCase())) + + const handleTagClick = (userId: string, displayName: string) => { + setTaggedUsers((prevTaggedUsers) => [...prevTaggedUsers, userId]) + + onTagSelected(displayName, userId) + } + + return ( + + {filteredNames.map(({ id, displayName }) => ( +
  • + handleTagClick(id, displayName)}> + {displayName} + +
  • + ))} +
    + ) +} + +export default TagDropDown \ No newline at end of file diff --git a/src/api/environmentConfig.ts b/src/api/environmentConfig.ts index d59b9fc3..17071408 100644 --- a/src/api/environmentConfig.ts +++ b/src/api/environmentConfig.ts @@ -22,8 +22,7 @@ export const ResolveConfiguration = (env: string) => { } case "dev": return { - REACT_APP_API_BASE_URL: - "http://localhost:5000", + REACT_APP_API_BASE_URL: "http://localhost:5000", BACKEND_APP_SCOPE: [ "api://412803ed-0c05-44a9-b433-b270706a6099/Datasheet.Write", ], diff --git a/src/utils/helpers.tsx b/src/utils/helpers.tsx index 9d266c57..f08a380f 100644 --- a/src/utils/helpers.tsx +++ b/src/utils/helpers.tsx @@ -55,3 +55,43 @@ export const formatDate = (dateString: string) => { export function getPropertyName(property: keyof T): keyof T { return property } + +/** + * Processes a string to replace elements with text inside of them wrapped in double curly brackets. + * Also extracts all of the "data-mention" values and converts any   instances into normal spaces. + * + * @param {string} input - The input string to process. + * @returns {Object} An object containing the processed string and an array of mention IDs. + * @returns {string} processedString - The processed string. + * @returns {number[]} mentions - An array of mention IDs. + */ +export function processMessageInput(input: string): { processedString: string, mentions: number[] } { + const regex = /([^<]+)<\/span>/g + + let match + const mentions: number[] = [] + + let processedString = input.replace(regex, (fullMatch, mentionId, content) => { + mentions.push(Number(mentionId)) + return `{{${content}}} ` + }) + + processedString = processedString.replace(/ /g, " ") + + return { + processedString, + mentions, + } +} + +/** + * Converts HTML entities in a string to their corresponding characters. + * + * @param {string} str - The string with HTML entities. + * @returns {string} The string with HTML entities replaced by their corresponding characters. + */ +export function unescapeHtmlEntities(str: string): string { + const parser = new DOMParser() + const doc = parser.parseFromString(str, "text/html") + return doc.documentElement.textContent || "" +}