react-mapfilter
Version:
These components are designed for viewing data in Mapeo. They share a common interface:
389 lines (335 loc) • 13.1 kB
JavaScript
import "core-js/modules/es.array.iterator";
import "core-js/modules/web.dom-collections.iterator";
import _findInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/find";
import _keysInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/keys";
import _Map from "@babel/runtime-corejs3/core-js-stable/map";
import _valuesInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/values";
import _includesInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/includes";
import _everyInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/every";
import _Array$isArray from "@babel/runtime-corejs3/core-js-stable/array/is-array";
import _sliceInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/slice";
import _Object$keys from "@babel/runtime-corejs3/core-js-stable/object/keys";
import _filterInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/filter";
import _forEachInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/for-each";
import _sortInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/sort";
import _JSON$stringify from "@babel/runtime-corejs3/core-js-stable/json/stringify";
import _Object$assign from "@babel/runtime-corejs3/core-js-stable/object/assign";
import _mapInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/map";
// @flow
import React, { useMemo, useEffect } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import List from '@material-ui/core/List';
import { defineMessages, useIntl } from 'react-intl';
import isEqual from 'lodash/isEqual';
import omit from 'lodash/omit';
import DateFnsUtils from '@date-io/date-fns'; // choose your lib
import { MuiPickersUtilsProvider } from '@material-ui/pickers';
import { createMemoizedStats } from '../lib/data_analysis/index';
import DiscreteFilter from './DiscreteFilter';
import DateFilter from './DateFilter';
import getStats from '../stats';
import FormattedFieldname from '../internal/FormattedFieldname';
/*:: import type {
Statistics,
Filter,
Field,
SelectOptions,
FieldStatistic
} from '../types'*/
/*:: import type { Observation, Preset } from 'mapeo-schema'*/
/*:: type Props = {
filter: Filter | null,
onChangeFilter: (filter: Filter | null) => void,
observations?: Observation[],
fields?: Field[],
presets?: Preset[]
}*/
const m = defineMessages({
// Button text to change which fields are shown and filterable in the filter pane
editFilters: {
"id": "eO0bIg==",
"defaultMessage": 'Edit Filters…'
},
// Label for filter by date observation was created
created: {
"id": "pxFasw==",
"defaultMessage": 'Date of observation'
},
// Label for filter by date observation was modified (e.g. edited by a user)
modified: {
"id": "eykPgw==",
"defaultMessage": 'Modified'
},
// Label for filter by category (e.g. the preset)
preset: {
"id": "KdbzkA==",
"defaultMessage": 'Category'
}
});
const memoizedStats = createMemoizedStats(); // Stats just for the created and modified fields. The other stat instance we
// use is for memoized stats of observation tags which does not include these
// top-level props
const getTimestampStats = (observations
/*: Observation[]*/
) =>
/*: Statistics*/
{
return memoizedStats(_mapInstanceProperty(observations).call(observations, o => ({
$created: o.created_at,
$modified: o.timestamp
})));
};
const FilterPanel = ({
filter,
onChangeFilter,
observations = [],
fields = [],
presets = []
}
/*: Props*/
) => {
var _context2, _context3;
const cx = useStyles();
const {
formatMessage: t
} = useIntl();
let filterByField
/*: { [fieldId: string]: Filter | null }*/
= {};
let filterError;
try {
filterByField = parseFilter(filter);
} catch (e) {
console.log(filter);
console.warn(e);
filterError = false;
}
const timestampStats = useMemo(() => getTimestampStats(observations), [observations]);
const stats = useMemo(() => getStats(observations), [observations]);
const handleChangeFilter = fieldId => filter => {
const newFilterByField = filter == null ? omit(filterByField, fieldId) : _Object$assign({}, filterByField, {
[fieldId]: filter
});
const newFilter = compileFilter(newFilterByField);
onChangeFilter(newFilter);
}; // Filters are shown for: date the observation was created, date modified
// (edited), the category (preset), and any fields defined in the preset which
// are select_one, date or date_time. Currently we don't show presets for
// free-form text fields (which could have too many values, or too many ways
// of spelling things to makes sense).
const filterFields = useMemo(() => {
var _context;
const $createdId = _JSON$stringify(['$created']);
const $presetId = _JSON$stringify(['$preset']);
const filterFields
/*: { [fieldId: string]: Field }*/
= {
[$createdId]: {
id: $createdId,
key: ['$created'],
label: t(m.created),
type: 'datetime',
min_value: timestampStats[$createdId] && timestampStats[$createdId].datetime.min,
max_value: timestampStats[$createdId] && timestampStats[$createdId].datetime.max
}
};
const presetOptions = _mapInstanceProperty(_context = _sortInstanceProperty(presets // .filter(
// preset =>
// stats['categoryId'] &&
// stats['categoryId'].string.values.has(preset.id)
// )
).call(presets, presetCompare)).call(_context, preset => ({
value: preset.id,
label: preset.name
}));
filterFields[$presetId] = {
id: $presetId,
key: ['$preset'],
label: t(m.preset),
type: 'select_one',
options: presetOptions
}; // Enable filtering by any select_one, date or date_time field that is
// defined in the preset, but add in options (for select_one) and min/max
// (for dates) from the actual data, since the data could include values
// outside the range defined in the preset
_forEachInstanceProperty(fields).call(fields, field => {
const fieldId = _JSON$stringify(field.key);
const fieldStats = stats[fieldId];
if (field.type === 'select_one') {
// $FlowFixMe
filterFields[fieldId] = _Object$assign({}, field, {
options: combineOptionsWithStats(field.options, fieldStats)
});
} else if (field.type === 'date' || field.type === 'datetime') {
// $FlowFixMe
filterFields[fieldId] = _Object$assign({}, field, {
min_value: fieldStats && fieldStats[field.type].min,
max_value: fieldStats && fieldStats[field.type].max
});
}
});
return filterFields;
}, [t, timestampStats, presets, fields, stats]);
useEffect(() => {
if (!filterError) return;
onChangeFilter(null);
}, [filterError, onChangeFilter]);
if (filterError) return null;
return /*#__PURE__*/React.createElement(MuiPickersUtilsProvider, {
utils: DateFnsUtils
}, /*#__PURE__*/React.createElement(List, {
className: cx.list
}, _filterInstanceProperty(_context2 = _mapInstanceProperty(_context3 = _Object$keys(filterFields)).call(_context3, id => {
const field = filterFields[id];
if (!field) return;
const fieldId = _JSON$stringify(field.key);
switch (field.type) {
case 'select_one':
return /*#__PURE__*/React.createElement(DiscreteFilter, {
key: field.id,
fieldKey: field.key,
label: /*#__PURE__*/React.createElement(FormattedFieldname, {
field: field
}),
filter: filterByField[fieldId],
options: field.options,
onChangeFilter: handleChangeFilter(fieldId)
});
case 'date':
case 'datetime':
return /*#__PURE__*/React.createElement(DateFilter, {
key: field.id,
fieldKey: field.key,
label: /*#__PURE__*/React.createElement(FormattedFieldname, {
field: field
}),
filter: filterByField[fieldId],
min: field.min_value || '2001-01-01',
max: field.max_value || new Date().toISOString(),
onChangeFilter: handleChangeFilter(fieldId)
});
}
})).call(_context2, Boolean)));
};
export default FilterPanel;
const comparisonOps = ['<=', '>='];
const membershipOps = ['in', '!in']; // Parse a filter and return filter expressions by field id
function parseFilter(filter
/*: Filter | null*/
)
/*: { [fieldId: string]: Filter | null }*/
{
var _context4;
const filterByField = {};
if (filter == null) return filterByField;
if (!isValidFilter(filter)) throw new Error('Unsupported filter expression');
_forEachInstanceProperty(_context4 = _sliceInstanceProperty(filter).call(filter, 1)).call(_context4, subFilter => {
if (!_Array$isArray(subFilter)) return;
if (subFilter[0] === 'all') {
const fieldId = _JSON$stringify(subFilter[1][1]);
filterByField[fieldId] = subFilter;
} else {
const fieldId = _JSON$stringify(subFilter[1]);
filterByField[fieldId] = subFilter;
}
});
return filterByField;
}
function compileFilter(filterByField
/*: {
[fieldId: string]: Filter
}*/
)
/*: Filter | null*/
{
var _context5;
const filter = ['all'];
_forEachInstanceProperty(_context5 = _Object$keys(filterByField)).call(_context5, fieldId => filter.push(filterByField[fieldId]));
if (filter.length === 1) return null; // $FlowFixMe
return filter;
} // Currently we only support a very specific filter structure
function isValidFilter(filter)
/*: boolean*/
{
var _context6;
if (!_Array$isArray(filter)) return false;
if (filter[0] !== 'all') return false;
return _everyInstanceProperty(_context6 = _sliceInstanceProperty(filter).call(filter, 1)).call(_context6, subFilter => {
if (!_Array$isArray(subFilter)) return false;
if (subFilter[0] === 'all') {
var _context7;
if (subFilter.length < 2) return false;
let key;
return _everyInstanceProperty(_context7 = _sliceInstanceProperty(subFilter).call(subFilter, 1)).call(_context7, subFilter => {
if (!_Array$isArray(subFilter)) return false;
if (!_includesInstanceProperty(comparisonOps).call(comparisonOps, subFilter[0])) return false;
if (subFilter.length !== 3) return false;
if (key && !isEqual(key, subFilter[1])) return false;
key = subFilter[1];
return true;
});
}
if (!_includesInstanceProperty(membershipOps).call(membershipOps, subFilter[0])) return false;
if (subFilter.length < 3) return false;
return true;
});
} // Takes a list of options for a select_one field defined in a preset, and adds
// in any values that exist in the data that are not already listed in the field
// definition. Mapeo Desktop (currently) allows you to add new option to a
// select_one field which aren't defined in the preset, and this allows you to
// filter by these new values
function combineOptionsWithStats(fieldOptions
/*: SelectOptions*/
, fieldStats
/*: FieldStatistic*/
)
/*: SelectOptions*/
{
var _context8;
if (!fieldStats) return fieldOptions;
const optionsWithStats = [...fieldOptions];
_forEachInstanceProperty(_context8 = _Object$keys(fieldStats)).call(_context8, valueType => {
if (!fieldStats[valueType] || !_valuesInstanceProperty(fieldStats[valueType])) return;
const values = _valuesInstanceProperty(fieldStats[valueType]);
if (!(values instanceof _Map)) return;
for (const value of _keysInstanceProperty(values).call(values)) {
if (_findInstanceProperty(optionsWithStats).call(optionsWithStats, o => o === value || typeof o === 'object' && o !== null && o.value === value)) continue;
optionsWithStats.push(value);
}
});
return optionsWithStats;
}
const useStyles = makeStyles(theme => ({
list: {
paddingTop: 0,
paddingBottom: 0,
overflowY: 'scroll'
},
settingsItem: {
paddingTop: 8,
paddingBottom: 8
},
listIcon: {
minWidth: 40
}
})); // Sort presets by sort property and then by name, then filter only point presets
function presetCompare(a, b) {
if (typeof _sortInstanceProperty(a) !== 'undefined' && typeof _sortInstanceProperty(b) !== 'undefined') {
// If sort value is the same, then sort by name
if (_sortInstanceProperty(a) === _sortInstanceProperty(b)) return compareStrings(a.name, b.name); // Lower sort numbers come before higher numbers
else return _sortInstanceProperty(a) - _sortInstanceProperty(b);
} else if (typeof _sortInstanceProperty(a) !== 'undefined') {
// If a has a sort field but b doesn't, a comes first
return -1;
} else if (typeof _sortInstanceProperty(b) !== 'undefined') {
// if b has a sort field but a doesn't, b comes first
return 1;
} else {
// if neither have sort defined, compare by name
return compareStrings(a.name, b.name);
}
}
function compareStrings(a = '', b = '') {
return a.toLowerCase().localeCompare(b.toLowerCase());
}
//# sourceMappingURL=FilterPanel.js.map