Custom List component - issues on modules import - admin-on-rest

I'm making a custom List component with a custom empty list message. But refresh and actions are not executed any more. I suspect I do not import properly 'DefaultActions' & 'refreshViewAction', do you see the issue?
I dont understand what is the issue on my imports as I took the 'admin-on-rest' List version, changed imports and customized it.
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { parse, stringify } from 'query-string';
import { push as pushAction } from 'react-router-redux';
import { Card, CardText } from 'material-ui/Card';
import compose from 'recompose/compose';
import { createSelector } from 'reselect';
import inflection from 'inflection';
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import autoprefixer from 'material-ui/utils/autoprefixer';
import { queryReducer } from 'admin-on-rest';
/*
import queryReducer, {
SET_SORT,
SET_PAGE,
SET_FILTER,
SORT_DESC,
} from '../../reducer/admin/resource/list/queryReducer';
*/
//import ViewTitle from '../layout/ViewTitle';
import { ViewTitle } from 'admin-on-rest';
//import Title from '../layout/Title';
import { Title } from 'admin-on-rest';
//import DefaultPagination from './Pagination';
import { Pagination as DefaultPagination } from 'admin-on-rest';
//import DefaultActions from './Actions';
// TODO : Bug on ref import here
import { Actions as DefaultActions } from 'admin-on-rest';
//import { crudGetList as crudGetListAction } from '../../actions/dataActions';
import { crudGetList as crudGetListAction } from 'admin-on-rest';
//import { changeListParams as changeListParamsAction } from '../../actions/listActions';
import { changeListParams as changeListParamsAction } from 'admin-on-rest';
//import { refreshView as refreshViewAction } from '../../actions/uiActions';
// TODO : Bug on ref import here
import { refreshView as refreshViewAction } from 'admin-on-rest/lib/actions/uiActions';
//import translate from '../../i18n/translate';
import { translate } from 'admin-on-rest';
//import removeKey from '../../util/removeKey';
import { removeKey } from 'admin-on-rest';
//import defaultTheme from '../defaultTheme';
import { defaultTheme } from 'admin-on-rest';
//import withPermissionsFilteredChildren from '../../auth/withPermissionsFilteredChildren';
import withPermissionsFilteredChildren from 'admin-on-rest/lib/auth/withPermissionsFilteredChildren';
import { LinkToUpload } from './ListActions'
import { WithPermission } from 'admin-on-rest'
import authClient from '../../authClient'
const { SET_SORT,
SET_PAGE,
SET_FILTER,
SORT_DESC, } = queryReducer
const styles = {
noResults: { padding: 20 },
header: {
display: 'flex',
justifyContent: 'space-between',
},
};
/**
* List page component
*
* The <List> component renders the list layout (title, buttons, filters, pagination),
* and fetches the list of records from the REST API.
* It then delegates the rendering of the list of records to its child component.
* Usually, it's a <Datagrid>, responsible for displaying a table with one row for each post.
*
* In Redux terms, <List> is a connected component, and <Datagrid> is a dumb component.
*
* Props:
* - title
* - perPage
* - sort
* - filter (the permanent filter to apply to the query)
* - actions
* - filters (a React Element used to display the filter form)
* - pagination
*
* #example
* const PostFilter = (props) => (
* <Filter {...props}>
* <TextInput label="Search" source="q" alwaysOn />
* <TextInput label="Title" source="title" />
* </Filter>
* );
* export const PostList = (props) => (
* <List {...props}
* title="List of posts"
* sort={{ field: 'published_at' }}
* filter={{ is_published: true }}
* filters={<PostFilter />}
* >
* <Datagrid>
* <TextField source="id" />
* <TextField source="title" />
* <EditButton />
* </Datagrid>
* </List>
* );
*/
export class List extends Component {
state = {};
componentDidMount() {
this.updateData();
if (Object.keys(this.props.query).length > 0) {
this.props.changeListParams(this.props.resource, this.props.query);
}
}
componentWillReceiveProps(nextProps) {
if (
nextProps.resource !== this.props.resource ||
nextProps.query.sort !== this.props.query.sort ||
nextProps.query.order !== this.props.query.order ||
nextProps.query.page !== this.props.query.page ||
nextProps.query.filter !== this.props.query.filter
) {
this.updateData(
Object.keys(nextProps.query).length > 0
? nextProps.query
: nextProps.params
);
}
if (nextProps.version !== this.props.version) {
this.updateData();
}
}
shouldComponentUpdate(nextProps, nextState) {
if (
nextProps.isLoading === this.props.isLoading &&
nextProps.width === this.props.width &&
nextProps.version === this.props.version &&
nextState === this.state
) {
return false;
}
return true;
}
getBasePath() {
return this.props.location.pathname.replace(/\/$/, '');
}
/**
* Merge list params from 3 different sources:
* - the query string
* - the params stored in the state (from previous navigation)
* - the props passed to the List component
*/
getQuery() {
const query =
Object.keys(this.props.query).length > 0
? this.props.query
: { ...this.props.params };
if (!query.sort) {
query.sort = this.props.sort.field;
query.order = this.props.sort.order;
}
if (!query.perPage) {
query.perPage = this.props.perPage;
}
return query;
}
updateData(query) {
const params = query || this.getQuery();
const { sort, order, page, perPage, filter } = params;
const pagination = {
page: parseInt(page, 10),
perPage: parseInt(perPage, 10),
};
const permanentFilter = this.props.filter;
this.props.crudGetList(
this.props.resource,
pagination,
{ field: sort, order },
{ ...filter, ...permanentFilter }
);
}
setSort = sort => this.changeParams({ type: SET_SORT, payload: sort });
setPage = page => this.changeParams({ type: SET_PAGE, payload: page });
setFilters = filters =>
this.changeParams({ type: SET_FILTER, payload: filters });
showFilter = (filterName, defaultValue) => {
this.setState({ [filterName]: true });
if (typeof defaultValue !== 'undefined') {
this.setFilters({
...this.props.filterValues,
[filterName]: defaultValue,
});
}
};
hideFilter = filterName => {
this.setState({ [filterName]: false });
const newFilters = removeKey(this.props.filterValues, filterName);
this.setFilters(newFilters);
};
changeParams(action) {
const newParams = queryReducer(this.getQuery(), action);
this.props.push({
...this.props.location,
search: `?${stringify({
...newParams,
filter: JSON.stringify(newParams.filter),
})}`,
});
this.props.changeListParams(this.props.resource, newParams);
}
refresh = () => {
if (process.env !== 'production') {
console.warn( // eslint-disable-line
'Deprecation warning: The preferred way to refresh the List view is to connect your custom button with redux and dispatch the `refreshView` action.'
);
}
this.props.refreshView();
};
render() {
const {
children,
filters,
pagination = <DefaultPagination />,
actions = <DefaultActions />,
resource,
hasCreate,
title,
data,
ids,
total,
isLoading,
translate,
theme,
version,
} = this.props;
const query = this.getQuery();
const filterValues = query.filter;
const basePath = this.getBasePath();
const resourceName = translate(`resources.${resource}.name`, {
smart_count: 2,
_: inflection.humanize(inflection.pluralize(resource)),
});
const defaultTitle = translate('aor.page.list', {
name: `${resourceName}`,
});
const titleElement = (
<Title title={title} defaultTitle={defaultTitle} />
);
const muiTheme = getMuiTheme(theme);
const prefix = autoprefixer(muiTheme);
return (
<div className="list-page">
<Card style={{ opacity: isLoading ? 0.8 : 1 }}>
<div style={prefix(styles.header)}>
<ViewTitle title={titleElement} />
{actions &&
React.cloneElement(actions, {
resource,
filters,
filterValues,
basePath,
hasCreate,
displayedFilters: this.state,
showFilter: this.showFilter,
theme,
refresh: this.refresh,
})}
</div>
{filters &&
React.cloneElement(filters, {
resource,
hideFilter: this.hideFilter,
filterValues,
displayedFilters: this.state,
setFilters: this.setFilters,
context: 'form',
})}
{isLoading || total > 0 ? (
<div key={version}>
{children &&
React.cloneElement(children, {
resource,
ids,
data,
currentSort: {
field: query.sort,
order: query.order,
},
basePath,
isLoading,
setSort: this.setSort,
})}
{pagination &&
React.cloneElement(pagination, {
total,
page: parseInt(query.page, 10),
perPage: parseInt(query.perPage, 10),
setPage: this.setPage,
})}
</div>
) : (
<CardText style={styles.noResults}>
You haven't uploaded the workforce database yet.
<WithPermission value={['admin', 'project-leader']} authClient={authClient} >
<LinkToUpload _projectId={filterValues._projectId} />
</WithPermission>
</CardText>
)}
</Card>
</div>
);
}
}
List.propTypes = {
// the props you can change
title: PropTypes.any,
filter: PropTypes.object,
filters: PropTypes.element,
pagination: PropTypes.element,
actions: PropTypes.element,
perPage: PropTypes.number.isRequired,
sort: PropTypes.shape({
field: PropTypes.string,
order: PropTypes.string,
}),
children: PropTypes.node,
// the props managed by admin-on-rest
authClient: PropTypes.func,
changeListParams: PropTypes.func.isRequired,
crudGetList: PropTypes.func.isRequired,
data: PropTypes.object, // eslint-disable-line react/forbid-prop-types
filterValues: PropTypes.object, // eslint-disable-line react/forbid-prop-types
hasCreate: PropTypes.bool.isRequired,
ids: PropTypes.array,
isLoading: PropTypes.bool.isRequired,
location: PropTypes.object.isRequired,
path: PropTypes.string,
params: PropTypes.object.isRequired,
push: PropTypes.func.isRequired,
query: PropTypes.object.isRequired,
resource: PropTypes.string.isRequired,
refreshView: PropTypes.func.isRequired,
total: PropTypes.number.isRequired,
translate: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired,
version: PropTypes.number.isRequired,
};
List.defaultProps = {
filter: {},
filterValues: {},
perPage: 10,
sort: {
field: 'id',
order: SORT_DESC,
},
theme: defaultTheme,
};
const getLocationSearch = props => props.location.search;
const getQuery = createSelector(getLocationSearch, locationSearch => {
const query = parse(locationSearch);
if (query.filter && typeof query.filter === 'string') {
query.filter = JSON.parse(query.filter);
}
return query;
});
function mapStateToProps(state, props) {
const resourceState = state.admin.resources[props.resource];
return {
query: getQuery(props),
params: resourceState.list.params,
ids: resourceState.list.ids,
total: resourceState.list.total,
data: resourceState.data,
isLoading: state.admin.loading > 0,
filterValues: resourceState.list.params.filter,
version: state.admin.ui.viewVersion,
};
}
const enhance = compose(
connect(mapStateToProps, {
crudGetList: crudGetListAction,
changeListParams: changeListParamsAction,
push: pushAction,
refreshView: refreshViewAction,
}),
translate,
withPermissionsFilteredChildren
);
export default enhance(List);

