Custom react-admin drag & drop list - reactjs

It can't drag. What is wrong with it?
I'm using react-sortable-hoc with material-ui to custom react-admin list page with drag & drop sortable.
Demo : https://codesandbox.io/s/vibrant-visvesvaraya-4k3gs
Source code: https://github.com/tangbearrrr/poc-ra-sort-drag/tree/main

As I checked you are getting data from the props and in props there is no data field exists, so the error is coming from there
Here is the all props list

The sortable method you are using is from react-sortable-hoc, which adds huge complexity to react-admin.
Not so fast, I have run out of attempts trying to debug your code and come up with another solution works just fine but not so ideal, is to use sortablejs:
yarn add sortablejs
yarn add #types/sortablejs --dev
Do not mess up with react-sortablejs, this also applies the same complexity level as react-sortable-hoc.
Let's use your cmsLanguage as an example, with changes to use Datagrid instead.
Just be reminded that this working solution needs several retries on null el (e.g. your data is fetching, slow network speed, etc). The code below has 3 retries, 1500 milliseconds per each retry. The initialisation will stop after 3 attempts.
import {Datagrid, ShowButton, TextField} from "react-admin";
import * as React from "react";
import MenuIcon from '#mui/icons-material/Menu';
import {useEffect} from "react";
import Sortable from 'sortablejs';
const LanguageList = () => {
// This will run the effect after every render
useEffect(() => {
// https://github.com/SortableJS/Sortable
const retries = 3;
const millisecords = 1500;
let attempts = 0;
const retrySortable = () => {
const el = document.querySelector('#sortable-list tbody');
if (!el) {
if (++attempts >= retries) {
console.log(`cannot initialise sortable after ${retries} retries.`);
} else {
setTimeout(() => retrySortable(), millisecords);
}
} else {
// #ts-ignore
new Sortable(el, {
handle: ".handle",
draggable: "tr",
animation: 150, // ms, animation speed moving items when sorting, `0` — without animation
easing: "cubic-bezier(1, 0, 0, 1)", // Easing for animation. Defaults to null. See https://easings.net/ for examples.
// Element dragging ended
onEnd: (evt) => {
// #ts-ignore
const reorderedList: string[] = [];
const list = document.querySelectorAll('#sortable-list tbody td.column-name span');
[].forEach.call(list, function (span: Element) {
reorderedList.push(span.innerHTML);
});
console.log(JSON.stringify(reorderedList));
console.log(evt);
},
});
}
}
retrySortable();
}, []);
return (
<section id="sortable-list">
<Datagrid>
<MenuIcon sx={{cursor: "pointer"}} className="handle"/>
<TextField source="name"/>
<ShowButton/>
</Datagrid>
</section>
);
};
export default LanguageList;
When someone has a request for a demo, I will draw some time to make this a GitHub repo for better reference.

Related

React - How to get unit test coverage for toggling open a popover that uses state or determine if it should be open

