How to map Github GraphQL API commit responses to react-bootstrap Cards? - reactjs

All the following code is in a custom component titled CommitCards.
Given the following gql query using React Apollo.
const GET_REPO_COMMITS = gql`
query GetRepoCommits($repoName: String!) {
repository(name: $repoName, owner: "FernandoH-G") {
defaultBranchRef {
target {
... on Commit {
history(first: 5) {
edges {
node {
pushedDate
message
url
}
}
}
}
}
}
}
}
`;
const repoName = props.rName
const { loading, error, data } = useQuery(GET_REPO_COMMITS, {
variables: { repoName },
});
if (loading) return (
<p>Loading...</p>
);
if (error) return (
<p>Error.</p>
);
I am able to get the last 5 commits from a given repository belonging to the given owner.
Given by the nature of how GraphQL's JSON response is structured, I feel the need to do the following:
const commits = data.repository.defaultBranchRef.target.history.edges
const innerCommits = commits.map(com =>(
com.node
))
Neither mapping over commits or innerCommits using more or less the following react-strap Card code:
return commits.map(com => {
<Card
key={com.node.url}
border="info">
<Card.Body>
<Card.Header as="h4"> {com.node.pushDate} </Card.Header>
<Card.Text> {com.node.message}</Card.Text>
</Card.Body>
</Card>
})
renders the cards on the screen.
Note that using the following test html does display the proper information, just as a single long string.
return(
<p>{commits.map( a => (
a.node.message
))}</p>
)
The component is called here:
<CardDeck>
<CommitCards rName={value} />
</CardDeck>

You might be missing the Bootstrap CSS that is required.
Add this import somewhere towards the top level of your app, like index.js or App.js:
import "bootstrap/dist/css/bootstrap.min.css";
See more: https://react-bootstrap.github.io/getting-started/introduction#stylesheets

