Commit 79f0fd3e authored by Pierre Marchand's avatar Pierre Marchand

Merge branch 'streaming-wip' into 'master'

Streaming wip

See merge request !7
parents e85a112a 2d9ec725
......@@ -123,6 +123,7 @@ export const tableEvents = (dispatch: TableSetter) => {
);
return {
...state,
window: { ...state.window, offset: 0 },
search: {
...search,
filters: search.filters.concat([filter])
......
......@@ -36,7 +36,11 @@ export interface TableDataRow {
cells: TableDataCell[];
}
export type TableWindow = { offset: number; size: number; autoScroll: boolean };
export type TableWindow = {
offset: number;
size: number;
autoScroll: boolean,
};
export interface ITableSort {
col: number | null;
......@@ -138,13 +142,24 @@ export type ToolbarFn = () => DOMElement<{}, Element>;
export type SelectRowHandler = (a: TableDataRow) => void;
export type SelectCellHandler = (a: TableDataRow, b: number) => void;
export interface TableSource {
export interface TableSourceLocal {
readonly kind: 'local';
data: TableDataRow[];
keys: string[];
types: TableDataType[];
}
export interface TableSourceRemote {
readonly kind: 'remote';
data: TableDataRow[];
keys: string[];
types: TableDataType[];
}
export type TableSource = TableSourceLocal | TableSourceRemote;
export const emptySource = (): TableSource => ({
kind: 'local',
data: [],
keys: [],
types: []
......@@ -152,10 +167,6 @@ export const emptySource = (): TableSource => ({
export interface Config {
className: string;
// loadData: LoadDataFn;
// loadKeys: LoadKeysFn;
// loadTypes: LoadTypesFn;
toolbar?: ToolbarFn;
onRowSelect?: SelectRowHandler;
onCellSelect?: SelectCellHandler;
......
......@@ -134,7 +134,7 @@ const makeDateTimeFilter = (f: FilterDateTime) => {
};
const makeFilterFunction = (fs: Filter[]) => {
const filters = fs.map(f => {
const filters = fs.map((f) => {
switch (f.tag) {
case 'string':
return makeStringFilter(f);
......@@ -181,6 +181,7 @@ export const tableQueries = (
const getFilteredData = () => {
const { data, types } = getSource();
const { search, sort } = getTable();
if (sort.col !== null || search.filters.length > 0) {
const f = filter(search.filters);
......@@ -196,6 +197,10 @@ export const tableQueries = (
return getTable().loaded;
},
getKind() {
return getSource().kind;
},
getKeys(): string[] {
return getSource().keys;
},
......@@ -214,12 +219,16 @@ export const tableQueries = (
},
getData(window?: TableWindow): TableDataRow[] {
if ('remote' === getSource().kind) {
return getSource().data;
}
if (window) {
return getFilteredData().slice(
window.offset,
window.offset + window.size
);
} else {
}
else {
return getFilteredData();
}
},
......
import { TableDataRow } from '.';
import { INPUT, SPAN, DIV } from '../elements';
import { uniqId, date8601, parseDate, datetime8601 } from '../../util';
import tr from '../../locale';
import { spanTooltipTop } from '../tooltip';
import { PropertyTypeDescriptor } from '../../source';
export type FilterOp = 'eq' | 'gt' | 'lt';
export interface FilterString {
readonly tag: 'string';
column: number;
pattern: string;
}
export const filterString = (
column: number,
pattern: string
): FilterString => ({ tag: 'string', column, pattern });
export interface FilterNumber {
readonly tag: 'number';
column: number;
value: number;
op: FilterOp;
}
export const filterNumber = (
column: number,
value: number,
op: FilterOp
): FilterNumber => ({
tag: 'number',
column,
value,
op
});
export interface FilterDate {
readonly tag: 'date';
column: number;
date: string;
op: FilterOp;
}
export const filterDate = (
column: number,
date: string,
op: FilterOp
): FilterDate => ({
tag: 'date',
column,
date,
op
});
export interface FilterDateTime {
readonly tag: 'datetime';
column: number;
datetime: string;
op: FilterOp;
}
export const filterDateTime = (
column: number,
datetime: string,
op: FilterOp
): FilterDateTime => ({
tag: 'datetime',
column,
datetime,
op
});
export type Filter = FilterDate | FilterNumber | FilterString | FilterDateTime;
export const makeInitialFilter = (
column: number,
dataType: PropertyTypeDescriptor,
) => {
switch (dataType) {
case 'number':
return filterNumber(column, 0, 'gt');
case 'date':
return filterDate(
column,
date8601(new Date(0)),
'gt'
);
case 'datetime':
return filterDateTime(
column,
datetime8601(new Date(0)),
'gt'
);
default:
return filterString(column, '');
}
};
const makeStringFilter = (f: FilterString) => {
const pat = new RegExp(`.*${f.pattern}.*`, 'i');
const col = f.column;
return ({ cells }: TableDataRow) => pat.test(cells[col]);
};
const makeNumberFilter = (f: FilterNumber) => {
const b = f.value;
const op =
f.op === 'eq'
? (a: number) => a === b
: f.op === 'gt'
? (a: number) => a >= b
: (a: number) => a <= b;
const cond = (cell: string) => op(parseFloat(cell));
const col = f.column;
return ({ cells }: TableDataRow) => cond(cells[col]);
};
const makeDateFilter = (f: FilterDate) => {
const b = Date.parse(f.date);
const op =
f.op === 'eq'
? (a: number) => a === b
: f.op === 'gt'
? (a: number) => a >= b
: (a: number) => a <= b;
const cond = (cell: string) => op(Date.parse(cell));
const col = f.column;
return ({ cells }: TableDataRow) => cond(cells[col]);
};
const makeDateTimeFilter = (f: FilterDateTime) => {
const b = Date.parse(f.datetime);
const op =
f.op === 'eq'
? (a: number) => a === b
: f.op === 'gt'
? (a: number) => a >= b
: (a: number) => a <= b;
const cond = (cell: string) => op(Date.parse(cell));
const col = f.column;
return ({ cells }: TableDataRow) => cond(cells[col]);
};
const makeFilterFunction = (fs: Filter[]) => {
const filters = fs.map((f) => {
switch (f.tag) {
case 'string':
return makeStringFilter(f);
case 'number':
return makeNumberFilter(f);
case 'date':
return makeDateFilter(f);
case 'datetime':
return makeDateTimeFilter(f);
}
});
return (row: TableDataRow) =>
filters.reduce((acc, f) => (acc === false ? acc : f(row)), true);
};
export const filterRows = (filters: Filter[]) => (data: TableDataRow[]) =>
data.filter(makeFilterFunction(filters));
/// < render
export type SetFilterFn = (filter: Filter, index: number) => void;
export const renderFilter = (
filter: Filter,
idx: number,
keys: string[],
filterData: SetFilterFn
) => {
switch (filter.tag) {
case 'string':
return renderStringFilter(filter, idx, keys, filterData);
case 'number':
return renderNumberFilter(filter, idx, keys, filterData);
case 'date':
return renderDateFilter(filter, idx, keys, filterData);
case 'datetime':
return renderDateTimeFilter(filter, idx, keys, filterData);
}
};
const renderStringFilter = (
filter: FilterString,
idx: number,
keys: string[],
filterData: SetFilterFn
) => {
const { column, pattern } = filter;
const colName = keys[column];
const fieldName = SPAN({ className: 'search-field' }, colName);
const searchField = INPUT({
autoFocus: true,
type: 'search',
name: 'search',
className: 'table-header-search-field',
defaultValue: pattern,
onChange: e => filterData(filterString(column, e.currentTarget.value), idx)
});
return DIV(
{
className: `table-search-item`,
key: uniqId()
},
fieldName,
searchField
);
};
const opString = (op: FilterOp) => {
switch (op) {
case 'eq': return '=';
case 'gt': return '';
case 'lt': return '';
}
};
const opFieldName =
(op: FilterOp, colName: string) =>
SPAN({ className: 'search-field' }, `${colName} ${opString(op)}`);
const renderOp = (
op: FilterOp,
filter: FilterNumber | FilterDate | FilterDateTime,
idx: number,
filterData: SetFilterFn
) =>
op === filter.op
? DIV(
{
className: `picto filter-op filter-op--${op} selected`
},
opString(op)
)
: DIV(
{
className: `picto filter-op filter-op--${op} interactive`,
onClick: () => filterData(Object.assign({}, filter, { op }), idx)
},
opString(op)
);
const renderOps = (
filter: FilterNumber | FilterDate | FilterDateTime,
idx: number,
filterData: SetFilterFn
) =>
DIV(
{ className: 'filter-op__wrapper' },
SPAN({ className: 'filter-op__label' }, `${tr.core('operator')} : `),
spanTooltipTop(tr.core('equal'), {}, renderOp('eq', filter, idx, filterData)),
spanTooltipTop(tr.core('greaterThanOrEqual'), {}, renderOp('gt', filter, idx, filterData)),
spanTooltipTop(tr.core('lessThanOrEqual'), {}, renderOp('lt', filter, idx, filterData))
);
const renderNumberFilter = (
filter: FilterNumber,
idx: number,
keys: string[],
filterData: SetFilterFn
) => {
const { column, value, op } = filter;
const colName = keys[column];
const searchField = INPUT({
autoFocus: true,
type: 'number',
name: 'search',
className: 'table-header-search-field',
defaultValue: value.toString(10),
onChange: e =>
filterData(
filterNumber(column, parseFloat(e.currentTarget.value), op), idx
)
});
return DIV(
{
className: `table-search-item`,
key: uniqId()
},
opFieldName(op, colName),
searchField,
renderOps(filter, idx, filterData)
);
};
const renderDateFilter = (
filter: FilterDate,
idx: number,
keys: string[],
filterData: SetFilterFn
) => {
const { column, date, op } = filter;
const colName = keys[column];
const searchField = INPUT({
autoFocus: true,
type: 'date',
name: 'search',
className: 'table-header-search-field',
defaultValue: date8601(parseDate(date).getOrElse(new Date())),
onChange: e => filterData(filterDate(column, e.currentTarget.value, op), idx)
});
return DIV(
{
className: `table-search-item`,
key: uniqId()
},
opFieldName(op, colName),
searchField,
renderOps(filter, idx, filterData)
);
};
const renderDateTimeFilter = (
filter: FilterDateTime,
idx: number,
keys: string[],
filterData: SetFilterFn
) => {
const { column, datetime, op } = filter;
const colName = keys[column];
const searchField = INPUT({
autoFocus: true,
type: 'datetime-local',
name: 'search',
className: 'table-header-search-field',
defaultValue: datetime8601(parseDate(datetime).getOrElse(new Date())),
onChange: e => filterData(filterDateTime(column, e.currentTarget.value, op), idx)
});
return DIV(
{
className: `table-search-item`,
key: uniqId()
},
opFieldName(op, colName),
searchField,
renderOps(filter, idx, filterData)
);
};
/// render >
import { StreamingField, PropertyTypeDescriptor, streamFieldName } from '../../source';
import { DIV, NodeOrOptional } from '../elements';
import { renderTableHeader, renderTableBody, renderFilters } from './render';
import { rect } from '../../app';
import { Option, none, some } from 'fp-ts/lib/Option';
import { TableSort, SortDirection } from './sort';
import { Filter, makeInitialFilter } from './filter';
export * from './filter';
export * from './sort';
export interface TableWindow {
offset: number;
size: number;
}
export interface TableState {
position: { x: number; y: number };
rowHeight: number;
selected: number;
viewHeight: number;
window: TableWindow;
sort: Option<TableSort>;
filters: Filter[];
}
export const defaultTableState = (): TableState => ({
position: { x: 0, y: 0 },
rowHeight: 19,
selected: -1,
viewHeight: -1,
window: { offset: 0, size: 100 },
sort: none,
filters: [],
});
export type TableDataCell = string;
export interface TableDataRow {
// pointer to origin
from: number | string;
cells: TableDataCell[];
}
export interface TableData {
rows: TableDataRow[];
total: number;
}
export interface TableSource {
data: (w: TableWindow) => TableData;
fields: StreamingField[];
}
export const emptySource = (): TableSource => ({
data: _w => ({ rows: [], total: 0 }),
fields: [],
});
export type TableDispatch = (r: (state: TableState) => TableState) => void;
export type TableQuery = () => TableState;
export type RowSelectHandler = (row: TableDataRow, i: number) => void;
export type TableFilterChange = () => void;
export const table = (
dispatch: TableDispatch,
query: TableQuery,
rowHandler: RowSelectHandler,
filterChange?: TableFilterChange,
) => {
/// < events
const setTableWindowOffset = (offset: number) =>
dispatch(s => ({ ...s, window: { ...s.window, offset } }));
const setTableWindowSize = (size: number): void =>
dispatch(s => ({ ...s, window: { ...s.window, size } }));
const setPosition = (x: number, y: number): void =>
dispatch(s => ({ ...s, position: { x, y } }));
const setSort = (col: number, direction: SortDirection) => {
dispatch(s => ({ ...s, sort: some({ col, direction }) }));
filterChange?.();
};
const rectify = rect(r =>
setTableWindowSize(Math.ceil(r.height / query().rowHeight)));
const clearFilters = () =>
dispatch(s => ({ ...s, filters: [] }));
const initFilter = (
column: number,
dataType: PropertyTypeDescriptor,
) => {
dispatch((state) => {
const { filters, window } = state;
return {
...state,
window: { ...window, offset: 0 },
filters: filters.concat([makeInitialFilter(column, dataType)])
};
});
filterChange?.();
};
const setFilter = (
filter: Filter,
index: number,
) => {
dispatch((state) => {
const filters = state.filters.map((f, i) => {
if (index === i && f.column === filter.column) {
return filter;
}
return f;
});
return { ...state, filters };
});
filterChange?.();
};
/// events >
/// < ref
let it: number | null = null;
let scrolls: number[] = [];
const mount = (_el: Element) => {
scrolls = [];
it = window.setInterval(() => {
const sl = scrolls.length;
if (sl > 0) {
const offsetTop = scrolls[sl - 1];
const { rowHeight, position, window } = query();
const rowOffset = Math.ceil(offsetTop / rowHeight);
if (position.y !== offsetTop) {
setPosition(0, offsetTop);
}
if (window.offset !== rowOffset) {
setTableWindowOffset(rowOffset);
}
}
scrolls = [];
}, 64);
};
const unmount = () => {
if (it !== null) {
window.clearInterval(it);
}
};
const setTableSize = (el: Element | null) => { if (el) { rectify(el); } };
const scroll = (e: React.UIEvent<Element>): void => {
scrolls.push(e.currentTarget.scrollTop);
};
const lifeCycle = (el: Element | null) => {
if (el === null) {
unmount();
}
else {
mount(el);
}
};
/// ref >
const renderBody = renderTableBody(setTableSize, scroll, rowHandler);
const render = (
source: TableSource,
toolbar: NodeOrOptional,
) => {
const state = query();
const data = source.data(state.window);
const filters = state.filters.length > 0 ?
some(renderFilters(
state.filters,
source.fields.map(streamFieldName),
setFilter,
clearFilters,
)) :
none;
const main = DIV(
{ className: 'table-main' },
renderTableHeader(
source.fields,
state.sort,
setSort,
initFilter
),
renderBody(data, source.fields, state)
);
return DIV({ className: 'infinite-table', ref: lifeCycle }, toolbar, filters, main);
};
return render;
};
import * as debug from 'debug';
import { DIV, SPAN } from '../elements';
import {
TableDataRow,
TableState,
TableDataCell,
RowSelectHandler,
TableData,
SetFilterFn,
} from '.';
import {
StreamingField,
streamFieldType,
streamFieldName,
PropertyTypeDescriptor,