How to enable bulk actions with Simple List - react-admin

I need to create a different view for a list so that it can be viewed on mobile devices. It was suggested I use SimpleList, but I still want the user to be able to select multiple items in the list and complete bulk actions. Is there a way to do this? There isn't much documentation on this scenario in the React Admin docs.

I did not have enough reputation to vote this up, however, using the current SimpleList, I have made a selectable (bulk actions) version that works for my purposes.
It may be useful for you as well.
It's in regular JS, not TypeScript like the original.
To use it, simply import the file such as:
import SelectSimpleList from './SelectSimpleList'
and use it exactly the same way as the original SimpleList.
SelectSimpleList.js:
import * as React from 'react';
import PropTypes from 'prop-types';
import {
Avatar,
List,
//ListProps,
ListItem,
ListItemAvatar,
ListItemIcon,
ListItemSecondaryAction,
ListItemText,
} from '#material-ui/core';
import { makeStyles } from '#material-ui/core/styles';
import { Link } from 'react-router-dom';
import {
linkToRecord,
sanitizeListRestProps,
useListContext,
//Record,
//RecordMap,
//Identifier,
} from 'ra-core';
import Checkbox from '#material-ui/core/Checkbox';
import { useTimeout } from 'ra-core';
import classnames from 'classnames';
const useStylesPlaceholder = makeStyles(
theme => ({
root: {
backgroundColor: theme.palette.grey[300],
display: 'flex',
},
}),
{ name: 'RaPlaceholder' }
);
const Placeholder = props => {
const classes = useStylesPlaceholder(props);
return (
<span className={classnames(classes.root, props.className)}>
</span>
);
};
const useStylesLoading = makeStyles(
theme => ({
primary: {
width: '30vw',
display: 'inline-block',
marginBottom: theme.spacing(),
},
tertiary: { float: 'right', opacity: 0.541176, minWidth: '10vw' },
}),
{ name: 'RaSimpleListLoading' },
);
const times = (nbChildren, fn) =>
Array.from({ length: nbChildren }, (_, key) => fn(key));
const SimpleListLoading = props => {
const {
classes: classesOverride,
className,
hasLeftAvatarOrIcon,
hasRightAvatarOrIcon,
hasSecondaryText,
hasTertiaryText,
nbFakeLines = 5,
...rest
} = props;
const classes = useStylesLoading(props);
const oneSecondHasPassed = useTimeout(1000);
return oneSecondHasPassed ? (
<List className={className} {...rest}>
{times(nbFakeLines, key => (
<ListItem key={key}>
{hasLeftAvatarOrIcon && (
<ListItemAvatar>
<Avatar> </Avatar>
</ListItemAvatar>
)}
<ListItemText
primary={
<div>
<Placeholder className={classes.primary} />
{hasTertiaryText && (
<span className={classes.tertiary}>
<Placeholder />
</span>
)}
</div>
}
secondary={
hasSecondaryText ? <Placeholder /> : undefined
}
/>
{hasRightAvatarOrIcon && (
<ListItemSecondaryAction>
<Avatar> </Avatar>
</ListItemSecondaryAction>
)}
</ListItem>
))}
</List>
) : null;
};
SimpleListLoading.propTypes = {
className: PropTypes.string,
hasLeftAvatarOrIcon: PropTypes.bool,
hasRightAvatarOrIcon: PropTypes.bool,
hasSecondaryText: PropTypes.bool,
hasTertiaryText: PropTypes.bool,
nbFakeLines: PropTypes.number,
};
const useStyles = makeStyles(
{
tertiary: { float: 'right', opacity: 0.541176 },
},
{ name: 'RaSimpleList' }
);
/**
* The <SimpleList> component renders a list of records as a material-ui <List>.
* It is usually used as a child of react-admin's <List> and <ReferenceManyField> components.
*
* Also widely used on Mobile.
*
* Props:
* - primaryText: function returning a React element (or some text) based on the record
* - secondaryText: same
* - tertiaryText: same
* - leftAvatar: function returning a React element based on the record
* - leftIcon: same
* - rightAvatar: same
* - rightIcon: same
* - linkType: 'edit' or 'show', or a function returning 'edit' or 'show' based on the record
* - rowStyle: function returning a style object based on (record, index)
*
* #example // Display all posts as a List
* const postRowStyle = (record, index) => ({
* backgroundColor: record.views >= 500 ? '#efe' : 'white',
* });
* export const PostList = (props) => (
* <List {...props}>
* <SimpleList
* primaryText={record => record.title}
* secondaryText={record => `${record.views} views`}
* tertiaryText={record =>
* new Date(record.published_at).toLocaleDateString()
* }
* rowStyle={postRowStyle}
* />
* </List>
* );
*/
const SelectSimpleList = props => {
const {
className,
classes: classesOverride,
hasBulkActions,
leftAvatar,
leftIcon,
linkType = 'edit',
primaryText,
rightAvatar,
rightIcon,
secondaryText,
tertiaryText,
rowStyle,
...rest
} = props;
const { basePath, data, ids, loaded, total, onToggleItem, selectedIds } = useListContext(props);
const classes = useStyles(props);
if (loaded === false) {
return (
<SimpleListLoading
classes={classes}
className={className}
hasLeftAvatarOrIcon={!!leftIcon || !!leftAvatar}
hasRightAvatarOrIcon={!!rightIcon || !!rightAvatar}
hasSecondaryText={!!secondaryText}
hasTertiaryText={!!tertiaryText}
/>
);
}
const isSelected = id => {
if (selectedIds.includes(id)){
return true;
}
return false;
}
return (
total > 0 && (
<List className={className} {...sanitizeListRestProps(rest)}>
{ids.map((id, rowIndex) => (
<LinkOrNot
linkType={linkType}
basePath={basePath}
id={id}
key={id}
record={data[id]}
>
<ListItem
//onClick={() => {onToggleItem(id)}}
button={!!linkType}
style={
rowStyle
? rowStyle(data[id], rowIndex)
: undefined
}
>
<Checkbox
checked={isSelected(id)}
onChange={() => onToggleItem(id)}
color="primary"
onClick={(e) => e.stopPropagation()}
inputProps={{ 'aria-label': 'primary checkbox' }}
/>
{leftIcon && (
<ListItemIcon>
{leftIcon(data[id], id)}
</ListItemIcon>
)}
{leftAvatar && (
<ListItemAvatar>
<Avatar>{leftAvatar(data[id], id)}</Avatar>
</ListItemAvatar>
)}
<ListItemText
primary={
<div>
{primaryText(data[id], id)}
{tertiaryText && (
<span className={classes.tertiary}>
{tertiaryText(data[id], id)}
</span>
)}
</div>
}
secondary={
secondaryText && secondaryText(data[id], id)
}
/>
{(rightAvatar || rightIcon) && (
<ListItemSecondaryAction>
{rightAvatar && (
<Avatar>
{rightAvatar(data[id], id)}
</Avatar>
)}
{rightIcon && (
<ListItemIcon>
{rightIcon(data[id], id)}
</ListItemIcon>
)}
</ListItemSecondaryAction>
)}
</ListItem>
</LinkOrNot>
))}
</List>
)
);
};
SelectSimpleList.propTypes = {
className: PropTypes.string,
classes: PropTypes.object,
leftAvatar: PropTypes.func,
leftIcon: PropTypes.func,
linkType: PropTypes.oneOfType([
PropTypes.string,
PropTypes.bool,
PropTypes.func,
]),
primaryText: PropTypes.func,
rightAvatar: PropTypes.func,
rightIcon: PropTypes.func,
secondaryText: PropTypes.func,
tertiaryText: PropTypes.func,
rowStyle: PropTypes.func,
};
const useLinkOrNotStyles = makeStyles(
{
link: {
textDecoration: 'none',
color: 'inherit',
},
},
{ name: 'RaLinkOrNot' }
);
const LinkOrNot = ({
classes: classesOverride,
linkType,
basePath,
id,
children,
record,
}) => {
const classes = useLinkOrNotStyles({ classes: classesOverride });
const link =
typeof linkType === 'function' ? linkType(record, id) : linkType;
return link === 'edit' || link === true ? (
<Link to={linkToRecord(basePath, id)} className={classes.link}>
{children}
</Link>
) : link === 'show' ? (
<Link
to={`${linkToRecord(basePath, id)}/show`}
className={classes.link}
>
{children}
</Link>
) : (
<span>{children}</span>
);
};
export default SelectSimpleList;