So I figured it out...
return commits.map(com => {
<Card
key={com.node.url}
border="info">
<Card.Body>
<Card.Header as="h4"> {com.node.pushDate} </Card.Header>
<Card.Text> {com.node.message}</Card.Text>
</Card.Body>
</Card>
})
should have been this:
return commits.map(com => (
<Card
key={com.node.url}
border="info">
<Card.Body>
<Card.Header as="h4"> {com.node.pushDate} </Card.Header>
<Card.Text> {com.node.message}</Card.Text>
</Card.Body>
</Card>
))
Note the ( vs the { .

Related

Testing-librairy : How to check a text when it is returned by a function

I have a react component which displays a text of which some words may change depending on the value of the object
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Card, Col, Row } from 'react-bootstrap';
function RenderPressMedia(props: any) {
const { t } = useTranslation();
const [pressPlanned, setPressPlanned] = useState<any[]>([]);
useEffect(() => {
if (!props.pressPlannedData) return;
setPressPlanned(props.pressPlannedData);
}, [props.pressPlannedData]);
const renderMedia = (media: string) => {
switch (media) {
case 'PHONE':
return t('press.media.phone');
case 'LETTER':
return t('press.media.letter');
case 'EMAIL':
return t('press.media.email');
case 'SMS':
return t('press.media.sms');
}
};
const renderPress = (media: string) => {
return (
<>
{t(`press.text`, {
media: renderMedia(media),
})}
</>
);
};
return (
<Row>
{pressPlanned.length > 0 &&
pressPlanned.map((press, index) => (
<Col lg={12} className="col-main" key={index}>
<Card>
<Card.Body>
<ul className="d-flex flex-row m-0 list-unstyled align-items-center">
<li aria-label="Reminder to do or reminder for today">
{renderPress(press.media)}
</li>
</ul>
</Card.Body>
</Card>
</Col>
))}
</Row>
);
}
export default RenderPressMedia;
renderPressMedia function returns a translation which will change depending on the variable sent.
Component that works very well.
But when I want to perform a test on this component with testing-library, I cannot verify the content returned by the pressMedia function.
Here is the test carried out.
import React from 'react';
import { render, screen } from '#testing-library/react';
import RenderPressMedia from '../render-press-media';
const data: any[] = [
{
id: 65,
media: 'LETTER',
firstPlannedDate: '2021-09-03',
plannedDate: '2021-09-03',
comment: 'autogenerated',
createdDate: '2021-08-27T09:43:52',
lastModifiedDate: '2021-08-27T09:43:52',
},
];
describe('<RenderPressMedia/>', () => {
it('should display an render press media with data', () => {
//given
render(<RenderPressMedia pressPlannedData={data} />);
//then
expect(screen.getByText(/letter/i)).toBeInTheDocument();
});
});
The test consists in verifying the presence of the word 'letter' returned by the pressMedia function
expect(screen.getByText(/letter/i)).toBeInTheDocument();
But unfortunately I got an error message
Unable to find an element with the text: /courrier/i. This could be because the
text is broken up by multiple elements. In this case, you can provide a function
for your text matcher to make your matcher more flexible.
Ignored nodes: comments, <script />, <style />
<body>
<div>
<div
class="row"
>
<div
class="col-main col-lg-12"
>
<div
class="card"
>
<div
class="card-body"
>
<ul
class="d-flex flex-row m-0 list-unstyled align-items-center"
>
<li
aria-label="Reminder to do or reminder for today"
>
press.text
</li>
</ul>
</div>
</div>
</div>
</div>
TestingLibraryElementError: Unable to find an element with the text: /courrier/i.
This could be because the text is broken up by multiple elements. In this case, you
can provide a function for your text matcher to make your matcher more flexible.
the pressMedia function in testingLibrairy does not return its contents so impossible to verify the presence of the word letter, return value is press.text
If you have a solution to correct this problem.
It looks like, it's happening because you're using the i18n functions, but also it's mocked and just gives back to you the text which received as a parameter.
Any case, in this case, what you're doing is pretty well. But also you're testing the translation. So, it can be more complicated and hard to maintain.
So, I'd recommend to test if it's using the correct media would be something like:
<li aria-label="Reminder to do or reminder for today" data-testid="component" data-media={press.media}>
{renderPress(press.media)}
</li>
And in the testing part:
expect(screen.getByTestId('component')).toHaveAttribute('data-media', data.media);
In this case, ignoring the language, You know which kind of media has your component.
I created this github repo as example:
Repo: https://github.com/joseglego/test-testing-library/
Component: https://github.com/joseglego/test-testing-library/blob/main/src/RenderPressMedia.js
Test: https://github.com/joseglego/test-testing-library/blob/main/src/RenderPressMedia.test.js
Specific commit: https://github.com/joseglego/test-testing-library/commit/f3c222f92313c909c8d4a1f359daf01bdd0f880d
Basically, that test is running on my local.

Get location path from use Location hook inside a column renderer from react-bootstrap-table2

Story
I'm creating 2 pages (Summary and Cycles pages) using react.js.
On the Summary page, there is a column named CN that every item links to the Cycles page.
Summary page has a path /route/summary/location=abc and Cycles page has a path /route/cycle/location=abc/deviceId=4410
For example, if I click the value from CN column in the first row of the table inside the Summary page, I will be redirected to the Cycles page with the path /route/cycle/location=abc/deviceId=4410.
In the Summary page, I use https://github.com/react-bootstrap-table/react-bootstrap-table2 for the table component and I use a columnRenderer function inside columns.js to render a custom item inside the table like this one:
Question
How can I put the pathname (example "abc") to a Link component inside cnColumnRenderer function in columns.js?
Ideal Condition I wanted:
Summary page with the path: /route/summary/location=abc
Cycles page with the path: /route/cycle/location=abc/deviceId=4410
Actual Condition:
Error because of invalid hook call while rendering the Summary page
My Code:
table code inside Summary page (inside Summary.js):
hint: focus on columns variable from './columns' and its implementation
import React from "react"
import { useLocation } from "react-router-dom"
import BootstrapTable from 'react-bootstrap-table-next';
import paginationFactory, {
PaginationProvider,
PaginationListStandalone
} from 'react-bootstrap-table2-paginator';
import ToolkitProvider, { Search } from 'react-bootstrap-table2-toolkit';
import columns from './columns'
const Summary = () => {
const location = useLocation()
const locationName = location['pathname'].replace('/route/summary/location=', '')
// console.log('Summary, location:', locationName)
// example: location = "/route/summary/location=abc" and locationName = "abc"
// ...some code here...
return (
<React.Fragment>
<div className="container-fluid ppa-route-summary-root">
<Row>
<Col className="col-12">
{/* ...some code here... */}
{/* TABLE CARD */}
<Card>
<CardBody>
<PaginationProvider
pagination={paginationFactory(paginationOptions)}
keyField='id'
columns={columns}
data={tableData}
>
{
({ paginationProps, paginationTableProps }) => (
<ToolkitProvider
keyField='id'
columns={columns}
data={tableData}
search
>
{
toolkitProps => (
<React.Fragment>
{/* ...some code here... */}
{/* TABLE ITSELF */}
<Row>
<Col xl="12">
<div className="table-responsive">
{
isTableLoading ?
<ReactLoading
type={'spin'}
color={'crimson'}
height={'5rem'}
width={'5rem'}
className='table-loading'
/> :
<BootstrapTable
keyField={"id"}
responsive
bordered={false}
striped={false}
// defaultSorted={defaultSorted}
// selectRow={selectRow}
classes={
"table align-middle table-nowrap"
}
headerWrapperClasses={"thead-light"}
{...toolkitProps.baseProps}
{...paginationTableProps}
/>
}
</div>
</Col>
</Row>
{/* ...some code here... */}
</React.Fragment>
)
}
</ToolkitProvider>
)
}
</PaginationProvider>
</CardBody>
</Card>
</Col>
</Row>
</div>
</React.Fragment>
)
}
export default Summary
columns.js:
import React from 'react'
import { Link, useLocation } from 'react-router-dom'
// IMAGES
import IconLocation from '../../../images/icons/location.svg'
const cnColumnRenderer = (cell, row, rowIndex, formatExtraData) => {
// console.log('columns, cnColumnRenderer:', cell, row, rowIndex, formatExtraData)
const deviceVersion = cell.split('-')[1] // example: deviceVersion = "4410"
const location = useLocation()
// console.log('Summary columns, location:', location['pathname'])
// here is the pathname I wanted: "/route/cycle/location=abc" so I can take the "location" path value as below:
const locationName = location['pathname'].replace('/route/summary/location=', '') // the result is locationName = "abc"
// then put the locationName inside the Link component
return(
<div className='route-summary-cn'>
<img src={IconLocation} alt='' className='icon-location'/>
{/* below is the pathname I wanted: "/route/cycle/location=abc/deviceId=4410" */}
<Link to={`/route/summary/location=${locationName}/deviceId=${row['deviceId']}`}>
{deviceVersion}
</Link>
</div>
)
}
const columns = [
{
dataField: 'deviceName',
text: 'CN',
formatter: cnColumnRenderer,
sort: true
},
{
dataField: 'convertedTotalCycle',
text: 'Cycle',
sort: true,
},
// ...some code here...
]
export default columns
Note: let me know if the question is confusing. I will try to update it.
React hooks are only valid in React functional components, not in any callbacks, loops, conditional blocks. If you need the location data in the callback it needs to be passed in.
From what I can tell it seems you need to move the columns.js code into the main component so the location values can be closed over in scope.
const Summary = () => {
const location = useLocation();
const locationName = location['pathname'].replace('/route/summary/location=', '')
// example: location = "/route/summary/location=abc" and locationName = "abc"
// ...some code here...
const cnColumnRenderer = (cell, row, rowIndex, formatExtraData) => {
// console.log('columns, cnColumnRenderer:', cell, row, rowIndex, formatExtraData)
const deviceVersion = cell.split('-')[1] // example: deviceVersion = "4410"
// then put the locationName inside the Link component
return(
<div className='route-summary-cn'>
<img src={IconLocation} alt='' className='icon-location'/>
{/* below is the pathname I wanted: "/route/cycle/location=abc/deviceId=4410" */}
<Link to={`/route/summary/location=${locationName}/deviceId=${row['deviceId']}`}>
{deviceVersion}
</Link>
</div>
);
};
const columns = [
{
dataField: 'deviceName',
text: 'CN',
formatter: cnColumnRenderer,
sort: true
},
{
dataField: 'convertedTotalCycle',
text: 'Cycle',
sort: true,
},
// ...some code here...
];
return (
...
);

How to handle "Cannot read property 'node' of null" in a query

I am pretty new to React/Gatsby and am doing a query with Apollo.
The thing is that if there is no title or image it'll say "Cannot read property 'node' of null". I get that because if I do not set a title or image in my headless CMS there's no data to read.
How can I make it conditional so that if 'title' is empty don't render it. Any other suggestions or tips about my code are always welcome!
Here's an example of my code
import React from "react"
import Container from "react-bootstrap/Container"
import Image from "react-bootstrap/Image"
import { useQuery, gql } from "#apollo/client"
const APOLLO_QUERY = gql`
{
posts {
nodes {
title
databaseId
content(format: RENDERED)
featuredImage {
node {
sourceUrl
}
}
}
}
}
`
const ApolloTest = () => {
const { data } = useQuery(APOLLO_QUERY)
return (
<Container>
{data &&
data.posts.nodes.map(post => {
return (
<article key={post.databaseId}>
<h3>{post.title}</h3>
<p>{post.content}</p>
<Image
src={post.featuredImage.node.sourceUrl}
alt={post.title}
style={{ width: "150px" }}
fluid
></Image>
</article>
)
})}
</Container>
)
}
export default ApolloTest
I would recommend short-circuit evaluation to first check if the information exists prior to trying to render it. A quick example is {post.title && <h3>{post.title}</h3>}, which will only render the h3 and everything inside it if post.title is truthy. You can extend this to work for the image as well:
return (
<Container>
{data?.posts.nodes.map(post => {
return (
<article key={post.databaseId}>
{post.title && <h3>{post.title}</h3>}
<p>{post.content}</p>
{post.featuredImage && <Image
src={post.featuredImage.node.sourceUrl}
alt={post.title}
style={{ width: "150px" }}
fluid
/>}
</article>
)
})}
</Container>
)

React-bootstrap cards not wrapping

My Code is:
import React, { useEffect, useState } from "react";
import styles from "./Cards.module.css";
import { CardDeck, Card } from "react-bootstrap";
const Cards = ({ animeArray }) => {
const [aanimeArray, setAnimeArray] = useState([]);
useEffect(() => {
setAnimeArray(animeArray);
}, [animeArray]);
if (!aanimeArray) {
return;
}
console.log("Anime Array", aanimeArray);
return (
<div className={styles.container}>
{aanimeArray === [] ? (
<h1>Search</h1>
) : (
<CardDeck>
{aanimeArray.map((anime) => {
return (
<Card>
<Card.Img variant = "top" src={anime.image_url} />
<Card.Body>
<Card.Title>{anime.title}</Card.Title>
</Card.Body>
<Card.Footer>
<small className="text-muted">{anime.rated}</small>
</Card.Footer>
</Card>
);
})}
</CardDeck>
)}
</div>
);
};
export default Cards;
I am not using any custom styling whatsoever.
The result of the above mentioned code is as seen on this image:
Image of the issue
You have to make the effort of making them wrap. In fact, as seen on the documentation, majority of the developers' examples includes the CSS property width with a value of 18rem.
Here is an example by leveraging minWidth:
const sampleStyle = {
minWidth: "20%",
flexGrow: 0
};
<Card style={sampleStyle}>
First thing.
aanimeArray === []
won't work since you are comparing an array with another array.
Best way in Javascript for this is to check the length of the array.
aanimeArray.length === 0
means it is an empty array.
About the styling I think you need to show us the CSS code as well. I'm not sure what CardDeck component does...

antd SubMenu "TypeError: Cannot read property 'isRootMenu' of undefined"

I use antd 3.15 and GraphQL to fetch data and generate a list of SubMenu and Menu.Item inside of Menu. However, I got the error message like this Uncaught TypeError: Cannot read property 'isRootMenu' of undefined I have no idea what is wrong with my code. isRootMenu is not a prop listed anywhere on the doc. ant.design/components/menu/#header and when I hardcoded all the SubMenu and Menu.List there is no problem. Can I iterate data from GraphQL to generate the SubMenu and Menu.List?
Can someone help me with this issue, please? Thank you! Here is my code:
import * as React from 'react';
import './SideNav.scss';
import { Menu, Icon } from 'antd';
import gql from 'graphql-tag';
import { Query } from 'react-apollo';
const FLOORS_QUERY = gql`
query {
getAllFloors {
id
floorName
rooms {
id
roomName
roomNumber
roomDescription
}
}
}
`;
export default class SideNav extends React.Component {
render() {
return (
<Menu theme="light" defaultSelectedKeys={['1']} mode="inline">
<Query query={FLOORS_QUERY}>
{({ loading, error, data }) => {
if (loading) return <h4> loading... </h4>;
if (error) console.log(error);
console.log(data);
return (
<React.Fragment>
{data.getAllFloors.map((floor) => (
<SubMenu
key={floor.id}
title={
<span>
<Icon type="plus" />
<span>{floor.floorName}</span>
</span>
}
>
<React.Fragment>
{floor.rooms.map((room) => (
<Menu.Item key={room.id}>{room.roomNumber}</Menu.Item>
))}
</React.Fragment>
</SubMenu>
))}
</React.Fragment>
);
}}
</Query>
</Menu>
);
}
}
You should pass the props to the submenu.
const CustomComponent = (props) => (
<Menu.SubMenu title='SubMenu' {...props}>
<Menu.Item>SubMenuItem</Menu.Item>
</Menu.SubMenu>
)
so a solution to your question would be to do the following;
move the query outside of the containing menu
pass the props to the SubMenu
const FloorMapSubMenu = ({ id, floorName, rooms, ...other }) => {
return (
<Menu.SubMenu
key={id}
title={
<span>
<Icon type="plus" />
<span>{floorName}</span>
</span>
}
{...other} // notice the other props, this is were the 'isRootMenu' is injected from the <Menu> children
>
<React.Fragment>
{rooms.map((room) => (
<Menu.Item key={room.id}>{room.roomNumber}</Menu.Item>
))}
</React.Fragment>
</Menu.SubMenu>
)
}
class SideNav extends React.Component {
render() {
return (
<Query query={FLOORS_QUERY}>
{({ loading, error, data }) => {
if (loading) return <h4> loading... </h4>
if (error) console.log(error)
console.log(data)
return (
<Menu theme='light' defaultSelectedKeys={['1']} mode='inline'>
{data.getAllFloors.map((floor, i) => (
<FloorMapSubMenu key={i} id={floor.id} floorName={floor.floorName} rooms={floor.rooms} />
))}
</Menu>
)
}}
</Query>
)
}
}
I found out the Ant design SubMenu needs to use the parent to check some properties like isRootMenu at
SubMenu.js:260
getPopupContainer = props.parentMenu.isRootMenu ? props.parentMenu.props.getPopupContainer : function (triggerNode) {
return triggerNode.parentNode;
}
In order to solve it you need to manually pass parent props into SubMenu like
<Menu.SubMenu {...this.props}/>
to solve the problem. Hope this helps u
Related Github issue item https://github.com/react-component/menu/issues/255
I had this issue while trying to add a <div> as a child of Menu. I just added an empty Menu.Item as the first child of my menu, and the error went away.
<Menu>
<Menu.Item style={{display: 'none'}} />
<div>...</div>
</Menu>
I ran into the same issue. It seems Antd does not allow to place arbitrary components into a Menu/SubMenu. My guess is that Menu.Item needs to get some props, which are passed from Menu/SubMenu to its children.
So you can either create a custom component that passes all props down, or remove the inner <React.Fragment> declaration (the one that is inside the SubMenu), which is not needed anyway.
I was able to make it work by putting the <Query> Component at the top:
<Query query={FLOORS_QUERY}>
{({ loading, error, data }) => {
if (loading) return <Spin />;
if (error) console.log(error);
console.log(data);
return (
<Menu theme="light" defaultSelectedKeys={['1']} mode="inline">
{data.getAllFloors.map((floor) => (
<SubMenu
key={floor.id}
title={
<Link to="/{floor.id}">
<span>
<Icon type="plus" />
<span>{floor.floorName}</span>
</span>
</Link>
}
>
{floor.rooms.map((room) => (
<Menu.Item key={room.id} onClick={this.showRoomProfile}>
{room.roomNumber}
</Menu.Item>
))}
</SubMenu>
))}
</Menu>
);
}}
</Query>
According to the Typescript definitions the childrens of Menu should be of kind Item, SubMenu, ItemGroup or Divider. If you must place a different component on the Header, wrap the Menu and the desired component on a Header component component as such:
import { Layout } from 'antd';
const { Header, Footer, Sider, Content } = Layout;
<Layout>
<Layout.Header>
<div className="logo" />
<Menu>
<Menu.Item key="1">nav 1</Menu.Item>
<Menu.Item key="2">nav 2</Menu.Item>
</Menu>
<Layout.Header>
</Layout>
I have run into the same issue. But my issues was I have using ternary condition to show some menu's dynamically inside part used the <></> element. That caused me this issue. Once removed that everything work fine.

Resources