I'm trying to get unit test coverage for the code in red (see screenshot) using react-testing-library. Would anyone know what unit test would cover this? I'm still learning the react-testing-library. TIA
screenshot of code here showing red, uncovered code
If you don't open the screenshot above, the code inside this function is what needs to be covered.
const togglePopover = () => {
setToolTipOpen((prev) => !prev);
};
actual full component code block:
import React, { FunctionComponent, useState, KeyboardEvent, useRef, useEffect } from 'react';
import styles from './InfoPopover.module.scss';
import { Popover, PopoverBody } from 'x'
import { PopperPlacementType } from '#material-ui/core';
import { ReactComponent as InfoIcon } from '../../../assets/icons/tooltipIcon.svg';
export interface PopperProps {
placement?: PopperPlacementType;
tipMessage: React.ReactNode | string;
stringedTipMessage: string;
}
const InfoPopover: FunctionComponent<PopperProps> = ({
placement,
tipMessage,
stringedTipMessage
}: PopperProps) => {
const [toolTipOpen, setToolTipOpen] = useState(false);
const togglePopover = () => {
setToolTipOpen((prev) => !prev);
};
const handleBlur = () => {
setToolTipOpen(false);
};
return (
<>
<button
id="popoverTarget"
className={styles.tooltipButton}
onBlur={handleBlur}
aria-label={`Tooltip Content - ${stringedTipMessage}`}
>
<InfoIcon aria-label="status tooltip" />
</button>
<Popover
target="popoverTarget"
trigger="legacy"
toggle={togglePopover}
placement={placement}
isOpen={toolTipOpen}
arrowClassName={styles.toolTipArrow}
popperClassName={styles.toolTipPopout}
>
<PopoverBody>{tipMessage}</PopoverBody>
</Popover>
</>
);
};
export default InfoPopover;
With React Testing Library, the approach is to test what the user can see/do rather than test the internals of your application.
With your example, assuming you are trying to test a simple open/close popup user flow then the user would be seeing a button, and when they activate that button they would see a popover. A simple RTL approach would be as follows:
const popoverTipMessage = "My popover message";
render(<InfoPopover tipMessage={popoverTipMessage} />);
// Popover isn't activated, so it shouldn't be in the DOM
expect(screen.getByText(popoverTipMessage)).not.toBeInDocument();
// Find button and click it to show the Popover
fireEvent.click(screen.getByRole('button', {
name: /tooltip content/i
}));
// Popover should now be activated, so check if it's visible (in the DOM)
await waitFor(() => {
// This relies on RTL's text matching to find the component.
expect(screen.getByText(popoverTipMessage)).toBeInDocument();
});
// Find button and click it again to hide the Popover
fireEvent.click(screen.getByRole('button', {
name: /tooltip content/i
}));
// Popover should now be hidden, so check if the DOM element has gone
// Note: There are other ways of checking appearance/disappearance. Check the RTL docs.
await waitFor(() => {
// This relies on RTL's text matching to find the component but there are other better ways to find an element
expect(screen.getByText(popoverTipMessage)).not.toBeInDocument();
});
The query methods I've used above are some of the basic ones, however RTL has many different queries to find the element you need to target. It has accessibility at the forefront of its design so leans heavily on these. Take a look in the docs: https://testing-library.com/docs/react-testing-library/example-intro

Why I'm getting Uncaught TypeError: Cannot read properties of null (reading 'add') on deployment?