Answer for React-Admin v4.
As I have decided to update all of my makeStyle code using tss-react/mui, you will need to install it prior to using this version of SelectSimpleList. (npm i tss-react/mui)
Using this updated version, no changes to your code 'should' be required in order for it to function. bulkActionButtons have also been added and should function.
import * as React from 'react';
import PropTypes from 'prop-types';
import { isValidElement } from 'react';
import {
Avatar,
List,
ListItem,
ListItemAvatar,
ListItemIcon,
ListItemSecondaryAction,
ListItemText
} from '#mui/material';
import { makeStyles } from 'tss-react/mui';
import { Link } from 'react-router-dom';
import {
useCreatePath,
sanitizeListRestProps,
useListContext,
useResourceContext,
RecordContextProvider,
} from 'ra-core';
import Checkbox from '#mui/material/Checkbox';
import { useTimeout } from 'ra-core';
import classnames from 'classnames';
import { BulkActionsToolbar } from 'react-admin';
import { BulkDeleteButton } from 'react-admin';
const defaultBulkActionButtons = <BulkDeleteButton />;
const useStylesPlaceholder = makeStyles()((theme) =>{
return {
root: {
backgroundColor: theme.palette.grey[300],
display: 'flex',
}
}
});
const Placeholder = props => {
const { classes } = useStylesPlaceholder(props);
return (
<span className={classnames(classes.root, props.className)}>
</span>
);
};
const useStylesLoading = makeStyles()((theme) => {
return {
primary: {
width: '30vw',
display: 'inline-block',
marginBottom: theme.spacing(),
},
tertiary: { float: 'right', opacity: 0.541176, minWidth: '10vw' }
}
})
const times = (nbChildren, fn) =>
Array.from({ length: nbChildren }, (_, key) => fn(key));
const SimpleListLoading = props => {
const {
classes: classesOverride,
className,
hasLeftAvatarOrIcon,
hasRightAvatarOrIcon,
hasSecondaryText,
hasTertiaryText,
nbFakeLines = 5,
...rest
} = props;
const { classes } = useStylesLoading(props);
const oneSecondHasPassed = useTimeout(1000);
return oneSecondHasPassed ? (
<List className={className} {...rest}>
{times(nbFakeLines, key => (
<ListItem key={key}>
{hasLeftAvatarOrIcon && (
<ListItemAvatar>
<Avatar> </Avatar>
</ListItemAvatar>
)}
<ListItemText
primary={
<div>
<Placeholder className={classes.primary} />
{hasTertiaryText && (
<span className={classes.tertiary}>
<Placeholder />
</span>
)}
</div>
}
secondary={
hasSecondaryText ? <Placeholder /> : undefined
}
/>
{hasRightAvatarOrIcon && (
<ListItemSecondaryAction>
<Avatar> </Avatar>
</ListItemSecondaryAction>
)}
</ListItem>
))}
</List>
) : null;
};
SimpleListLoading.propTypes = {
className: PropTypes.string,
hasLeftAvatarOrIcon: PropTypes.bool,
hasRightAvatarOrIcon: PropTypes.bool,
hasSecondaryText: PropTypes.bool,
hasTertiaryText: PropTypes.bool,
nbFakeLines: PropTypes.number,
};
const useStyles = makeStyles()((theme) => {
return {
tertiary: { float: 'right', opacity: 0.541176 },
}
})
/**
* The <SimpleList> component renders a list of records as a material-ui <List>.
* It is usually used as a child of react-admin's <List> and <ReferenceManyField> components.
*
* Also widely used on Mobile.
*
* Props:
* - primaryText: function returning a React element (or some text) based on the record
* - secondaryText: same
* - tertiaryText: same
* - leftAvatar: function returning a React element based on the record
* - leftIcon: same
* - rightAvatar: same
* - rightIcon: same
* - linkType: 'edit' or 'show', or a function returning 'edit' or 'show' based on the record
* - rowStyle: function returning a style object based on (record, index)
*
* #example // Display all posts as a List
* const postRowStyle = (record, index) => ({
* backgroundColor: record.views >= 500 ? '#efe' : 'white',
* });
* export const PostList = (props) => (
* <List {...props}>
* <SimpleList
* primaryText={record => record.title}
* secondaryText={record => `${record.views} views`}
* tertiaryText={record =>
* new Date(record.published_at).toLocaleDateString()
* }
* rowStyle={postRowStyle}
* />
* </List>
* );
*/
const SelectSimpleList = props => {
const {
className,
classes: classesOverride,
bulkActionButtons = defaultBulkActionButtons,
leftAvatar,
leftIcon,
linkType = 'edit',
primaryText,
rightAvatar,
rightIcon,
secondaryText,
tertiaryText,
rowStyle,
isRowSelectable,
...rest
} = props;
const hasBulkActions = !!bulkActionButtons !== false;
const resource = useResourceContext(props);
const { data, isLoading, total, onToggleItem, selectedIds } = useListContext(props);
const { classes } = useStyles(props);
if (isLoading === true) {
return (
<SimpleListLoading
classes={classes}
className={className}
hasBulkActions={hasBulkActions}
hasLeftAvatarOrIcon={!!leftIcon || !!leftAvatar}
hasRightAvatarOrIcon={!!rightIcon || !!rightAvatar}
hasSecondaryText={!!secondaryText}
hasTertiaryText={!!tertiaryText}
/>
);
}
const isSelected = id => {
if (selectedIds.includes(id)){
return true;
}
return false;
}
return (
total > 0 && (
<>
{bulkActionButtons !== false ? (
<BulkActionsToolbar selectedIds={selectedIds}>
{isValidElement(bulkActionButtons)
? bulkActionButtons
: defaultBulkActionButtons}
</BulkActionsToolbar>
) : null}
<List className={className} {...sanitizeListRestProps(rest)}>
{data.map((record, rowIndex) => (
<RecordContextProvider key={record.id} value={record}>
<LinkOrNot
linkType={linkType}
resource={resource}
id={record.id}
key={record.id}
record={record}
style={
rowStyle
? rowStyle(record, rowIndex)
: undefined
}
>
{
!!isRowSelectable ? (
<>
{
!!isRowSelectable(record) ? (
<Checkbox
checked={isSelected(record.id)}
onChange={() => onToggleItem(record.id)}
color="primary"
onClick={(e) => e.stopPropagation()}
inputProps={{ 'aria-label': 'selected checkbox' }}
/>
) : (
<div style={{width: '46px'}} />
)
}
</>
) : (
<Checkbox
checked={isSelected(record.id)}
onChange={() => onToggleItem(record.id)}
color="primary"
onClick={(e) => e.stopPropagation()}
inputProps={{ 'aria-label': 'selected checkbox' }}
/>
)
}
{leftIcon && (
<ListItemIcon>
{leftIcon(record, record.id)}
</ListItemIcon>
)}
{leftAvatar && (
<ListItemAvatar>
<Avatar>{leftAvatar(record, record.id)}</Avatar>
</ListItemAvatar>
)}
<ListItemText
primary={
<div>
{primaryText(record, record.id)}
{tertiaryText && (
<span className={classes.tertiary}>
{tertiaryText(record, record.id)}
</span>
)}
</div>
}
secondary={
secondaryText && secondaryText(record, record.id)
}
/>
{(rightAvatar || rightIcon) && (
<ListItemSecondaryAction>
{rightAvatar && (
<Avatar>
{rightAvatar(record, record.id)}
</Avatar>
)}
{rightIcon && (
<ListItemIcon>
{rightIcon(record, record.id)}
</ListItemIcon>
)}
</ListItemSecondaryAction>
)}
</LinkOrNot>
</RecordContextProvider>
))}
</List>
</>
)
);
};
SelectSimpleList.propTypes = {
className: PropTypes.string,
classes: PropTypes.object,
leftAvatar: PropTypes.func,
leftIcon: PropTypes.func,
linkType: PropTypes.oneOfType([
PropTypes.string,
PropTypes.bool,
PropTypes.func,
]),
primaryText: PropTypes.func,
rightAvatar: PropTypes.func,
rightIcon: PropTypes.func,
secondaryText: PropTypes.func,
tertiaryText: PropTypes.func,
rowStyle: PropTypes.func,
};
const useLinkOrNotStyles = makeStyles()((theme) => {
return {
link: {
textDecoration: 'none',
color: 'inherit',
}
}
})
const LinkOrNot = ({
classes: classesOverride,
linkType,
resource,
id,
children,
record,
...rest
}) => {
const { classes } = useLinkOrNotStyles({ classes: classesOverride });
const createPath = useCreatePath();
const type =
typeof linkType === 'function' ? linkType(record, id) : linkType;
return type === false ? (
<ListItem
// #ts-ignore
component="div"
className={classes.link}
{...rest}
>
{children}
</ListItem>
) : (
// #ts-ignore
<ListItem
component={Link}
button={true}
to={createPath({ resource, id, type })}
className={classes.link}
{...rest}
>
{children}
</ListItem>
);
};
export default SelectSimpleList;

