node-red-contrib-chatbot
Version:
REDBot a Chat bot for a full featured chat bot for Telegram, Facebook Messenger and Slack. Almost no coding skills required
303 lines (290 loc) • 9.21 kB
JavaScript
import React, { useState, forwardRef, useImperativeHandle, useEffect } from 'react';
import { Table, Placeholder, Checkbox } from 'rsuite';
import _ from 'lodash';
import PropTypes from 'prop-types';
const { Pagination, Column, HeaderCell, Cell } = Table;
const { Grid } = Placeholder;
import useRouterQuery from '../../../src/hooks/router-query';
import TableFilters from '../table-filters';
import ShowError from '../show-error';
import useTable from './hooks/table';
import './style.scss';
const LABELS = {
empty: 'No Content'
};
const CheckCell = ({ rowData, onChange, checked, checkedKeys, dataKey, disabled = false, ...props }) => (
<Cell {...props} style={{ padding: 0 }}>
<div className="checkbox-cell">
<Checkbox
value={rowData[dataKey]}
inline
disabled={disabled}
onChange={onChange}
checked={checked || checkedKeys.some(item => item === rowData[dataKey])}
/>
</div>
</Cell>
);
CheckCell.propTypes = {
rowData: PropTypes.object,
onChange: PropTypes.func,
checked: PropTypes.bool,
checkedKeys: PropTypes.array,
dataKey: PropTypes.string,
disabled: PropTypes.bool
};
const CustomTable = forwardRef(({
children,
query,
variables = {},
initialSortField = 'createdAt',
initialSortDirection = 'desc',
labels,
filtersSchema = [],
toolbar,
disabled = false,
selectable = false,
//selection = { ids: [], all: false },
onFilters = () => {},
filterEvaluateParams = ['data'],
onData = () => {},
onSelect = () => {},
...rest
}, ref) => {
const filterKeys = (filtersSchema || []).map(item => item.name);
// get all keys that are numeric and must be parsed to int
const numericKeys = (filtersSchema || [])
.filter(filter => filter.type === 'number')
.map(filter => filter.name);
const [loaded, setLoaded] = useState(false);
const [ selection, setSelection] = useState({ all: false, ids: []});
const { query: urlQuery, setQuery } = useRouterQuery({
numericKeys,
onChangeQuery: query => {
setFilters(_.pick(query, filterKeys));
// if query changes, then reload the query, but don't do at startup
if (loaded) {
refetch();
}
}
});
const [ filters, setFilters ] = useState(_.pick(urlQuery, filterKeys));
const [ cursor, setCursor ] = useState({
page: 1,
limit: 10,
sortField: initialSortField,
sortType: initialSortDirection,
total: 0 // total records
});
const { limit, page, sortField, sortType, total } = cursor;
useEffect(() => {
// reset cursor everytime filter changes
setCursor({ ...cursor, page: 1, limit: 10 });
}, [filters, sortField, sortType ]);
const {
bootstrapping,
loading,
error,
data,
refetch
} = useTable({
query,
limit,
page,
sortField,
sortType,
filters,
variables,
onCompleted: (rows, counters) => {
onData(rows);
setCursor({ ...cursor, total: counters.rows.count });
setLoaded(true);
}
});
useImperativeHandle(ref, () => ({
refetch: async () => {
setSelection({ all: false, ids: [] });
onSelect({ all: false, ids: [] });
const { data } = await refetch();
// check cursor consistency (in case in last page and some records delete and reduced the page)
const totalPages = Math.floor(data.counters.rows.count / cursor.limit)
+ ((data.counters.rows.count % cursor.limit) !== 0 ? 1 : 0);
// update the total records
setCursor({ ...cursor, total: data.counters.rows.count });
// if deleted so many records the current page fell beyond the limit, then adjust
if (page > totalPages) {
setCursor({ ...cursor, page: totalPages });
}
},
getPrevious: id => {
let previous;
data.rows.forEach((row, idx) => {
if (row.id === id && idx > 0) {
previous = data.rows[idx - 1].id;
}
});
return previous;
},
getNext: id => {
let next;
data.rows.forEach((row, idx) => {
if (row.id === id && idx < (data.rows.length - 1)) {
next = data.rows[idx + 1].id;
}
});
return next;
}
}));
labels = { ...LABELS, ...labels };
const schema = filtersSchema.map(filter => {
const evaluated = { ...filter };
// iterate over params to be evaluated
filterEvaluateParams.forEach(param => {
if (_.isFunction(evaluated[param])) {
if (bootstrapping) {
evaluated[param] = [];
} else {
evaluated[param] = evaluated[param](data);
}
}
});
return evaluated;
});
const { all: selectedAll = false, ids: checkedKeys = [] } = selection;
return (
<div className="ui-custom-table">
{bootstrapping && <Grid columns={9} rows={3} />}
{!bootstrapping && schema != null && (
<div className="header">
<div className="filters">
<TableFilters
filters={filters}
disabled={disabled || error}
onChange={filters => {
setFilters(filters);
setQuery(filters);
onFilters(filters);
}}
schema={schema}
/>
</div>
{toolbar != null && _.isFunction(toolbar) && (
<div className="toolbar">
{toolbar({
filters,
count: !error && !bootstrapping ? data.counters.rows.count : 0,
selection: { ids: checkedKeys, all: selectedAll },
setSelection
})}
</div>
)}
{toolbar != null && !_.isFunction(toolbar) && (
<div className="toolbar">
{toolbar}
</div>
)}
</div>
)}
{!bootstrapping && error && (
<ShowError error={error} />
)}
{!bootstrapping && !error && (
<Table
data={data.rows || []}
loading={loading}
sortColumn={sortField}
sortType={sortType}
renderEmpty={() => <div style={{ textAlign: 'center', padding: 80}}>{labels.empty}</div>}
onSortColumn={(sortField, sortType) => setCursor({ ...cursor, sortField, sortType })}
{...rest}
>
{selectable && (
<Column width={50} align="center" key="selector">
<HeaderCell style={{ padding: 0 }}>
<div style={{ marginTop: '-9px' }}>
<Checkbox
inline
disabled={disabled}
checked={selectedAll}
indeterminate={checkedKeys.length !== 0}
onChange={() => {
onSelect({ ids: [], all: !selectedAll });
setSelection({ ids: [], all: !selectedAll });
}}
/>
</div>
</HeaderCell>
<CheckCell
dataKey="id"
checkedKeys={checkedKeys}
disabled={disabled}
checked={selectedAll}
onChange={(value, checked) => {
if (selectedAll) {
onSelect({ ids: [value], all: false });
} else {
const ids = checked
? [...checkedKeys, value]
: checkedKeys.filter(item => item !== value);
onSelect({ ids, all: false });
setSelection({ ids, all: false });
}
}}
/>
</Column>
)}
{children}
</Table>
)}
{!error && !bootstrapping && (
<Pagination
activePage={page}
displayLength={limit}
disabled={loading || disabled}
onChangePage={page => setCursor({ ...cursor, page })}
lengthMenu={[{ label: '10', value: 10 }, { label: '20', value: 20 }, { label: '30', value: 30 } ]}
onChangeLength={limit => setCursor({ ...cursor, limit, page: 1 })}
total={total}
/>
)}
</div>
);
});
CustomTable.displayName = 'CustomTable';
CustomTable.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]),
toolbar: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
PropTypes.func
]),
disabled: PropTypes.bool,
selectable: PropTypes.bool,
variables: PropTypes.object,
query: PropTypes.object,
// the subset of content to display
namespace: PropTypes.string,
initialSortField: PropTypes.string,
initialSortDirection: PropTypes.oneOf(['desc', 'asc']),
// string labels of the component
labels: PropTypes.shape({
empty: PropTypes.string
}),
onData: PropTypes.func,
onSelect: PropTypes.func,
filterEvaluateParams: PropTypes.arrayOf(PropTypes.string),
filtersSchema: PropTypes.arrayOf(PropTypes.oneOfType([
PropTypes.func,
PropTypes.shape({
name: PropTypes.string,
label: PropTypes.string,
control: PropTypes.any
})
])),
// callback when filters changes
onFilters: PropTypes.func
};
export default CustomTable;