I'm working on a editor app using fabric.js.On localhost, When i click on Add Circle it works fine while on deployment it is causing cannot read properties of null.
Here is the code:
I'm using react context api by which i add objects in canvas and it displays on screen.
FabricCircle.js
import { fabric } from 'fabric';
import ContextCanvas from '../../../context/ContextCanvas';
import { Button } from '#chakra-ui/react';
const FabricTextBox = () => {
const [canvas] = useContext(ContextCanvas);
function addTextBox() {
const textbox = new fabric.Textbox('Click on the Rectangle to move it.', {
fontSize: 20,
left: 50,
top: 100,
width: 200,
fill: 'black',
color: 'white',
cornerColor: 'blue',
});
canvas.add(textbox);
canvas.requestRenderAll();
}
return (
<>
<Button
type="button"
colorScheme="blue"
onClick={addTextBox}
variant={'ghost'}
_hover={{}}
_focus={{}}
_active={{}}
textColor={'white'}
fontWeight={'light'}
>
Text Field
</Button>
</>
);
};
export default FabricTextBox;
FabricCanvas.js
import React, { useContext, useLayoutEffect } from 'react';
import { fabric } from 'fabric';
import ContextCanvas from '../../context/ContextCanvas';
const FabricCanvas = () => {
const [canvas, initCanvas] = useContext(ContextCanvas);
useLayoutEffect(() => {
return () => {
initCanvas(new fabric.Canvas('c'));
};
}, []);
return (
<>
<canvas
id="c"
width={window.innerWidth}
height={window.innerHeight}
/>
</>
)
}
export default FabricCanvas;
ContextCanvas.js
import { fabric } from 'fabric';
const ContextCanvas = createContext();
export function CanvasProvider({ children }) {
const [canvas, setCanvas] = useState(null);
const initCanvas = c => {
setCanvas(c);
c.renderAll();
};
return (
<ContextCanvas.Provider value={[canvas, initCanvas]}>
{children}
</ContextCanvas.Provider>
);
}
export default ContextCanvas;
I think the error is related to this line in FabricCircle.js
canvas.add(textbox);
^^^^
because your canvas object is null in production.
Assuming you use React.StrictMode
Using React.StrictMode
With Strict Mode starting in React 18, whenever a component mounts in development, React will simulate immediately unmounting and remounting the component:
Strict mode flow (read more)
* React mounts the component.
* Layout effects are created.
* Effect effects are created.
* React simulates effects being destroyed on a mounted component.
* Layout effects are destroyed. [THIS]
* Effects are destroyed.
* React simulates effects being re-created on a mounted component.
* Layout effects are created
* Effect setup code runs
The step marked with [THIS] is what makes you feel all right in the local environment (but it's an illusion... See why in the next section).
With that been said:
Your canvas is initialized null inside useState and in the useLayoutEffect, you are calling the initCanvas method inside the cleanup function (so it will only be called when it's too late in production, while in development with StrictMode act like an init-function although it's a cleanup function).
useLayoutEffect(() => {
// Your code should be here
return () => { // The returned function is the cleanup function
// This is executed only when disposing the component.
initCanvas(new fabric.Canvas('c'));
// But when strict mode is active
// the component is disposed and re-mounted immidiatly.
};
}, []);
This is why the local environment works and the production environment doesn't.
Solution
Try updating your useLayoutEffect like this:
useLayoutEffect(() => {
initCanvas(new fabric.Canvas('c'));
}, []);
Conclusion
You should not initialize your state inside a cleanup function.
In this case, React.StrictMode behavior prevents you from realizing the error (without strict mode, it wouldn't even work in development).
Since you were initializing the canvas inside the cleanup function, the canvas never get initialized in time (without the strict mode), remaining null, as the error you receive states.

Moving slider with Cypress

I've got a Slider component from rc-slider and I need Cypress to set the value of it.
<Slider
min={5000}
max={40000}
step={500}
value={this.state.input.amount}
defaultValue={this.state.input.amount}
className="sliderBorrow"
onChange={(value) => this.updateInput("amount",value)}
data-cy={"input-slider"}
/>
This is my Cypress code:
it.only("Changing slider", () => {
cy.visit("/");
cy.get(".sliderBorrow")
.invoke("val", 23000)
.trigger("change")
.click({ force: true })
});
What I've tried so far does not work.
Starting point of slider is 20000, and after test runs it goes to 22000, no matter what value I pass, any number range.
Looks like it used to work before, How do interact correctly with a range input (slider) in Cypress? but not anymore.
The answer is very and very simple. I found the solution coincidentally pressing enter key for my another test(date picker) and realized that pressing left or right arrow keys works for slider.
You can achieve the same result using props as well. The only thing you need to do is to add this dependency: cypress-react-selector and following instructions here: cypress-react-selector
Example of using {rightarrow}
it("using arrow keys", () => {
cy.visit("localhost:3000");
const currentValue = 20000;
const targetValue = 35000;
const increment = 500;
const steps = (targetValue - currentValue) / increment;
const arrows = '{rightarrow}'.repeat(steps);
cy.get('.rc-slider-handle')
.should('have.attr', 'aria-valuenow', 20000)
.type(arrows)
cy.get('.rc-slider-handle')
.should('have.attr', 'aria-valuenow', 35000)
})
#darkseid's answer helped guide me reach an optimal solution.
There are two steps
Click the slider's circle, to move the current focus on the slider.
Press the keyboard arrow buttons to reach your desired value.
My slider jumps between values on the sliders, therefore this method would work. (I am using Ion range slider)
This method doesn't require any additional depedency.
// Move the focus to slider, by clicking on the slider's circle element
cy.get(".irs-handle.single").click({ multiple: true, force: true });
// Press right arrow two times
cy.get(".irs-handle.single").type(
"{rightarrow}{rightarrow}"
);
You might be able to tackle this using Application actions, provided you are able to modify the app source code slightly.
Application actions give the test a hook into the app that can be used to modify the internal state of the app.
I tested it with a Function component exposing setValue from the useState() hook.
You have used a Class component, so I guess you would expose this.updateInput() instead, something like
if (window.Cypress) {
window.app = { updateInput: this.updateInput };
}
App: index.js
import React, { useState } from 'react';
import { render } from 'react-dom';
import './style.css';
import Slider from 'rc-slider';
import 'rc-slider/assets/index.css';
function App() {
const [value, setValue] = useState(20000);
// Expose the setValue() method so that Cypress can set the app state
if (window.Cypress) {
window.app = { setValue };
}
return (
<div className="App">
<Slider
min={5000}
max={40000}
step={500}
value={value}
defaultValue={value}
className="sliderBorrow"
onChange={val => setValue(val)}
data-cy={"input-slider"}
/>
<div style={{ marginTop: 40 }}><b>Selected Value: </b>{value}</div>
</div>
);
}
render(<App />, document.getElementById('root'));
Test: slider.spec.js
The easiest way I found assert the value in the test is to use the aria-valuenow attribute of the slider handle, but you may have another way of testing that the value has visibly changed on the page.
describe('Slider', () => {
it("Changing slider", () => {
cy.visit("localhost:3000");
cy.get('.rc-slider-handle')
.should('have.attr', 'aria-valuenow', 20000)
cy.window().then(win => {
win.app.setValue(35000);
})
cy.get('.rc-slider-handle')
.should('have.attr', 'aria-valuenow', 35000)
})
})
For whoever comes across this with Material UI/MUI 5+ Sliders:
First off, this github issue and comment might be useful: https://github.com/cypress-io/cypress/issues/1570#issuecomment-606445818.
I tried changing the value by accessing the input with type range that is used underneath in the slider, but for me that did not do the trick.
My solution with MUI 5+ Slider:
<Slider
disabled={false}
step={5}
marks
data-cy="control-percentage"
name="control-percentage"
defaultValue={0}
onChange={(event, newValue) =>
//Handle change
}
/>
What is important here is the enabled marks property. This allowed me to just click straight on the marks in the cypress test, which of course can also be abstracted to a support function.
cy.get('[data-cy=control-percentage]').within(() => {
// index 11 represents 55 in this case, depending on your step setting.
cy.get('span[data-index=11]').click();
});
I got this to work with the popular react-easy-swipe:
cy.get('[data-cy=week-picker-swipe-container]')
.trigger('touchstart', {
touches: [{ pageY: 0, pageX: 0 }]
})
.trigger('touchmove', {
touches: [{ pageY: 0, pageX: -30 }]
})

Forge Viewer - Markups extension inside of a react app

I am trying to use the forge viewer with markups extension inside of a react app and have ran into a problem.
The viewer is docked within a sheet that slides out from the right handside of the page. The first time I open the viewer it works fine, I click the markup icon and can draw an arrow e.g.:
When I reopen the same document, click the markup icon and draw another arrow, the arrow is huge, like the scale is all wrong:
This is the full react component so far
import React, { useRef, useEffect } from 'react';
import { ReadonlyFormHTMLContainer } from '../../Components/Form/FormComponents';
import { useGlobalContext } from '../../GlobalState';
type ForgeViewerProps = {
id: string;
fileUrl: string;
filename: string;
};
let viewer: Autodesk.Viewing.Viewer3D | null | undefined;
// I can't find a type for Autodesk.Viewing.MarkupsCore - if you can find it, replace any with the correct type, if not you are on your own, no intellisense!
let markup: any;
export const ForgeViewer = (props: ForgeViewerProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const context = useGlobalContext();
useEffect(() => {
window.Autodesk.Viewing.Initializer({ env: 'Local', useADP: false }, () => {
viewer = new window.Autodesk.Viewing.GuiViewer3D(containerRef.current!);
viewer.start();
viewer.loadExtension('Autodesk.PDF').then(() => {
viewer!.loadModel(props.fileUrl, viewer!);
viewer!.loadExtension('Autodesk.Viewing.MarkupsCore').then(ext => {
markup = ext;
});
viewer!.loadExtension('Autodesk.Viewing.MarkupsGui');
});
});
return () => {
console.log('Running clean up');
viewer?.tearDown();
viewer?.finish();
viewer = null;
};
}, []);
return (
<ReadonlyFormHTMLContainer
title={'Document markup'}
subtitle={props.filename}
formErrorMessage=""
formErrorTitle=""
onClose={() => context.hideSheet()}
>
<div ref={containerRef}></div>{' '}
</ReadonlyFormHTMLContainer>
);
};
I have imported following modules from npm:
Forge version: https://developer.api.autodesk.com/modelderivative/v2/viewers/7.*/viewer3D.js
Does anyone have any idea on how to resolve this? As you can see I've tried some clean up code when the component unmounts but I cannot get the viewer to work "normally" after the initial open.
Edit
repro link: https://github.com/philwindsor/forge-repro/blob/master/index.html
This is a timing issue.
When you open the viewer for the first time, the markup extension takes some time to download and it is therefore initialized after the model has already been loaded. Because of that, the extension knows how to initialize the scale of its markups properly.
When you open the viewer for the second time, the markup extension is already available, and it is loaded and initialized before any model is available. And because of that, it cannot configure the "expected" scale.
To resolve the issue, simply load the Autodesk.Viewing.MarkupsCore and Autodesk.Viewing.MarkupsGui extensions after the model is loaded, for example:
viewer.loadModel(urn, options, function onSuccess() {
viewer.loadExtension("Autodesk.Viewing.MarkupsCore");
viewer.loadExtension("Autodesk.Viewing.MarkupsGui");
});

React execute script if window width

I have a button in React that executes a function onClick. I want to get rid of the button, and instead programmatically execute the function if window width < 1000px.
A restriction is that I can not add a plugin.
Here's what the code looks like...
// Do I need useState, useEffect?
import React, { PureComponent } from "react";
class MainNav extends PureComponent {
state = {
// Does something go here? What goes here and how do I use
// state to execute the function?
navIsCollapsed: false,
};
// this controls rendering of huge images
toggleShowImages() {
this.setState({
navIsCollapsed: !this.state.navIsCollapsed,
});
}
// I want this to be executed by width < 1000
handleSideNavToggle = () => {
this.toggleShowImages(); // controls if React renders components
document.body.classList.toggle("side-nav-closed");
}
Here's render the button that's currently executing the function. I want width < 1000 to programmatically execute its function.
// window width < 1000 should execute this function
<div onClick={this.handleSideNavToggle}>Don't render huge images</div>
// here are the images the function conditionally renders
<should show images &&
<div>Massive huge image</div>
<div>Massive huge image</div>
<div>Massive huge image</div>
>
I could use CSS media query to show or hide the massive images I don't want, but that's horrible use of React.
I've looked and tried to implement similar questions on SO that either invoke plugins, are out of date, or the use case is too different (for example, "re-render everything based on screen size"). I've also tried to React-ify vanilla javascript. This seems like it ought to be simple to do but I can't make it work.
Any React wizards out there who can answer with a clean, efficient solution?
Use the above method that Mathis Delaunay mentioned to get viewport/window width, then to get rid of that button. Just simply add a condition to whether render it or not and then watch on state changes to trigger the function.
Here I use hooks to do it
import React, { useEffect, useState } from "react";
import ReactDOM from "react-dom";
function App() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
function handleResize() {
setWidth(window.innerWidth);
}
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, [width]);
useEffect(() => {
width < 600 && handleSideNavToggle();
},[width]);
function handleSideNavToggle() {
console.log("toggle it");
}
return (
<div className="App">
{width > 600 && (
<button onClick={() => handleSideNavToggle()}>
Don't render huge images
</button>
)}
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Here is a working example. I set the width to be handled as 600 to make it easy to see.
https://codesandbox.io/s/react-hooks-counter-demo-w9wgv
Try looking at this answer, i think it is what your are searching for :
Get viewport/window height in ReactJS
You just need to check in the updateWindowDimension if the window.innerWidth is under 1000, if so, change the css button property to display : none; or visibility: hidden;.

Resources