Related

How can I access the values in formik before submitting in react native?

Here, is the RegisterScreen, that I want to change the formik values before submitting.
function RegisterScreen({ navigation }) {
const [hidePassword, setHidePassword] = useState(true);
const [showDatePicker, setShowDatePicker] = useState(false);
return (
<ScrollView>
<View style={{ flex: 1 }}>
<AppImageBackground>
<AppForm
initialValues={{ name: '', date: '', checkBox: '', email: '', password: '', phone: '', gender: null, city: null, bloodGroup: null }}
onSubmit={(values) => {
navigation.navigate("Login Successfully")
console.log(values);
}}
validationSchema={validationschema}
>
//FormFields
//FormFields
<SubmitButton title={"Register"} />
</AppForm>
</AppImageBackground>
</View>
</ScrollView>
);
}
How can I access the values and change it before submitting. Here below is Formik component.
Note I want to change the values in RegisterScreen (above code).
function AppForm({ initialValues, onSubmit, validationSchema, children }) {
return (
<View style={styles.container}>
<Formik
initialValues={initialValues}
onSubmit={onSubmit}
validationSchema={validationSchema}
>
{() => (<>{children}</>)}
</Formik>
</View>
);
}
You can use the onSubmit handler to access / change the values -
<Formik
onSubmit={(values, actions) => {
const phone = values.phone.replace(...)
const valuesToSend = { ...values, phone }
onSubmit(valuesToSend, actions) //from props
}
}
You can use useFormik as below
Make common validation and scheme file
useCreateUserForm.tsx
import * as Yup from 'yup';
import {FormikHelpers, useFormik} from 'formik';
export interface IUserFromType {
name: string;
}
const defaultValues: IUserFromType = {
name: '',
};
const schema = Yup.object().shape({
name: Yup.string().min(4, 'Group Name must be at least 4 characters').required("Group Name is required field"),
});
export const useCreateUserForm = (
onSubmit: (
values: IUserFromType,
formikHelpers: FormikHelpers<IUserFromType>,
) => void | Promise<unknown>,
initialValues: IUserFromType = defaultValues,
) => {
return useFormik<IUserFromType>({
initialValues,
enableReinitialize: true,
validationSchema: schema,
validateOnChange: false,
validateOnBlur: true,
onSubmit,
});
};
After that you need to add main render file like below
const formik = useCreateUserForm(onSubmit);
<UserForm formik={formik} />
Now Create UserForm.tsx
import React, {useState} from 'react';
import {FormikProps, FormikProvider} from 'formik';
interface IGroupFormProps {
formik: FormikProps<IUserFromType>;
}
function UserForm(props: IGroupFormProps) {
const {formik} = props;
const {values, handleChange, handleBlur, setFieldValue, setFieldError} = formik;
return (
<FormikProvider value={formik}>
<View>
<TextInput
onChangeText={handleChange('name')}
onBlur={handleBlur('name')}
value={values.name}
/>
</View>
</FormikProvider>
);
}
export default UserForm;
Same render all your component inside FormikProvider. Hope This will resolve your issue.