You should put all your imports from admin-on-rest on the same line:
import { ViewTitle, Title, [ALL OTHERS HERE TOO] } from 'admin-on-rest';

Related

Why are the components not rerendering after Response?

Table with pinia store equipmentlist, fethced from usequipments but table does not rerender
VueTableComponent
<script setup lang="ts">
import { useEquipmentStore } from '~/store/equipmentStore'
import useEquipments from '~/composables/equipments'
const { getEquipmentList } = useEquipments()
onBeforeMount(() => { getEquipmentList() })
const state = useEquipmentStore()
const equipmentList: any = state.equipmentList
const loaded = state.loaded
</script>
<template>
<el-table :key="loaded" :data="equipmentList" style="width: 100%">
<el-table-column type="expand">
<template #default="props">
ID: {{ props.row.id }}
</template>
</el-table-column>
<el-table-column label="ID" prop="id" />
<el-table-column label="Name" prop="name" />
</el-table>
</template>
Typescript File for all CRUD Operations equipment.ts
import { useRouter } from 'vue-router'
import http from '../http-common'
import { useEquipmentStore } from '~/store/equipmentStore'
export default function useEquipments() {
const state = useEquipmentStore()
const errors = ref([]) // array of strings
const router = useRouter()
const getEquipmentList = async() => {
try {
console.log(state.equipmentList.length)
const response = await http.get('/equipment/list')
state.equipmentList = response.data
console.log(state.equipmentList.length)
console.log(state.equipmentList[0])
}
catch (error: any) {
console.log(error.message)
}
}
Equipment(Pinia)Store
import { defineStore } from 'pinia'
import type { Ref } from 'vue'
export const useEquipmentStore = defineStore('equipment', {
state: () => ({
equipment: ref({}) as any,
equipmentList: ref([]) as Ref<any[]>,
}),
actions: {
reset() {
this.equipment = {}
this.equipmentList = []
},
},
})
1. i called several times getEquipment list and it is faster done then i stored an initial equipment, 2. i clicked on the link on the left and fetched several times more and as u can see there is something fetchd but not displayed, 3. after repeating to home and again to Link the component is there and alll other next fetches do indeed function well
Main.ts
app.component('InputText', InputText)
app.mount('#app')
useEquipments().initDB()
}
same fetching class equipment.ts
const initDB = () => {
try {
if (state.equipmentList.length === 0) { storeEquipment({ id: 1, name: 'Firma' }) }
else {
for (const equipment of state.equipmentList) {
console.log(equipment)
if (equipment === 'Firma')
state.equipmentList.splice(state.equipmentList.indexOf(equipment), 1)
}
}
}
catch (error: any) {
console.log(error.message)
}
}

