Related
hello i am not sure why i getting this error massage can some one correct my code i can not find key in my new post components
this is my newpost.js code
import React, { useContext } from 'react';
import { useHttpClient } from '../../hooks/useHttpClient';
import useForm from '../../hooks/useForm';
import { AuthContext } from '../../context/auth';
import { useHistory } from 'react-router-dom/cjs/react-router-dom.min';
import { newPostForm } from '../../utils/formConfig';
import { appendData, renderRepeatedSkeletons } from '../../utils';
import ErrorModal from '../../components/Modal/ErrorModal';
import SkeletonElement from '../../components/Skeleton/SkeletonElement';
const NewPost = () => {
const auth = useContext(AuthContext);
const history = useHistory();
const { currentUser } = auth;
const { isLoading, sendReq, error, clearError } = useHttpClient();
const { renderFormInputs, renderFormValues, isFormValid } =
useForm(newPostForm);
const formValues = renderFormValues();
const formInputs = renderFormInputs();
const postSubmitHandle = async (evt) => {
evt.preventDefault(); //otherwise, there will be a reload
const formData = appendData(formValues);
formData.append('author', currentUser.userId);
try {
await sendReq(
`${process.env.REACT_APP_BASE_URL}/posts`,
'POST',
formData,
{
Authorization: `Bearer ${currentUser.token}`,
}
);
history.push('/');
} catch (err) {}
};
return (
<>
<ErrorModal error={error} onClose={clearError} />
{isLoading ? (
renderRepeatedSkeletons(<SkeletonElement type='text' />, 20)
) : (
<div className='container-create-page'>
<form className='form form__create'>
<h2>Create a new post</h2>
{formInputs}
<button
onClick={postSubmitHandle}
className='btn'
disabled={!isFormValid()}
>
Submit <span>→</span>
</button>
</form>
</div>
)}
</>
);
};
export default NewPost;
and this is my useform.js code
import { useState, useCallback } from 'react';
//"signupForm" => "formObj" (name, email, password) => "form"
const useForm = (formObj) => {
const [form, setForm] = useState(formObj);
const renderFormInputs = () => {
//renders an [] of <Input> for all input fields
return Object.values(form).map((inputObj) => {
const { value, label, errorMessage, valid, renderInput } = inputObj;
return renderInput(
onInputChange,
value,
valid,
errorMessage,
label,
onCustomInputChange
);
});
};
const renderFormValues = () => {
let values = {};
Object.keys(form).forEach((inputObj) => {
values[inputObj] = form[inputObj].value;
});
return values;
};
const isInputFieldValid = useCallback(
(inputField) => {
for (const rule of inputField.validationRules) {
if (!rule.validate(inputField.value, form)) {
inputField.errorMessage = rule.message;
return false;
}
}
return true;
},
[form]
);
const onInputChange = useCallback(
(event) => {
const { name, value } = event.target;
let inputObj = { ...form[name], value };
const isValidInput = isInputFieldValid(inputObj);
if (isValidInput && !inputObj.valid) {
inputObj = { ...inputObj, valid: true };
} else if (!inputObj.touched && !isValidInput && inputObj.valid) {
inputObj = { ...inputObj, valid: false };
}
inputObj = { ...inputObj, touched: true };
setForm({ ...form, [name]: inputObj });
},
[form, isInputFieldValid]
);
const onCustomInputChange = useCallback(
(type, value, InputIsValid) => {
setForm({
...form,
[type]: { ...form[type], value, valid: InputIsValid },
});
},
[form]
);
const isFormValid = useCallback(
(customForm) => {
let isValid = true;
const arr = Object.values(customForm || form);
for (let i = 0; i < arr.length; i++) {
if (!arr[i].valid) {
isValid = false;
break;
}
}
return isValid;
},
[form]
);
return {
renderFormInputs,
renderFormValues,
isFormValid,
setForm,
};
};
export default useForm;
submit button does not work . it just a simple form with submit button and 4 input value and one image . if some one need more information . please ask in the comment section
index.js contain renderRepeatedSkeletons
export const checkInArray = (arr, elem) => {
return arr && arr.indexOf(elem) !== -1;
};
export const canModifyComment = (currentUserId, authorId) =>
currentUserId === authorId;
export const canReply = (currentUserId) => !!currentUserId;
export const isReplying = (activeComment, commentId) =>
activeComment &&
activeComment.type === 'replying' &&
activeComment.id === commentId;
export const isEditing = (activeComment, commentId) =>
activeComment &&
activeComment.type === 'editing' &&
activeComment.id === commentId;
export const readingTime = (body) => {
const wpm = 225;
const words = body.trim().split(/\s+/).length;
return `${Math.ceil(words / wpm)} min read`;
};
export const appendData = (data) => {
const formData = new FormData();
for (let [key, value] of Object.entries(data)) {
if (Array.isArray(value)) {
value = JSON.stringify(value);
}
formData.append(`${key}`, value);
}
return formData;
};
export const getReplies = (comments, commentId) => {
return (
comments &&
comments
.filter((comment) => comment && comment.parentId === commentId)
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
);
};
export const formatDate = (date) => {
const options = { year: 'numeric', month: 'short', day: 'numeric' };
const today = new Date(date);
return today.toLocaleDateString('en-US', options);
};
export const getRandomColor = () => {
const letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
};
export const renderRepeatedSkeletons = (element, count) => {
let skeletons = [];
for (let i = 0; i < count; i++) {
skeletons.push(element);
}
return skeletons;
};
export const renderAlternateSkeletons = (elementOne, elementTwo, count) => {
let skeletons = [];
for (let i = 0; i < count; i++) {
if (i % 2 === 0) {
skeletons.push(elementOne);
} else {
skeletons.push(elementTwo);
}
}
return skeletons;
};
Your renderRepeatedSkeletons function have to add a key to elements
export const renderRepeatedSkeletons = (element, count) => {
let skeletons = [];
for (let i = 0; i < count; i++) {
skeletons.push(<React.Fragment key={i} />{element}</React.Fragment>);
}
return skeletons;
};
I am building a clone of the Google Keep app with react js. I added all the basic functionality (expand the create area, add a note, delete it) but I can't seem to manage the edit part. Currently I am able to edit the inputs and store the values in the state, but how can I replace the initial input values for the new values that I type on the input?
This is Note component
export default function Note(props) {
const [editNote, setEditNote] = useState(false);
const [currentNote, setCurrentNote] = useState({
id: props.id,
editTitle: props.title,
editContent: props.content,
});
const handleDelete = () => {
props.deleteNote(props.id);
};
const handleEdit = () => {
setEditNote(true);
setCurrentNote((prevValue) => ({ ...prevValue }));
};
const handleInputEdit = (event) => {
const { name, value } = event.target;
setCurrentNote((prevValue) => ({
...prevValue,
[name]: value,
}));
};
const updateNote = () => {
setCurrentNote((prevValue, id) => {
if (currentNote.id === id) {
props.title = currentNote.editTitle;
props.content = currentNote.editContent;
} else {
return { ...prevValue };
}
});
setEditNote(false);
};
return (
<div>
{editNote ? (
<div className='note'>
<input
type='text'
name='edittitle'
defaultValue={currentNote.editTitle}
onChange={handleInputEdit}
className='edit-input'
/>
<textarea
name='editcontent'
defaultValue={currentNote.editContent}
row='1'
onChange={handleInputEdit}
className='edit-input'
/>
<button onClick={() => setEditNote(false)}>Cancel</button>
<button onClick={updateNote}>Save</button>
</div>
) : (
<div className='note' onDoubleClick={handleEdit}>
<h1>{props.title}</h1>
<p>{props.content}</p>
<button onClick={handleDelete}>DELETE</button>
</div>
)}
</div>
);
}
And this is the Container component where I am renderind the CreateArea and mapping the notes I create. I tried to map the notes again with the new values but it wasn't working.
export default function Container() {
const [notes, setNotes] = useState([]);
const addNote = (newNote) => {
setNotes((prevNotes) => {
return [...prevNotes, newNote];
});
};
const deleteNote = (id) => {
setNotes((prevNotes) => {
return prevNotes.filter((note, index) => {
return index !== id;
});
});
};
// const handleUpdateNote = (id, updatedNote) => {
// const updatedItem = notes.map((note, index) => {
// return index === id ? updatedNote : note;
// });
// setNotes(updatedItem);
// };
return (
<div>
<CreateArea addNote={addNote} />
{notes.map((note, index) => {
return (
<Note
key={index}
id={index}
title={note.title}
content={note.content}
deleteNote={deleteNote}
//handleUpdateNote={handleUpdateNote}
/>
);
})}
</div>
);
}
There are a couple of mistakes in your code.
The state properties are in the camel case
const [currentNote, setCurrentNote] = useState({
...
editTitle: props.title,
editContent: props.content,
});
But the names of the input are in lowercase.
<input
name='edittitle'
...
/>
<textarea
name='editcontent'
...
/>
Thus in handleInputEdit you don't update the state but add new properties: edittitle and editcontent. Change the names to the camel case.
In React you cant assign to the component prop values, they are read-only.
const updateNote = () => {
...
props.title = currentNote.editTitle;
props.content = currentNote.editContent;
You need to use the handleUpdateNote function passed by the parent component instead. You have it commented for some reason.
<Note
...
//handleUpdateNote={handleUpdateNote}
/>
Check the code below. I think it does what you need.
function Note({ id, title, content, handleUpdateNote, deleteNote }) {
const [editNote, setEditNote] = React.useState(false);
const [currentNote, setCurrentNote] = React.useState({
id,
editTitle: title,
editContent: content,
});
const handleDelete = () => {
deleteNote(id);
};
const handleEdit = () => {
setEditNote(true);
setCurrentNote((prevValue) => ({ ...prevValue }));
};
const handleInputEdit = (event) => {
const { name, value } = event.target;
setCurrentNote((prevValue) => ({
...prevValue,
[name]: value,
}));
};
const updateNote = () => {
handleUpdateNote({
id: currentNote.id,
title: currentNote.editTitle,
content: currentNote.editContent
});
setEditNote(false);
};
return (
<div>
{editNote ? (
<div className='note'>
<input
type='text'
name='editTitle'
defaultValue={currentNote.editTitle}
onChange={handleInputEdit}
className='edit-input'
/>
<textarea
name='editContent'
defaultValue={currentNote.editContent}
row='1'
onChange={handleInputEdit}
className='edit-input'
/>
<button onClick={() => setEditNote(false)}>Cancel</button>
<button onClick={updateNote}>Save</button>
</div>
) : (
<div className='note' onDoubleClick={handleEdit}>
<h1>{title}</h1>
<p>{content}</p>
<button onClick={handleDelete}>DELETE</button>
</div>
)}
</div>
);
}
function CreateArea() {
return null;
}
function Container() {
const [notes, setNotes] = React.useState([
{ title: 'Words', content: 'hello, bye' },
{ title: 'Food', content: 'milk, cheese' }
]);
const addNote = (newNote) => {
setNotes((prevNotes) => {
return [...prevNotes, newNote];
});
};
const deleteNote = (id) => {
setNotes((prevNotes) => {
return prevNotes.filter((note, index) => {
return index !== id;
});
});
};
const handleUpdateNote = ({ id, title, content }) => {
const _notes = [];
for (let i = 0; i < notes.length; i++) {
if (i === id) {
_notes.push({ id, title, content });
} else {
_notes.push(notes[i]);
}
}
setNotes(_notes);
};
return (
<div>
<CreateArea addNote={addNote} />
{notes.map((note, index) => {
return (
<Note
key={index}
id={index}
title={note.title}
content={note.content}
deleteNote={deleteNote}
handleUpdateNote={handleUpdateNote}
/>
);
})}
</div>
);
}
function App() {
return (
<div>
<Container />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
<script src="https://unpkg.com/react#17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#17/umd/react-dom.development.js" crossorigin></script>
<div id="root"></div>
Also, you can store the notes in an object or hash map instead of an array. For example
const [notes, setNotes] = React.useState({
'unique_id': { title: 'Words', content: 'hello, bye' }
});
Then in handleUpdateNote you have
setNotes((prev) => ({ ...prev, unique_id: { title, content } }))
I created a simple logic: when you click on a certain block, the classname changes, but the problem is that when you click on a certain block, the classname changes and the rest of the blocks looks like this
I need to change only the name of the class that I clicked, I think I need to use the index, but I don't quite understand how to reolize it
export default function SelectGradientTheme(props) {
const resultsRender = [];
const [borderColor, setBorderColor] = useState(false);
const setBorder = () => {
setBorderColor(!borderColor)
}
const borderColorClassName = borderColor ? "selectBorder" : null;
for (var i = 0; i < GradientThemes.length; i += 3) {
resultsRender.push(
<div className={"SelectThemePictures_Separator"}>
{
GradientThemes.slice(i, i + 3).map((col, index) => {
return (
<div key={index} className={borderColorClassName} onClick={() => props.SideBarPageContent(col)|| setBorder()}>
</div>
);
})
}
</div>
)
}
return (
<div className="SelectThemeWrapper">
{resultsRender}
</div>
);
};
You can remember the selected index
Please reference the following code:
export default function SelectGradientTheme(props) {
const resultsRender = [];
const [selectedIndex, setSelectedIndex] = useState(false);
const setBorder = (index) => {
setSelectedIndex(index);
};
for (var i = 0; i < GradientThemes.length; i += 3) {
resultsRender.push(
<div className={"SelectThemePictures_Separator"}>
{
GradientThemes.slice(i, i + 3).map((col, index) => {
return (
<div key={index}
className={index === selectedIndex ? 'selectBorder' : null}
onClick={() => props.SideBarPageContent(col)|| setBorder(index)}>
</div>
);
})
}
</div>
)
}
return (
<div className="SelectThemeWrapper">
{resultsRender}
</div>
);
};
I'm using a Searchable Tree component in Ant Design. I would like to filter treeNodes on search. So that if a search value is found, a treeNode is shown. If not, it is hidden (i. e. filtered).
There is a filterTreeNode prop in API. Is it possible to filter treeNodes (hide unrelevant treeNodes from search) with this prop? If not, how can achieve the desired result?
Here is my code for filterTreeNode function:
const filterTreeNode = (node) => {
const title = node.title.props.children[2];
const result = node.key.indexOf(searchValue) !== -1 ? true : false
console.log(searchValue);
console.log(result);
return result;
};
I can see that the result (true or false) is logged in the console, but with the Tree itself nothing happens.
Here is a link to codesandbox and the full code for the component:
import React, { useState } from "react";
import ReactDOM from "react-dom";
import { Tree, Input } from "antd";
import gData from "./gData.js";
const { Search } = Input;
const dataList = [];
const generateList = (data) => {
for (let i = 0; i < data.length; i++) {
const node = data[i];
const { key } = node;
dataList.push({ key, title: key });
if (node.children) {
generateList(node.children);
}
}
};
generateList(gData);
const getParentKey = (key, tree) => {
let parentKey;
for (let i = 0; i < tree.length; i++) {
const node = tree[i];
if (node.children) {
if (node.children.some((item) => item.key === key)) {
parentKey = node.key;
} else if (getParentKey(key, node.children)) {
parentKey = getParentKey(key, node.children);
}
}
}
return parentKey;
};
const SearchTree = () => {
const [expandedKeys, setExpandedKeys] = useState([]);
const [autoExpandParent, setAutoExpandParent] = useState(true);
const [searchValue, setSearchValue] = useState("");
const onExpand = (expandedKeys) => {
setExpandedKeys(expandedKeys);
setAutoExpandParent(false);
};
const onChange = (e) => {
const { value } = e.target;
const expandedKeys = dataList
.map((item) => {
if (item.title.indexOf(value) > -1) {
return getParentKey(item.key, gData);
}
return null;
})
.filter((item, i, self) => item && self.indexOf(item) === i);
if (value) {
setExpandedKeys(expandedKeys);
setSearchValue(value);
setAutoExpandParent(true);
} else {
setExpandedKeys([]);
setSearchValue("");
setAutoExpandParent(false);
}
};
const filterTreeNode = (node) => {
const title = node.title.props.children[2];
const result = title.indexOf(searchValue) !== -1 ? true : false;
console.log(searchValue);
console.log(result);
return result;
};
const loop = (data) =>
data.map((item) => {
const index = item.title.indexOf(searchValue);
const beforeStr = item.title.substr(0, index);
const afterStr = item.title.substr(index + searchValue.length);
const title =
index > -1 ? (
<span>
{beforeStr}
<span className="site-tree-search-value">{searchValue}</span>
{afterStr}
</span>
) : (
<span>{item.title}</span>
);
if (item.children) {
return { title, key: item.key, children: loop(item.children) };
}
return {
title,
key: item.key
};
});
return (
<div>
<Search
style={{ marginBottom: 8 }}
placeholder="Search"
onChange={onChange}
/>
<Tree
onExpand={onExpand}
expandedKeys={expandedKeys}
autoExpandParent={autoExpandParent}
treeData={loop(gData)}
filterTreeNode={filterTreeNode}
/>
</div>
);
};
ReactDOM.render(<SearchTree />, document.getElementById("container"));
As mentioned in the API document, filterTreeNode will highlight tree node, and will not hide it.
filterTreeNode
Defines a function to filter (highlight) treeNodes. When the function returns true, the corresponding treeNode will be highlighted
If you want to hide tree node, you will have to manually filter it first before before passing it to Tree in loop function, something like:
import React, { useState } from "react";
import ReactDOM from "react-dom";
import "antd/dist/antd.css";
import "./index.css";
import { Tree, Input } from "antd";
import gData from "./gData.js";
const { Search } = Input;
const dataList = [];
const generateList = (data) => {
for (let i = 0; i < data.length; i++) {
const node = data[i];
const { key } = node;
dataList.push({ key, title: key });
if (node.children) {
generateList(node.children);
}
}
};
generateList(gData);
const getParentKey = (key, tree) => {
let parentKey;
for (let i = 0; i < tree.length; i++) {
const node = tree[i];
if (node.children) {
if (node.children.some((item) => item.key === key)) {
parentKey = node.key;
} else if (getParentKey(key, node.children)) {
parentKey = getParentKey(key, node.children);
}
}
}
return parentKey;
};
const SearchTree = () => {
const [expandedKeys, setExpandedKeys] = useState([]);
const [autoExpandParent, setAutoExpandParent] = useState(true);
const [searchValue, setSearchValue] = useState("");
const [treeData, setTreeData] = useState(gData);
const onExpand = (expandedKeys) => {
setExpandedKeys(expandedKeys);
setAutoExpandParent(false);
};
const onChange = (e) => {
const value = e.target.value?.toLowerCase();
const expandedKeys = dataList
.map((item) => {
if (item.title.indexOf(value) > -1) {
return getParentKey(item.key, gData);
}
return null;
})
.filter((item, i, self) => item && self.indexOf(item) === i);
if (value) {
const hasSearchTerm = (n) => n.toLowerCase().indexOf(value) !== -1;
const filterData = (arr) =>
arr?.filter(
(n) => hasSearchTerm(n.title) || filterData(n.children)?.length > 0
);
const filteredData = filterData(gData).map((n) => {
return {
...n,
children: filterData(n.children)
};
});
setTreeData(filteredData);
setExpandedKeys(expandedKeys);
setSearchValue(value);
setAutoExpandParent(true);
} else {
setTreeData(gData);
setExpandedKeys([]);
setSearchValue("");
setAutoExpandParent(false);
}
};
const filterTreeNode = (node) => {
const title = node.title.props.children[2];
const result = title.indexOf(searchValue) !== -1 ? true : false;
console.log(searchValue);
console.log(result);
return result;
};
const loop = (data) =>
data.map((item) => {
const index = item.title.indexOf(searchValue);
const beforeStr = item.title.substr(0, index);
const afterStr = item.title.substr(index + searchValue.length);
const title =
index > -1 ? (
<span>
{beforeStr}
<span className="site-tree-search-value">{searchValue}</span>
{afterStr}
</span>
) : (
<span>{item.title}</span>
);
if (item.children) {
return { title, key: item.key, children: loop(item.children) };
}
return {
title,
key: item.key
};
});
return (
<div>
<Search
style={{ marginBottom: 8 }}
placeholder="Search"
onChange={onChange}
/>
<Tree
onExpand={onExpand}
expandedKeys={expandedKeys}
autoExpandParent={autoExpandParent}
treeData={loop(treeData)}
filterTreeNode={filterTreeNode}
/>
</div>
);
};
ReactDOM.render(<SearchTree />, document.getElementById("container"));
Couple weeks ago, i had to meet same like this client's enhancement. So how i did it was, we had Search over DirectoryTree and inside tree we rendered the nodes with a function
<DirectoryTree
onExpand={handleOperatorExpand}
expandedKeys={expandedKeys}
autoExpandParent={autoExpandParent}
onSelect={handleOperatorSelect}
>
{renderTreeNodes(mtoCharts)}
</DirectoryTree>
In search onChange event we saved values to a state, like searchValue. Then searchValue was checked in renderTreeNodes function with regex:
let dynamicRegex = `.*(${searchValue.toUpperCase()}){1}.*`;
if
item.name.match(dynamicRegex)
then we display the node otherwise we dont. SIMPLE !
UPDATE : codes
<Search onChange={handleOperatorChange} />
const handleOperatorChange = e => {
const { value } = e.target;
let temp = value.replace(/[\.,-\/#!$%\^&\*;:{}=\-_`~()"'+#<>?\\\[\]]/g, '');
setSearchValue(temp);
setAutoExpandParent(true);};
and finally renderTreeNodes :
const renderTreeNodes = data =>
data &&
data instanceof Array &&
data.map(item => {
const title = (
<Highlighter
highlightStyle={{ color: '#f50' }}
searchWords={[searchValue]}
autoEscape={true}
textToHighlight={item.name}
/>
);
if (searchValue) {
let dynamicRegex = `.*(${searchValue.toUpperCase()}){1}.*`;
if (!isEmpty(item.children)) {
// let doesChildrenHaveMatchingName = false;
// item.children.forEach(itemChild => {
// if (itemChild.name.match(dynamicRegex)) {
// doesChildrenHaveMatchingName = true;
// }
// });
// if (doesChildrenHaveMatchingName) {
return (
<TreeNode
className={!item.active ? 'dim-category' : ''}
key={item.code}
title={title}
id={item.id}
code={item.code}
level={item.level}
parentId={item.parentId}
parentReference={item.path}
icon={({ expanded }) => (
<Icon type={expanded ? 'folder-open' : 'folder'} style={{ color: '#32327f' }} />
)}
>
{renderTreeNodes(item.children)}
</TreeNode>
);
// }
}
if (item.name.match(dynamicRegex) || item.path.match(dynamicRegex)) {
return (
<TreeNode
className={item.active ? 'false' : 'dim-category'}
key={item.code}
title={!item.leaf ? title : getMtoTitle(item, title)}
{...(item.leaf ? { leaf: item.leaf } : {})}
id={item.id}
code={item.code}
level={item.level}
parentId={item.parentId}
parentReference={item.path}
icon={({ expanded }) => {
return item.leaf ? (
<Icon type="money-collect" style={{ color: '#47bfa5' }} />
) : (
<Icon type={expanded ? 'folder-open' : 'folder'} style={{ color: '#32327f' }} />
);
}}
/>
);
}
} else {
if (!isEmpty(item.children)) {
return (
<TreeNode
className={!item.active ? 'dim-category' : ''}
key={item.code}
title={title}
id={item.id}
code={item.code}
level={item.level}
parentId={item.parentId}
parentReference={item.path}
icon={({ expanded }) => (
<Icon type={expanded ? 'folder-open' : 'folder'} style={{ color: '#32327f' }} />
)}
>
{renderTreeNodes(item.children)}
</TreeNode>
);
}
return (
<TreeNode
className={item.active ? 'false' : 'dim-category'}
key={item.code}
title={!item.leaf ? title : getMtoTitle(item, title)}
{...(item.leaf ? { leaf: item.leaf } : {})}
id={item.id}
code={item.code}
level={item.level}
parentId={item.parentId}
parentReference={item.path}
icon={({ expanded }) => {
return item.leaf ? (
<Icon type="money-collect" style={{ color: '#47bfa5' }} />
) : (
<Icon type={expanded ? 'folder-open' : 'folder'} style={{ color: '#32327f' }} />
);
}}
/>
);
}
});
I have a tsx file which creates a table of comments. When the page is rendered, an array of information representing comments is gathered. A Set containing the indexes of the comments in the array, which is part of the state, determines whether a link under the comment reads 'show more' or 'show less'. Once that link is clicked, the index of the comment is added to the Set, and the state is updated with the addition of that index to the Set, if the Set contains the index of a comment when the state is updated, it should read 'show less'. The problem is when I click that link, it changes the link of each list element in the table, not just one.
import * as React from "react";
import { LocalizationInfo } from "emt-localization";
import { DateHandler } from "../handlers/date-handler";
import { UserIcon } from "./user-icon";
import { OnDownloadDocument } from "../models/generic-types";
import { getUserString } from "../models/user";
import { ClaimAction, ActionDetails, ReasonActionDetails, CommentDetails, DocumentDetails } from "../models/action";
const stateChangeActions = new Set<string>([
'Dispute',
'Rejection',
'SetUnderReview',
'Approval',
'Recall',
'Submission',
'RequestPartnerAction'
]);
const fiveMinutes = 5 * 60 * 1000;
const underscoreRegex = /_[^_]*_/g;
const actionTypeClassMap: {[actionType: string]: string} = {
'Submission': 'success',
'RequestPartnerAction': 'warn',
'Rejection': 'warn',
'Approval': 'success',
'Recall': 'warn',
'Dispute': 'warn',
'SetUnderReview': 'info',
'CustomerConsentDeclined': 'rejected'
};
const maxShortLength = 100;
interface ActionsProps {
readonly localization: LocalizationInfo;
readonly actions: ClaimAction[];
readonly onDownloadDocument: OnDownloadDocument;
readonly isInternalFacing: boolean;
readonly isHistoryPaneDisplay: boolean;
}
class ActionsState {
readonly expandedComments = new Set<number>();
}
export class Actions extends React.Component<ActionsProps, ActionsState> {
constructor(props: ActionsProps) {
super(props);
this.state = new ActionsState();
}
render():JSX.Element {
const loc = this.props.localization;
const isInternalFacing = this.props.isInternalFacing;
const toTime = (action:ClaimAction) => new Date(action.timeStamp).getTime();
const sortBy = (a:ClaimAction, b:ClaimAction) => toTime(b) - toTime(a);
const actions = this.props.actions.filter(action => {
if (isDocumentDetails(action.details)) {
if (action.actionType == 'DocumentSubmission' && action.details.documentType == 'Invoice') {
return false;
}
}
return true;
});
actions.sort(sortBy);
const grouped = groupActions(actions);
return (
<ul className={`claim-actions ${this.props.isHistoryPaneDisplay ? '' : 'user-comment-box'}`}>
{grouped.map((actions, index) => {
let actionClass = '';
actions.forEach(action => {
actionClass = actionClass || actionTypeClassMap[action.actionType];
});
const first = actions[0];
const icon = actionClass == 'success' ?
sequenceIcon('complete') :
actionClass == 'warn' ?
sequenceIcon('action-required') :
actionClass == 'rejected' ?
sequenceIcon('rejected') :
actionClass == 'info' ?
sequenceIcon('editing') :
<UserIcon
user={first.user}
isInternalFacing={isInternalFacing}
/>;
const elements = actions.map((action, actionIndex) => this.renderAction(action, actionIndex));
return (
<li className={actionClass} key={index}>
{icon}
<div className="win-color-fg-secondary">
<span className="claim-action-name win-color-fg-primary">
{ getUserString(first.user, isInternalFacing) }
</span>
<span className="text-caption">
{loc.piece("HistoryItemTitle", 0)}
{DateHandler.friendlyDate(first.timeStamp, true)}
</span>
</div>
{elements}
</li>
)
})}
</ul>
)
}
private renderAction(action:ClaimAction, actionIndex:number):JSX.Element|null {
const strings = this.props.localization.strings;
if (action.actionType == 'AddComments' || action.actionType == 'AddInternalComments') {
return this.renderComment((action.details as CommentDetails).comments, actionIndex);
}
const document = isDocumentDetails(action.details) ? action.details : null;
const documentLink = document ?
(key:number) =>
<a
key={key}
onClick={() => this.props.onDownloadDocument(document.documentId, document.name)}>
{document.name}
</a>
: null;
const locKey = `History_${action.actionType}`;
const localizedFlat = strings[locKey] || "";
const localized = replaceUnderscores(localizedFlat, documentLink);
const reason = (action.actionType === 'RequestPartnerAction' || action.actionType === 'Rejection') && action.details ? (action.details as ReasonActionDetails).reasonCode : '';
const reasonString = reason.charAt(0).toUpperCase() + reason.slice(1)
if (localized) {
return (
<div key={actionIndex}>
<div className="claim-action">
<span className="text-caption">{localized}</span>
</div>
<div className="claim-action">
{ reasonString && <span className="text-caption"><strong>{strings['ReasonLabel']}</strong>{` ${strings[reasonString]}`}</span> }
</div>
</div>
);
}
console.error(`Unknown action type ${action.actionType}`);
return null;
}
private renderComment(comment: string, actionIndex: number): JSX.Element {
const strings = this.props.localization.strings;
const canShorten = comment.length > maxShortLength;
const shouldShorten = canShorten && !this.state.expandedComments.has(actionIndex);
const shortened = shouldShorten ?
comment.substring(0, maxShortLength) + "\u2026" :
comment;
const paragraphs = shortened
.split('\n')
.map(s => s.trim())
.filter(s => s);
const elements = paragraphs.map((comment, i) =>
<div className="claim-comment" key={i}>
{comment}
</div>
);
const toggle = () => {
const next = new Set<number>(this.state.expandedComments);
if (next.has(actionIndex)) {
next.delete(actionIndex)
}
else {
next.add(actionIndex);
}
this.setState({ expandedComments: next });
}
const makeLink = (locKey:string) =>
<a
onClick={toggle}>{strings[locKey]}</a>;
const afterLink = canShorten ?
shouldShorten ?
makeLink('ShowMore') :
makeLink('ShowLess') :
null;
return (
<React.Fragment key={actionIndex}>
{elements}
{afterLink}
</React.Fragment>
);
}
}
// Function groups actions together under some conditions
function groupActions(actions:ClaimAction[]):ClaimAction[][] {
const grouped:ClaimAction[][] = [];
actions.forEach(action => {
if (grouped.length) {
const lastGroup = grouped[grouped.length - 1];
const timeDifference = new Date(lastGroup[0].timeStamp).getTime() - new Date(action.timeStamp).getTime();
if (stateChangeActions.has(lastGroup[0].actionType) && action.actionType == 'AddComments' && timeDifference < fiveMinutes) {
lastGroup.push(action);
return;
}
}
grouped.push([action]);
});
return grouped;
}
function isDocumentDetails(details:ActionDetails|null): details is DocumentDetails {
return !!details && (details.$concreteClass == 'InvoiceActionDetails' || details.$concreteClass == 'DocumentActionDetails');
}
function sequenceIcon(className: string):JSX.Element {
return (
<div className="sequence sequence-status claims-icon">
<div className={`step ${className}`} />
</div>
);
}
function replaceUnderscores(str: string, documentLink: ((k:number)=>JSX.Element)|null, startKey: number=0):JSX.Element[] {
if (!str) {
return [];
}
const match = underscoreRegex.exec(str);
if (!match) {
return replaceDocumentLink(str, documentLink, startKey);
}
const firstText = str.substring(0, match.index);
const middleText = match[0].substring(1, match[0].length - 1);
const lastText = str.substring(match.index + match[0].length);
const first = replaceUnderscores(firstText, documentLink, startKey);
const middle = [<strong className={'claims-emphasis'} key={startKey + first.length}>{middleText}</strong>];
const last = replaceUnderscores(lastText, documentLink, startKey + first.length + 1);
return first.concat(middle, last);
}
function replaceDocumentLink(str: string, documentLink: ((k:number)=>JSX.Element)|null, startKey: number=0):JSX.Element[] {
const replaceIndex = str.indexOf('{0}');
if (replaceIndex >= 0 && documentLink) {
return [
<React.Fragment key={startKey}>{str.substring(0, replaceIndex)}</React.Fragment>,
documentLink(startKey+1),
<React.Fragment key={startKey + 2}>{str.substring(replaceIndex+3)}</React.Fragment>
];
}
return [<React.Fragment key={startKey}>{str}</React.Fragment>];
}