Apollo cache query fields policy offsetLimitPagination() doesnt work with subscriptions

I use apollo client for react native.
When I use offsetLimitPagination() for pagination my subscriptions doesn't update cache.
Subscriptions works correctly but doesn't update flatlist data.
When i remove offsetLimitPagination function it works. I can't use together subscriptions and offsetLimitPagination function on cache.
Is there any solution for that?`
Thanks.
Cache
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
chatDetail: offsetLimitPagination(),
}
}
},
});
ChatDetailPage
import React, { useState, useCallback } from 'react'
import { StyleSheet, Text, View, FlatList } from 'react-native'
import { ActivityIndicator } from 'react-native-paper';
import { useQuery } from '#apollo/client'
import { useSelector } from 'react-redux'
import { CHAT_DETAIL } from '../../../Graphql/Queries/Message'
import { MESSAGE_SUB } from '../../../Graphql/Subscriptions/Message'
import MainFlow from './Components/Flow/MainFlow'
const ChatDetailMain = () => {
const user = useSelector(state => state.auth.user)
const currentRoom = useSelector(state => state.room.currentRoom)
const [hasNext, setHasNext] = useState(true)
const limit = 15
const { error, loading, data, refetch, fetchMore, subscribeToMore } = useQuery(CHAT_DETAIL, {
variables: { userId: user._id, roomId: currentRoom._id, limit }, fetchPolicy: "cache-and-
network",
nextFetchPolicy: "cache-first" })
// render item
const renderItem = (
({item} ) => {
return <MainFlow item={item} />
}
)
if (error) {
console.warn('CHAT_DETAIL QUERY ERROR: ', error)
console.log(error.message);
return (
<View>
<Text>
An Error Occured: {error.message}
</Text>
</View>
)
}
if (loading || data == undefined || data == null) {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<ActivityIndicator size={50} color="gray" />
</View>
)
}
// fetchMore
const fetchMoreData=()=>{
// console.log("fetchMore runnig hasnext limit, data.chatDetail.length >= limit ", hasNext, limit, data.chatDetail.length >= limit);
if(hasNext && data.chatDetail.length >= limit){
fetchMore({
variables:{
offset: data.chatDetail.length,
limit: data.chatDetail.length+limit
}
}).then((flowMoredata)=>{
if(flowMoredata.data.chatDetail.length==0 || flowMoredata.data.chatDetail.length === data.chatDetail.length){
setHasNext(false)
}
})
}
}
// subscription area
const subscribeQ = () => subscribeToMore({
document: MESSAGE_SUB,
variables: {
userId: user._id
},
updateQuery: (prev, { subscriptionData }) => {
if (!subscriptionData.data) return prev;
const { messageSub } = subscriptionData.data
let current = {}
let others = []
switch(messageSub.type){
case 'update':
prev.chatDetail.map(message => {
if (message._id != messageSub.message._id) others.push(message)
if (message._id == messageSub.message._id) current = messageSub.message
})
return { chatDetail: [ current, ...others ]}
case 'remove':
prev.chatDetail.map(message => {
if (message._id != messageSub.message._id) others.push(message)
if (message._id == messageSub.message._id) current = messageSub.message
})
return { chatDetail: [ ...others ]}
case 'create':
return { chatDetail: { ...prev, chatDetail: [ messageSub.message, ...prev.chatDetail] }}
default: return { ...prev }
}
}
})
if (subscribeToMore != undefined && subscribeToMore) {
subscribeQ()
}
return (
<View>
<FlatList
data={data.chatDetail}
renderItem={renderItem}
keyExtractor={(item, index) => String(index)}
onEndReached={fetchMoreData}
onEndReachedThreshold={0.2}
contentContainerStyle={{ paddingTop: 80 }}
inverted={true}
/>
</View>
)
}
export default ChatDetailMain
const styles = StyleSheet.create({})
It was about cache merge issue. If you want to cache data, you shoul give a key to apollo client "Cache according to the what, for each room has an id or roomName for keyArgs param it should uniqe value like that
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
chatDetail: {
keyArgs:['roomId'],
merge(incoming=[], existing=[]){
.
.
.
return offsetLimitPagination()
}}
}
}
},
});

Limit number of checkboxes selected and save value

I am building a food delivery application, and I would like to know how I can limit the number of checkboxes selected. An example is when entering the subsidiary, it displays a list of products. If I select a pizza, there is an extras section that limits the number of extras you can select, if you want to select more than two and your limit is two it should not allow you
all this with react hooks, I attach a fragment of my component
const ExtrasSelector = ({options = [{}], onPress = () => {}, limit = 0}) => {
const [showOptions, setShowOptions] = useState(true);
const [selectedAmount, setSelectedAmount] = useState(0);
const EXTRA = ' extra';
const EXTRAS = ' extras';
const updatedList = options.map(data => ({
id: data.id,
name: data.name,
price: data.price,
selected: false,
}));
const [itemsList, setItemsList] = useState(updatedList);
const toggleOptions = () => setShowOptions(!showOptions);
useEffect(() => {
}, [selectedAmount]);
// onPress for each check-box
const onPressHandler = index => {
setItemsList(state => {
state[index].selected = !state[index].selected;
onPress(state[index], getSelectedExtras(state));
// Increments or decreases the amount of selected extras
if (state[index].selected) {
setSelectedAmount(prevState => prevState + 1);
} else {
setSelectedAmount(prevState => prevState - 1);
}
return state;
});
};
const getSelectedExtras = extrasArr => {
const selectedExsArr = [];
extrasArr.map(item => {
if (item.selected) {
selectedExsArr.push(item);
}
});
return selectedExsArr;
};
return (
<View>
<View style={styles.container}>
<TouchableOpacity style={styles.row} onPress={toggleOptions}>
<Text style={styles.boldTitleSection}>
Extras {'\n'}
<Text style={titleSection}>
Selecciona hasta {limit}
{limit > 1 ? EXTRAS : EXTRA}
</Text>
</Text>
<View style={styles.contentAngle}>
<View style={styles.contentWrapperAngle}>
<Icon
style={styles.angle}
name={showOptions ? 'angle-up' : 'angle-down'}
/>
</View>
</View>
</TouchableOpacity>
{showOptions ? (
itemsList.map((item, index) => (
<View key={index}>
<CheckBox
label={item.name}
price={item.price}
selected={item.selected}
otherAction={item.otherAction}
onPress={() => {
onPressHandler(index, item);
}}
/>
<View style={styles.breakRule} />
</View>
))
) : (
<View style={styles.breakRule} />
)}
</View>
</View>
);
};
This is a simple react implementation of "checkboxes with limit" behaviour with useReducer. This way the business logic (here the limitation but can be any) is implemented outside of the component in a pure js function while the component itself is just a simple reusable checkbox group.
const { useReducer } = React; // --> for inline use
// import React, { useReducer } from 'react'; // --> for real project
const reducer = (state, action) => {
if (state.checkedIds.includes(action.id)) {
return {
...state,
checkedIds: state.checkedIds.filter(id => id !== action.id)
}
}
if (state.checkedIds.length >= 3) {
console.log('Max 3 extras allowed.')
return state;
}
return {
...state,
checkedIds: [
...state.checkedIds,
action.id
]
}
}
const CheckBoxGroup = ({ data }) => {
const initialState = { checkedIds: [] }
const [state, dispatch] = useReducer(reducer, initialState)
return (
<table border="1">
{data.map(({ id, label }) => (
<tr key={id}>
<td>
<input
onClick={() => dispatch({ id })}
checked={state.checkedIds.includes(id)}
type="checkbox"
/>
</td>
<td>
{label}
</td>
</tr>
))}
</table>
)
};
const data = [
{ id: "1", label: "Mashroom" },
{ id: "2", label: "Ham" },
{ id: "3", label: "Egg" },
{ id: "4", label: "Ananas" },
{ id: "5", label: "Parmesan" },
]
ReactDOM.render(<CheckBoxGroup data={data} />, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.9.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.9.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Apollo Graphql Pagination failed with limit

I am trying out Apollo pagination. It works correctly if I do not pass the limit argument from the client and hard code the limit argument in my hasMoreData function. If I were to add in the limit argument, all the data will be returned from my server and it will not paginate. The server side code should be correct (I tested it on GraphQL playground).
This does not work properly:
import React, { Component } from "react";
import {
View,
Text,
ActivityIndicator,
FlatList,
Button,
StyleSheet
} from "react-native";
import { graphql } from "react-apollo";
import gql from "graphql-tag";
let picturesList = [];
class HomeScreen extends Component {
loadMore = () => {
this.props.data.fetchMore({
variables: {
offset: picturesList.length
},
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) {
return prev;
}
return {
...prev,
pictures: [...prev.pictures, ...fetchMoreResult.pictures]
};
}
});
};
render() {
const { loading, pictures, variables } = this.props.data;
picturesList = pictures;
if (loading) {
return <ActivityIndicator size="large" />;
}
//TODO - hard coded the limit as 3 which is not supposed to
let hasMoreData = picturesList.length % 3 === 0;
if (picturesList.length <= variables.offset) {
hasMoreData = false;
}
return (
<View style={styles.root}>
<Button title="Show More" onPress={this.loadMore} />
<FlatList
data={picturesList}
renderItem={({ item }) => (
<View style={styles.contentContainer}>
<Text style={styles.content}>{item.title}</Text>
</View>
)}
keyExtractor={item => item.id}
ListFooterComponent={() =>
hasMoreData ? (
<ActivityIndicator size="large" color="blue" />
) : (
<View />
)
}
/>
</View>
);
}
}
const styles = StyleSheet.create({
root: {
flex: 1
},
content: {
fontSize: 35
},
contentContainer: {
padding: 30
}
});
// adding the limit variable here will cause my server to return all data
const PICTURES_QUERY = gql`
query($offset: Int, $limit: Int) {
pictures(offset: $offset, limit: $limit) {
id
title
pictureUrl
}
}
`;
export default graphql(PICTURES_QUERY)(HomeScreen);
The server-side code, just in case:
pictures: async (_, { offset, limit }) => {
let picturesDB = getConnection()
.getRepository(Picture)
.createQueryBuilder("p");
return picturesDB
.take(limit)
.skip(offset)
.getMany();
}
I have added a default parameter in my GraphQL schema:
type Query {
pictures(offset: Int, limit: Int = 3): [Picture!]!
}
Managed to pass the limit variable using Apollo HOC pattern...
export default graphql(PICTURES_QUERY, {
options: () => ({
variables: {
limit: limitAmt
}
})
})(HomeScreen);

Chai testing on React App returning unexpected result

Hi I ran the following test and the test confirmed that ChannelListItem exists:
import React from 'react';
//import expect from 'expect';
import { expect } from 'chai';
import io from 'socket.io-client';
import sinon from 'sinon';
import { Provider } from 'react-redux';
import configureStore from '../../src/common/store/configureStore';
import { shallow,mount } from 'enzyme';
import Channels from '../../src/common/components/Channels';
import ChannelListItem from '../../src/common/components/ChannelListItem';
import { fakeChannels } from '../fakeData/channelsFake';
import { fakeMessages } from '../fakeData/messagesFake';
const initialState = window.__INITIAL_STATE__;
const store = configureStore(initialState);
const socket = io('', { path: '/api/chat' });
describe('Channels', () => {
const changeActiveChannel = sinon.spy()
const dispatch = sinon.spy(store, 'dispatch')
let Component;
beforeEach(() => {
Component =
shallow(<Provider store={store}>
<Channels
socket = {socket}
onClick = {changeActiveChannel}
channels = {fakeChannels}
messages = {fakeMessages}
dispatch = {dispatch}
/>
</Provider>);
});
it('should render', () => {
expect(Component).to.be.ok;
});
it('should have a ChannelListItem', () => {
const channelListItem = Component.find('ChannelListItem')
expect(channelListItem).to.exist;
However, when I ran the following test, I got channelListItem.length equal 0
expect(channelListItem.length).to.equal(3);
Any ideas what could be wrong? I clearly have a channelListItem inside my Channel component:
return (
<ChannelListItem style={{paddingLeft: '0.8em', background: '#2E6DA4', height: '0.7em'}} channel={'aa'} key={'1'} onClick={::this.handleChangeChannel} />
);
Code for Channels:
import React, { Component, PropTypes } from 'react';
import ChannelListItem from './ChannelListItem';
import ChannelListModalItem from './ChannelListModalItem';
import { Modal, Glyphicon, Input, Button } from 'react-bootstrap';
import * as actions from '../actions/actions';
import uuid from 'node-uuid';
import { createChannel } from '../reducers/channels';
export default class Channels extends Component {
static propTypes = {
channels: PropTypes.array.isRequired,
onClick: PropTypes.func.isRequired,
messages: PropTypes.array.isRequired,
dispatch: PropTypes.func.isRequired
};
constructor(props, context) {
super(props, context);
this.state = {
addChannelModal: false,
channelName: '',
moreChannelsModal: false
};
}
handleChangeChannel(channel) {
if(this.state.moreChannelsModal) {
this.closeMoreChannelsModal();
}
this.props.onClick(channel);
}
openAddChannelModal(event) {
//event.preventDefault();
this.setState({addChannelModal: true});
}
closeAddChannelModal(event) {
event.preventDefault();
this.setState({addChannelModal: false});
}
handleModalChange(event) {
this.setState({channelName: event.target.value});
}
handleModalSubmit(event) {
const { channels, dispatch, socket } = this.props;
event.preventDefault();
if (this.state.channelName.length < 1) {
this.refs.channelName.getInputDOMNode().focus();
}
if (this.state.channelName.length > 0 && channels.filter(channel => {
return channel.name === this.state.channelName.trim();
}).length < 1) {
const newChannel = {
name: this.state.channelName.trim(),
id: `${Date.now()}${uuid.v4()}`,
private: false
};
dispatch(createChannel(newChannel));
this.handleChangeChannel(newChannel);
socket.emit('new channel', newChannel);
this.setState({channelName: ''});
this.closeAddChannelModal(event);
}
}
validateChannelName() {
const { channels } = this.props;
if (channels.filter(channel => {
return channel.name === this.state.channelName.trim();
}).length > 0) {
return 'error';
}
return 'success';
}
openMoreChannelsModal(event) {
event.preventDefault();
this.setState({moreChannelsModal: true});
}
closeMoreChannelsModal(event) {
//event.preventDefault();
this.setState({moreChannelsModal: false});
}
createChannelWithinModal() {
this.closeMoreChannelsModal();
this.openAddChannelModal();
}
render() {
const { channels, messages } = this.props;
const filteredChannels = channels.slice(0, 8);
const moreChannelsBoolean = channels.length > 8;
const restOfTheChannels = channels.slice(8);
const newChannelModal = (
<div>
<Modal key={1} show={this.state.addChannelModal} onHide={::this.closeAddChannelModal}>
<Modal.Header closeButton>
<Modal.Title>Add New Channel</Modal.Title>
</Modal.Header>
<Modal.Body>
<form onSubmit={::this.handleModalSubmit} >
<Input
ref="channelName"
type="text"
help={this.validateChannelName() === 'error' && 'A channel with that name already exists!'}
bsStyle={this.validateChannelName()}
hasFeedback
name="channelName"
autoFocus="true"
placeholder="Enter the channel name"
value={this.state.channelName}
onChange={::this.handleModalChange}
/>
</form>
</Modal.Body>
<Modal.Footer>
<Button onClick={::this.closeAddChannelModal}>Cancel</Button>
<Button disabled={this.validateChannelName() === 'error' && 'true'} onClick={::this.handleModalSubmit} type="submit">
Create Channel
</Button>
</Modal.Footer>
</Modal>
</div>
);
const moreChannelsModal = (
<div style={{background: 'grey'}}>
<Modal key={2} show={this.state.moreChannelsModal} onHide={::this.closeMoreChannelsModal}>
<Modal.Header closeButton >
<Modal.Title>More Channels</Modal.Title>
<a onClick={::this.createChannelWithinModal} style={{'cursor': 'pointer', 'color': '#85BBE9'}}>
Create a channel
</a>
</Modal.Header>
<Modal.Body>
<ul style={{height: 'auto', margin: '0', overflowY: 'auto', padding: '0'}}>
{restOfTheChannels.map(channel =>
<ChannelListModalItem channel={channel} key={channel.id} onClick={::this.handleChangeChannel} />
)}
</ul>
</Modal.Body>
<Modal.Footer>
<button onClick={::this.closeMoreChannelsModal}>Cancel</button>
</Modal.Footer>
</Modal>
</div>
);
return (
<section>
<div>
<span style={{paddingLeft: '0.8em', fontSize: '1.5em'}}>
Channels
<button onClick={::this.openAddChannelModal} style={{fontSize: '0.8em', 'background': 'Transparent', marginLeft: '2.8em', 'backgroundRepeat': 'noRepeat', 'border': 'none', 'cursor': 'pointer', 'overflow': 'hidden', 'outline': 'none'}}>
<Glyphicon glyph="plus" />
</button>
</span>
</div>
{newChannelModal}
<div>
<ul style={{display: 'flex', flexDirection: 'column', listStyle: 'none', margin: '0', overflowY: 'auto', padding: '0'}}>
{filteredChannels.map(channel =>
<ChannelListItem style={{paddingLeft: '0.8em', background: '#2E6DA4', height: '0.7em'}} messageCount={messages.filter(msg => {
return msg.channelID === channel.name;
}).length} channel={channel} key={channel.id} onClick={::this.handleChangeChannel} />
)}
</ul>
{moreChannelsBoolean && <a onClick={::this.openMoreChannelsModal} style={{'cursor': 'pointer', 'color': '#85BBE9'}}> + {channels.length - 8} more...</a>}
{moreChannelsModal}
</div>
</section>
);
}
}