Implement force-directed graph in next js

I'm trying to create a force-directed graph for mapping the interactions between courses in an institution. Using Next JS + TypeScript for my frontend.
Have tried several attempts at charting this out using react-flow, dagre, vis-network but am getting either a window : undefined error or just the damn alignment of nodes not being force-directed inside the box I have defined.
Before I move on with implementing d3-force right out of the box, can someone please recommend any alternative solution to this ?
Here's what my nodes & edges look like :
Here's my attempt with reactflow & dagre :
import React, { useCallback, useEffect, useState } from 'react';
import ReactFlow, {
addEdge,
useNodesState,
useEdgesState,
Edge,
Node,
Position,
ConnectionLineType,
ReactFlowProvider,
MiniMap,
Controls,
Background,
} from 'react-flow-renderer';
import dagre from 'dagre';
import { NodeData, useCourseNodes } from 'src/hooks/useCourseNodes';
import { useDepartment } from '#contexts/ActiveDepartmentContext';
import {
useUpdateActiveCourse,
} from '#contexts/ActiveCourseContext';
import { useDrawerOpen, useUpdateDrawerOpen } from '#contexts/DrawerContext';
const dagreGraph = new dagre.graphlib.Graph({directed:true});
dagreGraph.setDefaultEdgeLabel(() => ({}));
const nodeWidth = 10.2;
const nodeHeight = 6.6;
const getLayoutedElements = (
nodes: Node[],
edges:Edge[],
) => {
// const isHorizontal = direction === 'LR';
dagreGraph.setGraph( {width:900, height:900, nodesep:20, ranker:'longest-path' });
nodes.forEach((node: Node) => {
dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
});
edges.forEach((edge: Edge) => {
dagreGraph.setEdge(edge.source, edge.target);
});
dagre.layout(dagreGraph);
nodes.forEach((node) => {
const nodeWithPosition = dagreGraph.node(node.id);
// node.targetPosition = isHorizontal ? Position.Left : Position.Top;
// node.sourcePosition = isHorizontal ? Position.Right : Position.Bottom;
node.targetPosition = Position.Top;
node.sourcePosition = Position.Bottom;
// We are shifting the dagre node position (anchor=center center) to the top left
// so it matches the React Flow node anchor point (top left).
node.position = {
x: nodeWithPosition.x - nodeWidth / 2,
y: nodeWithPosition.y - nodeHeight / 2,
};
console.log(nodeWithPosition)
return node;
})
return { layoutedNodes:nodes, layoutedEdges:edges };
};
const LayoutFlow = () => {
const activeDept = useDepartment();
const setActiveCourse = useUpdateActiveCourse();
const setDrawerOpen = useUpdateDrawerOpen()
const drawerOpen = useDrawerOpen();
const {courseList, edgeList} = useCourseNodes()
const { layoutedNodes, layoutedEdges } = getLayoutedElements(courseList, edgeList)
const [nodes, setNodes, onNodesChange] = useNodesState(layoutedNodes);
const [edges, setEdges,onEdgesChange] = useEdgesState(layoutedEdges);
console.log(layoutedNodes)
const onConnect = useCallback(
(params) =>
setEdges((eds) =>
addEdge({ ...params, type: ConnectionLineType.SimpleBezier, animated: true }, eds),
),
[],
);
// ? For switching between layouts (horizontal & vertical) for phone & desktop
// const onLayout = useCallback(
// (direction) => {
// const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(
// nodes,
// edges,
// direction
// );
// setNodes([...layoutedNodes]);
// setEdges([...layoutedEdges]);
// },
// [nodes, edges]
// );
// ? M1 - for force re-rendering react flow graph on state change - https://github.com/wbkd/react-flow/issues/1168
// ? M2 - (Applied currently in useEffect block below)for force re-rendering react flow graph on state change - https://github.com/wbkd/react-flow/issues/1168
useEffect(() => {
const {layoutedNodes, layoutedEdges} = getLayoutedElements(courseList, edgeList)
setNodes([...layoutedNodes]);
setEdges([...layoutedEdges]);
}, [activeDept, drawerOpen]);
return (
<div style={{ width: '100%', height: '100%' }} className="layoutflow">
<ReactFlowProvider>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onNodeClick={(e: React.MouseEvent, node: Node<NodeData>) => {
e.preventDefault();
// created a copy of the node since we're only deleting the "label" property from the node object to conveniently map the rest of the data to the "data" property of the active course
const nodeCopy = JSON.parse(JSON.stringify(node))
const { data } = nodeCopy;
const { label } = data
delete data.label
setActiveCourse({
courseId: label,
data
});
setDrawerOpen(true);
}}
connectionLineType={ConnectionLineType.SimpleBezier}
fitView
>
<MiniMap />
<Controls />
{/* <Background /> */}
</ReactFlow>
</ReactFlowProvider>
<div className="controls">
{/* <button onClick={() => onLayout('TB')}>vertical layout</button>
<button onClick={() => onLayout('LR')}>horizontal layout</button> */}
</div>
</div>
);
};
export default LayoutFlow;
Here's my attempt with vis-network : (note : I did slightly modify edges to have from-to instead of source-target when working with this)
import { useCourseNodes } from "#hooks/useCourseNodes";
import React, { useEffect, useRef } from "react";
import { Network } from "vis-network";
const GraphLayoutFour: React.FC = () => {
const {courseList:nodes, edgeList:edges} = useCourseNodes()
// Create a ref to provide DOM access
const visJsRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const network =
visJsRef.current &&
new Network(visJsRef.current, { nodes, edges } );
// Use `network` here to configure events, etc
}, [visJsRef, nodes, edges]);
return typeof window !== "undefined" ? <div ref={visJsRef} /> : <p>NOT AVAILABLE</p>;
};
export default GraphLayoutFour;
Here's my attempt with react-sigma
import React, { ReactNode, useEffect, useState } from "react";
import ReactDOM from "react-dom";
import { UndirectedGraph } from "graphology";
import erdosRenyi from "graphology-generators/random/erdos-renyi";
import randomLayout from "graphology-layout/random";
import chroma from "chroma-js";
import { Attributes } from "graphology-types";
import { ControlsContainer, ForceAtlasControl, SearchControl, SigmaContainer, useLoadGraph, useRegisterEvents, useSetSettings, useSigma, ZoomControl } from "react-sigma-v2/lib/esm";
interface MyCustomGraphProps {
children?: ReactNode;
}
export const MyCustomGraph: React.FC<MyCustomGraphProps> = ({ children }) => {
const sigma = useSigma();
const registerEvents = useRegisterEvents();
const loadGraph = useLoadGraph();
const setSettings = useSetSettings();
const [hoveredNode, setHoveredNode] = useState<any>(null);
useEffect(() => {
// Create the graph
const graph = erdosRenyi(UndirectedGraph, { order: 100, probability: 0.2 });
randomLayout.assign(graph);
graph.nodes().forEach(node => {
graph.mergeNodeAttributes(node, {
label: "label",
size: Math.max(4, Math.random() * 10),
color: chroma.random().hex(),
});
});
loadGraph(graph);
// Register the events
registerEvents({
enterNode: event => setHoveredNode(event.node),
leaveNode: () => setHoveredNode(null),
});
}, []);
useEffect(() => {
setSettings({
nodeReducer: (node, data) => {
const graph = sigma.getGraph();
const newData: Attributes = { ...data, highlighted: data.highlighted || false };
if (hoveredNode) {
//TODO : add type safety
if (node === hoveredNode || (graph as any).neighbors(hoveredNode).includes(node)) {
newData.highlighted = true;
} else {
newData.color = "#E2E2E2";
newData.highlighted = false;
}
}
return newData;
},
edgeReducer: (edge, data) => {
const graph = sigma.getGraph();
const newData = { ...data, hidden: false };
//TODO : add type safety
if (hoveredNode && !(graph as any).extremities(edge).includes(hoveredNode)) {
newData.hidden = true;
}
return newData;
},
});
}, [hoveredNode]);
return <>{children}</>;
};
ReactDOM.render(
<React.StrictMode>
<SigmaContainer>
<MyCustomGraph />
<ControlsContainer position={"bottom-right"}>
<ZoomControl />
<ForceAtlasControl autoRunFor={2000} />
</ControlsContainer>
<ControlsContainer position={"top-right"}>
<SearchControl />
</ControlsContainer>
</SigmaContainer>
</React.StrictMode>,
document.getElementById("root"),
);
import { useCourseNodes } from '#hooks/useCourseNodes'
import dynamic from 'next/dynamic';
import React from 'react'
import { useSigma } from 'react-sigma-v2/lib/esm';
const GraphLayoutThree = () => {
const isBrowser = () => typeof window !== "undefined"
const { courseList, edgeList } = useCourseNodes()
const sigma = useSigma();
if(isBrowser) {
const SigmaContainer = dynamic(import("react-sigma-v2").then(mod => mod.SigmaContainer), {ssr: false});
const MyGraph = dynamic(import("./CustomGraph").then(mod => mod.MyCustomGraph), {ssr: false});
return (
<SigmaContainer style={{ height: "500px", width: "500px" }} >
<MyGraph/>
</SigmaContainer>
)
}
else return (<p>NOT AVAILABLE</p>)
}
export default GraphLayoutThree
Here's my attempt with react-force-graph (note : I did slightly modify edges to have from-to instead of source-target when working with this)
import dynamic from "next/dynamic";
const GraphLayoutTwo = () => {
const isBrowser = () => typeof window !== "undefined"
if(isBrowser) {
const MyGraph = dynamic(import("./CustomGraphTwo").then(mod => mod.default), {ssr: false});
return (
<MyGraph/>
)
}
else return (<p>NOT AVAILABLE</p>)
}
export default GraphLayoutTwo
import dynamic from "next/dynamic";
const GraphLayoutTwo = () => {
const isBrowser = () => typeof window !== "undefined"
if(isBrowser) {
const MyGraph = dynamic(import("./CustomGraphTwo").then(mod => mod.default), {ssr: false});
return (
<MyGraph/>
)
}
else return (<p>NOT AVAILABLE</p>)
}
export default GraphLayoutTwo
To implement something similar we use react-graph-vis inside a nextjs application.
If you have the window is not defined error, just wrap the component and import it with dynamic
// components/graph.tsx
export const Graph = ({data, options, events, ...props}) => {
return (
<GraphVis
graph={transformData(data)}
options={options}
events={events}
/>
)
}
then in your page
// pages/index.ts
const Graph = dynamic(() => (import("../components/graph").then(cmp => cmp.Graph)), { ssr: false })
const Index = () => {
return (
<>
<Graph data={...} .... />
</>
)
}
export default Index;

error:Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined

Thanks for everyone stop here whether you inspire me or not.In App.js,there is error message:Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.
This is very classic todolist project in reduxjs official document,but I reshape it with react and react-redux hooks.
Tree view down:
Index.js:
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { createStore } from "redux";
import rootReducer from "./reducers";
import { Provider } from "react-redux";
const store = createStore(
rootReducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
App.js:
import React from "react";
import Addtodo from "./components/Addtodo";
import Footer from "./components/Footer";
import Todolist from "./components/Todolist";
const App = () => {
return (
<>
<Addtodo />
<Todolist />
<Footer />
</>
);
};
export default App;
actions.js
let nextTodoId = 0;
const addtodo = (text) => {
return {
type: "ADD_TODO",
id: nextTodoId++,
text: text
};
};
const toggletodo = (id) => {
return { type: "TOGGLE_TODO", id: id };
};
const setfilter = (filter) => {
return {
type: "SET_VISIBILITY_FILTER",
filter: filter
};
};
const todoActions = { addtodo, toggletodo };
const filterActions = { setfilter };
const allActions = {
todoActions,
filterActions
};
export default allActions;
reducers.js
import { combineReducers } from "redux";
const todo = (state = {}, action) => {
switch (action.type) {
case "ADD_TODO":
return { id: action.id, text: action.text, completed: false };
case "TOGGLE_TODO":
if (state.id === action.id)
return { ...state, completed: !state.completed };
else return state;
default:
return state;
}
};
const todos = (state = [], action) => {
switch (action.type) {
case "ADD_TODO":
return [...state, todo(undefined, action)];
case "TOGGLE_TODO":
return state.map((s) => todo(s, action));
default:
return state;
}
};
const visibilityFilter = (state = "ALL", action) => {
switch (action.type) {
case "SET_VISIBILITY_FILTER":
if (state !== action.filter) return action.filter;
default:
return state;
}
};
const rootReducer = combineReducers({ todos, visibilityFilter });
export default rootReducer;
Addtodo.js
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import allActions from "../actions";
const Addtodo=() => {
const dispatch = useDispatch();
return(
<>
<input
ref={(node) => {
this.input = node;
}}
/>
<button
onClick={() => {
dispatch(allActions.todoActions.addtodo(this.input.value));
this.input.value = "";
}}
>
Add To Do
</button>
</>
);
};
export default Addtodo;
Todolist.js
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import allActions from "../actions";
const getvisibletodos = (todos = [], currentFilter) => {
switch (currentFilter) {
case "ALL":
return todos;
case "ACTIVE":
return todos.filter((todo) => todo.completed === false);
case "COMPLETED":
return todos.filter((todo) => todo.completed === true);
}
};
const Todolist = () => {
const todos = useSelector((state) =>
getvisibletodos(state.todos, state.visibilityFilter)
);
const dispatch = useDispatch();
return (
<ul>
{todos.map((todo) => (
<li
key={todo.id}
style={{ textDecoration: todo.completed ? "line-through" : "none" }}
onClick={() => dispatch(allActions.todoActions.toggletodo(todo.id))}
>
{todo.text}
</li>
))}
</ul>
);
};
export default Todolist;
Footer.js
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import allActions from "../actions";
const Filterlink = ({
children,
filter
}) => {
const active = useSelector(state => state.visibilityFilter ===filter);
const dispatch=useDispatch();
if (active) {
return <span>{children}</span>;
}
return (
<a href='#'
onClick={e => {
e.preventDefault();
dispatch(allActions.filterActions.setfilter(filter));
}}
>
{children}
</a>
);
};
export const Footer = () => (
<p>
Show:
{' '}
<FilterLink filter='ALL'>
All
</FilterLink>
{', '}
<FilterLink filter='ACTIVE'>
Active
</FilterLink>
{', '}
<FilterLink filter='COMPLETED'>
Completed
</FilterLink>
</p>
);
The codesandebox link available:https://codesandbox.io/s/lesson-27-28-29-transition-to-react-redux-hooks-stackoverflow-0fsh6
Wish everyone get new taste in coding everyday.

Redux property increment not working, if call through child component

I have flatlist having images, videos. for images, I defined duration to show and then move to the next item in flatlist, but in case of the video once a video ended then move to next item.
I am using Redux for currentlyPlayingIndex and flatlist datasource.
If I have only images in flatlist it is working fine, but if I have a video, on video end I need to pass event from child to its parent. The parent calls the same method to move to the next index as for image duration end but the increment of currentlyPlayingIndex by one is not happening in case of video end.
parent component code or flatlist handler
import React, { Component } from 'react'
import { TouchableWithoutFeedback, View,StatusBar,Dimensions,FlatList } from 'react-native'
import {FileType,getFileType,getFileExtension} from '../services/FileManagerService'
import VideoPlayer from '../components/PlayerTypes/VideoPlayer'
import ImagePlayer from '../components/PlayerTypes/ImagePlayer'
import PdfPlayer from '../components/PlayerTypes/PdfPlayer'
import { ScaledSheet } from 'react-native-size-matters';
import NothingTpPlay from '../components/PlayerTypes/NothingTpPlay'
//redux
import {bindActionCreators} from 'redux';
import { connect } from 'react-redux';
import * as Actions from '../database/actions/ScheduleActions.js'; //Import your actions
import * as Animatable from 'react-native-animatable';
import constants from '../config/constants'
import KeepAwake from 'react-native-keep-awake';
import AudioPlayer from '../components/PlayerTypes/AudioPlayer'
import { showToastMessage } from '../utils/ToastMessage'
import I18n from "../locales/i18n-js";
import WebsitePlayer from '../components/PlayerTypes/WebsitePlayer'
import FullScreen from "../NativeBridgingHeader/FullScreen";
let deviceWidth = Dimensions.get('window').width
let deviceHeight = Dimensions.get('window').height
class PlaylistPlayerScreen extends Component {
constructor() {
super();
this.state = {
currentVisibleIndex:-1
}
this.playNextFile = this.playNextFile.bind(this)
this.videoEnded = this.videoEnded.bind(this)
this.schedulePlayDurationTimer = this.schedulePlayDurationTimer.bind(this)
this.viewabilityConfig = {
waitForInteraction: false,
itemVisiblePercentThreshold: 99,
}
}
static navigationOptions = {
header: null,
};
onViewableItemsChanged = ({ viewableItems }) => {
// viewableItems will show you what items are in view
// console.log("onViewableItemsChanged called" + JSON.stringify(viewableItems))
if(viewableItems.length >= 1) {
const visibleFileIndex = viewableItems[0].index
// console.log("visible index " + visibleFileIndex)
this.setState({currentVisibleIndex:visibleFileIndex})
const file = this.props.schedulesFiles[visibleFileIndex]
const fileType = getFileType(file)
console.log("file type is " + fileType)
if (fileType == FileType.Video) {
console.log("video file type")
} else {
this.schedulePlayDurationTimer(visibleFileIndex)
}
}
}
getItemLayout = (data, index) => ({
length: deviceWidth,
offset: deviceWidth * index,
index,
})
componentDidMount(){
this.props.getScheduleFiles()
}
shouldComponentUpdate(nextProps, nextState) {
return true
}
componentDidUpdate(){
console.log("componentDidUpdate")
}
schedulePlayDurationTimer(file_index) {
const file = this.props.schedulesFiles[file_index]
const playDuration = file.play_duration_in_milliseconds
this.timer = setTimeout(() => {
clearTimeout(this.timer)
this.playNextFile()
}, playDuration);
}
videoEnded = () => {
console.log("video ended")
this.playNextFile()
}
playNextFile = () => {
if(this.props.currentlyPlayingIndex == (this.props.schedulesFiles.length - 1)) {
//last file played
this.props.getScheduleFiles()
this.props.playNextFile(this.props.schedulesFiles,this.props.currentlyPlayingIndex)
this.listRef.scrollToIndex({animated: false, index: this.props.currentlyPlayingIndex})
} else {
console.log("playNextFile current index " + this.props.currentlyPlayingIndex)
this.props.playNextFile(this.props.schedulesFiles,this.props.currentlyPlayingIndex)
console.log("playNextFile next index " + this.props.currentlyPlayingIndex)
this.listRef.scrollToIndex({animated: true, index: this.props.currentlyPlayingIndex})
}
}
_renderItem = ({item, index}) => {
return (
this.renderPlayer(item,index)
);
}
renderPlayer(file,index) {
switch (getFileType(file)) {
case FileType.Video:
return <VideoPlayer file={file} onEnd={this.videoEnded} currentIndex={index} currentVisibleIndex={this.state.currentVisibleIndex} />
case FileType.Audio:
return <AudioPlayer file={file} onEnd={this.playNextFile} />
case FileType.Image:
return <ImagePlayer file={file} onEnd={this.playNextFile} />
case FileType.Pdf:
return <PdfPlayer file={file} onEnd={this.playNextFile} />
case FileType.WebpageContent:
return <WebsitePlayer file={file} onEnd={this.playNextFile} />
default:
showToastMessage(
I18n.t('ErrorMessage.FormatNotSupported', {
name: getFileExtension(file).toUpperCase()
})
)
this.playNextFile()
}
}
render() {
if(this.props.schedulesFiles.length > 0 ) {
return (
<View style={{flex:1}}>
<StatusBar hidden={true} />
<FlatList
style={{flex:1}}
bounces={false}
removeClippedSubviews={true}
scrollEnabled={false}
showsHorizontalScrollIndicator={false}
ref={el => this.listRef = el}
horizontal={true}
keyExtractor={(item, index) => index.toString()}
data={this.props.schedulesFiles}
renderItem={this._renderItem}
onViewableItemsChanged={this.onViewableItemsChanged}
viewabilityConfig={this.viewabilityConfig}
getItemLayout={this.getItemLayout}
initialNumToRender={2}
maxToRenderPerBatch={2}
windowSize={this.props.schedulesFiles.length}
/>
<KeepAwake />
</View>
)
}else {
return (
<TouchableWithoutFeedback delayLongPress={constants.REVEAL_SIDE_BAR_MENU_PRESS_DURATION} onLongPress={() => this.props.navigation.openDrawer()}>
<View style={styles.container}>
<NothingTpPlay/>
<KeepAwake />
</View>
</TouchableWithoutFeedback>
)
}
}
}
const styles = ScaledSheet.create({
container: {
flex:1,
backgroundColor : 'white',
}
});
//redux binding
// The function takes data from the app current state,
// and insert/links it into the props of our component.
// This function makes Redux know that this component needs to be passed a piece of the state
function mapStateToProps(state, props) {
return {
loading: state.scheduleReducer.loading,
schedulesFiles: state.scheduleReducer.data,
currentlyPlayingIndex: state.scheduleReducer.nextFileIndex,
}
}
// Doing this merges our actions into the component’s props,
// while wrapping them in dispatch() so that they immediately dispatch an Action.
// Just by doing this, we will have access to the actions defined in out actions file (action/home.js)
function mapDispatchToProps(dispatch) {
return bindActionCreators(Actions, dispatch);
}
//Connect everything
export default connect(mapStateToProps, mapDispatchToProps)(PlaylistPlayerScreen);
Render method for video player child component code is:
<VideoPlayer file={file} onEnd={this.videoEnded} currentIndex={index} currentVisibleIndex={this.state.currentVisibleIndex} />
VideoPlayer.js relevant code
export default class VideoPlayer extends React.Component {
constructor() {
super();
this.state = {
}
this.videoEnded = this.videoEnded.bind(this)
}
videoEnded() {
if (this.props.shouldRepeat == true) {
} else {
this.video.paused = true
this.video.seek(0)
}
this.props.onEnd()
}
render() {
return (
<Video
ref={ref => {
this.video = ref;
}}
onError={this.videoEnded}
minLoadRetryCount={1}
useTextureView={true}
controls={false}
style={ContainerStyle.playerTypeStyle}
onEnd={this.videoEnded}
repeat={this.props.shouldRepeat}
playInBackground={false}
playWhenInactive={false}
ignoreSilentSwitch={"ignore"}
resizeMode={this.props.file.resize_mode}
source={{uri:getFileAbsolutePath(this.props.file)}}
paused={this.props.currentIndex != this.props.currentVisibleIndex}
/>
)
}
}
Reducer code
import { combineReducers } from 'redux';
import { SCHEDULE_REFRESHED,PLAY_NEXT_FILE } from "../actions/ScheduleActions.js" //Import the actions types constant we defined in our actions
let dataState = { data: [], loading:true };
const scheduleReducer = (state = dataState, action) => {
switch (action.type) {
case SCHEDULE_REFRESHED:
state = Object.assign({}, state, { data: action.data, nextFileIndex:action.nextFileIndex });
return state;
case PLAY_NEXT_FILE:
state = Object.assign({}, state, { nextFileIndex: action.nextFileIndex});
return state;
default:
return state;
}
}
// Combine all the reducers
const rootReducer = combineReducers({
scheduleReducer
// ,[ANOTHER REDUCER], [ANOTHER REDUCER] ....
})
export default rootReducer;
Action code
//gets called initially on app launch to get files to be played
export function getScheduleFiles(){
return (dispatch) => {
getOfflineNextScheduleFiles().then((files)=>{//get offline files/schedule first
plainFiles = convertToArray(files)
dispatch({type: SCHEDULE_REFRESHED, data:plainFiles,nextFileIndex:0});
}).catch((error)=>{//if offline schedules is not available to play, refresh online
triggerPlaylistsRefresh().then((files)=>{
plainFiles = convertToArray(files)
dispatch({type: SCHEDULE_REFRESHED, data:plainFiles,nextFileIndex:0});
}).catch((error)=>{
console.log("nothing to play")
dispatch({type: PLAY_NEXT_FILE, nextFileIndex:0});
showToastMessage(I18n.t("ErrorMessage.NoSchedulesAvailableForCurrentTimeError"))
})
})
}
}
//get called from PlaylistPlayerScreen after each file played
export function playNextFile(files,filePlayedIndex){
return (dispatch) => {
if(filePlayedIndex < files.length-1) {
dispatch({type: PLAY_NEXT_FILE, nextFileIndex:filePlayedIndex+1});
}else {
console.log("all files played")
dispatch({type: PLAY_NEXT_FILE, nextFileIndex:0});
}
}
}

Is there a simple way of implementing a column picker for a List?

We are going to implement a columnpicker and currently the only idea I have is to implement a ColumnPickableList that wraps a List. This would also hold a list of checkboxes that will enable the user to hide a column.
But before I go ahead do that I just wondered if I'm reinveting the wheel and if there is a simpler approach to solving this?
No simpler way. You'll have to implement your own List component for that
I'm following up on this since I'm struggling to make this work. Maybe it is because I have chosen to create a wrapper that filters the children to be displayed. So techically this approach doesn't implement its own List.
I have made a naive draft which I was hoping would work, but it fails to re-render the children even though they are changed/filtered in the parent component.
The console.log(..) in ColumnPickableList render()-function does print the correct children/props, but still the children won't update/re-render. Any clues as to why? Is this approach too naive?
So here is the current draft:
ColumnPicker.js
import React, { PropTypes } from 'react';
import Checkbox from 'material-ui/Checkbox';
export default class ColumnPicker extends React.Component {
constructor(props) {
super(props);
this.onCheck = this.onCheck.bind(this);
}
onCheck(column, isChecked) {
return this.props.onCheckboxChanged(column, isChecked);
}
renderCheckbox(column, onCheck) {
const disabled = (column.source === 'id');
return (<Checkbox key={column.source} label={column.source.toUpperCase()} onCheck={(event, checked) => onCheck(column, checked)} defaultChecked disabled={disabled} />);
}
render() {
const columns = this.props.columns || [];
return (
<div className="column-picker">
{columns.map((column) => {
return this.renderCheckbox(column, this.onCheck);
})}
</div>
);
}
}
ColumnPicker.propTypes = {
columns: PropTypes.array,
onCheckboxChanged: PropTypes.func,
};
ColumnPicker.defaultProps = {
columns: [], // [{source: myField, checked: true} ...]
};
ColumnPickableList.js:
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import { List, Datagrid } from 'admin-on-rest';
import ColumnPicker from './ColumnPicker';
import { toggleColumnPickerStatusAction, initializeColumnPickerAction } from './actions';
export class ColumnPickableList extends React.Component {
componentWillMount() {
let columnSourceNames = [];
if (this.props.children) {
columnSourceNames = React.Children.map(this.props.children, (child) => {
return ({ source: child.props.source, checked: true });
});
}
const columnsDisplayed = columnSourceNames.filter((column) => column.source);
this.props.initializeColumnPicker(this.props.resource, columnsDisplayed);
}
shouldComponentUpdate(nextProps) {
const diff = nextProps.columnsDisplayed.filter((currentColumn) => {
return !this.props.columnsDisplayed.some((prevColumn) => {
return currentColumn.source === prevColumn.source && currentColumn.checked === prevColumn.checked;
});
});
return diff.length > 0;
}
removeHiddenColumns(children) {
return React.Children.map(children, (child) => {
if (!child.props.source) {
return child;
}
const column = this.props.columnsDisplayed.find((columnDisplayed) => {
return columnDisplayed.source === child.props.source;
});
if (this.props.columnsDisplayed.length === 0 || (column && column.checked)) {
return React.cloneElement(child);
}
return null;
});
}
render() {
const { children, ...rest } = this.props;
const displayedChildren = this.removeHiddenColumns(children);
console.log('Does it render? Rendering children', displayedChildren.map((child) => child.props.source));
return (
<div className="columnpickable-list">
<ColumnPicker columns={this.props.columnsDisplayed} onCheckboxChanged={this.props.handleCheckboxChanged} />
<List {...rest}>
<Datagrid>
{displayedChildren}
</Datagrid>
</List>
</div>
);
}
}
ColumnPickableList.propTypes = {
resource: PropTypes.string,
columnsDisplayed: PropTypes.array,
children: PropTypes.node,
initializeColumnPicker: PropTypes.func,
handleCheckboxChanged: PropTypes.func,
};
ColumnPickableList.defaultProps = {
columnsDisplayed: [],
};
function mapStateToProps(state) {
return {
columnsDisplayed: state.columnsDisplayed || [],
};
}
actions.js:
export const actions = {
INIT_COLUMNPICKER: 'INIT_COLUMNPICKER',
TOGGLE_COLUMNPICKER_STATUS: 'UPDATE_COLUMNPICKER_STATUS',
UPDATE_COLUMNPICKER_STATUSES: 'UPDATE_COLUMNPICKER_STATUSES',
}
export function initializeColumnPickerAction(resource, columns) {
return {
type: actions.INIT_COLUMNPICKER,
columns,
meta: { resource },
};
}
export function toggleColumnPickerStatusAction(column) {
return {
type: actions.TOGGLE_COLUMNPICKER_STATUS,
column,
};
}
reducers.js:
import { actions } from './actions';
function columnPickerReducer(state = [], action) {
switch (action.type) {
case actions.INIT_COLUMNPICKER: {
console.log('Init columnopicker reducer');
return action.columns;
}
case actions.TOGGLE_COLUMNPICKER_STATUS: {
const columns = state.map((column) => {
if (column.source === action.column.source) {
return { ...column, checked: !column.checked };
}
return column;
});
return columns;
}
default:
return state;
}
}
export default columnPickerReducer;
Example snippet of parent component:
...
<ColumnPickableList title="SillyStuff" {...props}>
<TextField source="id" />
<TextField source="NAME" />
<TextField source="SILLY_NAME" />
<TextField source="CHANGED_BY" />
<DateField source="CHANGED_TS" showTime />
<EditButton />
<DeleteButton />
</ColumnPickableList>
...

Resources