Przeglądaj źródła

+ test TableAjax React view

Piotr Labudda 7 lat temu
rodzic
commit
558b3562b1

+ 4 - 0
SE/se-lib/TableAjax.php

@@ -429,6 +429,10 @@ class TableAjax extends ViewAjax {
 			'URI_WPS' => Request::getPathUri() . 'wps.php',
 			'DBG' => ('1' === V::get('DBG_JS', '', $_GET)),
 		]);
+		UI::inlineJS(__FILE__ . '.createTableDataStateObject.js', [
+			'URI_WPS' => Request::getPathUri() . 'wps.php',
+			'DBG' => ('1' === V::get('DBG_JS', '', $_GET)),
+		]);
 		UI::inlineJS(__FILE__ . '.p5UI__TableAjax.js', [
 			'DBG' => ('1' === V::get('DBG_JS', '', $_GET)),
 		]);

+ 69 - 3
SE/se-lib/TableAjax.php.TableAjax.js

@@ -10,6 +10,7 @@ if (!global.p5VendorJs) throw "Missing p5 Vendor js libs";
 if (!global.p5VendorJs.Redux) throw "Missing p5 Vendor js lib: Redux";
 if (!global.createTableFiltersStateObject) throw "Missing createTableFiltersStateObject";
 if (!global.createTableSelectedStateObject) throw "Missing createTableSelectedStateObject";
+if (!global.createTableDataStateObject) throw "Missing createTableDataStateObject";
 
 var createReactClass = global.p5VendorJs.createReactClass;
 var h = global.p5VendorJs.React.createElement;
@@ -785,7 +786,6 @@ var TableAjax = function() {
 			jQuery(window).on('resize', priv.onWindowResize)
 		}
 		priv.initialRender();// set up _uiNode$...
-		if (RENDER_AS_REACT) priv.initialReactRender(); // set up _reactUITableNode
 		priv.options.types.string = ((priv.options.types || {}).string || {});
 		priv.options.types.number = ((priv.options.types || {}).number || {});
 		priv.options.types.bool = ((priv.options.types || {}).bool || {});
@@ -803,6 +803,61 @@ var TableAjax = function() {
 
 		_state._forceFilterQuery = p5Utils__objectToQueryWithKeyPrefix(priv.options.forceFilterInit, 'f_')
 
+		var fetchDataPromise = (function (props) {
+			var props = props || {};
+			return function (namespace, filterParams) {
+				var urlAdd = '';
+				DBG && console.log('DBG: fetchDataPromise', props.url)
+				return window.fetch(props.url + urlAdd, {
+				  method: 'GET', // priv.options.urlPost ? 'POST' : 'GET',
+					credentials: 'same-origin',// add cookies
+				}).then(function (response) {
+					// TODO: handle parse json error
+					return response.json()
+				}).then(function (responseJson) {
+					// if (priv.options.debug || DBG) console.log('loadDataAjax:fetch:loadPage: request finished, data:', data);
+					// // props.notify(data);
+					if ('success' !== responseJson.type) throw responseJson.msg || "Wystąpił problem podczas pobierania danych";
+					// 	state = {data: {}};
+					// 	state.data.cols = data.cols || {};
+					// 	state.data.rows = data.rows || [];
+					// 	state.data.total = data.total || 0;
+					// 	state.data.primaryKey = data.primaryKey || 'ID';
+					// 	state.page = data.page || 0;
+					// 	state.pageSize = data.pageSize || priv.options.pageSize;
+					// 	state.filters = data.filters || {};
+					// 	DBG && console.log('DBG::loadPage... - FETCH_DATA END priv.setState(state);', { state });
+					// 	priv.setState(state);
+					// 	_uiNode$Table.parent().parent().removeClass('AjaxTable-loading');
+					// } else {
+					// 	props.notify(data);
+					// 	props.showFailFetchDataMsg();
+					// }
+					return responseJson;
+				}).then(function (responseData) {
+					DBG && console.log('DBG: fetchDataPromise responseData', { responseData })
+					var colsOrder = []; // responseData.cols.index
+					return {
+						rawReponse: responseData,
+						colsOrder: colsOrder,
+						rows: responseData.rows,
+					}
+				}).catch(function (err) {
+					props.notify({ type: 'error', msg: ''+err });
+					props.showFailFetchDataMsg();
+				});
+			};
+		})({
+			url: priv.options.url,
+			showFailFetchDataMsg: priv.showFailFetchDataMsg.bind(priv),
+			notify: p5UI__notifyAjaxCallback,
+		});
+
+		var stateObjectTableData = createTableDataStateObject({}, fetchDataPromise);
+		DBG && console.log('DBG: stateObjectTableData', stateObjectTableData);
+		priv.options.tableDataStore = stateObjectTableData.store;
+		priv.options.tableDataActions = stateObjectTableData.actions;
+
 		var stateObjectFilter = createTableFiltersStateObject(priv.options.filterInit || {});
 		priv.options.filterStore = stateObjectFilter.store;
 		priv.options.filterActions = stateObjectFilter.actions;
@@ -878,6 +933,8 @@ var TableAjax = function() {
 			}
 		}
 
+		priv.initialReactRender(RENDER_AS_REACT); // set up _reactUITableNode, require stores created
+
 		if (location.hash) {
 			priv.options.router()
 		} else {
@@ -1013,13 +1070,21 @@ var TableAjax = function() {
 	priv.getStickyCol3Width = function () { return 50 + 2 * 5 + 1; }
 	priv.getStickyColsSumWidth = function () { return priv.getStickyCol1Width() + priv.getStickyCol2Width() + priv.getStickyCol3Width(); }
 
-	priv.initialReactRender = function () { // TODO: render table as React node
+	priv.initialReactRender = function (isVisible) { // TODO: render table as React node
+		if (!isVisible) return; // TODO: double request fetchData BUG `_task=loadDataAjax` - second request send without filter/sort
 		_reactUITableNode = document.createElement('div')
+		if (!isVisible) _reactUITableNode.style.display = 'none';
 		_reactUITableNode.setAttribute('class', "TableAjax-reactUITableNode");
 		_uiNodeCont.parentNode.appendChild(_reactUITableNode);
 		ReactDOM.render(
 			h(p5UI__TableAjax, {
-				namespace: priv.options.namespace
+				namespace: priv.options.namespace,
+				dataStore: priv.options.tableDataStore,
+				dataActions: priv.options.tableDataActions,
+				filterStore: priv.options.filterStore,
+				filterActions: priv.options.filterActions,
+				selectedStore: priv.options.selectedStore,
+				selectedActions: priv.options.selectedActions,
 			}),
 			_reactUITableNode
 		);
@@ -1879,6 +1944,7 @@ var TableAjax = function() {
 	};
 
 	priv.renderTableTheadFilter = function() {
+		DBG && console.log('DBG::renderTableTheadFilter...');
 		var nodeClass = 'tblAjax__' + 'head__filter',
 				currentNode = _uiNode$Table.find('thead').find('.' + nodeClass),
 				node;

+ 141 - 0
SE/se-lib/TableAjax.php.createTableDataStateObject.js

@@ -0,0 +1,141 @@
+var DBG = DBG || false;
+if (!URI_WPS) throw "Missing URI_WPS";
+var DBG1 = true;
+if (!global.p5VendorJs) throw "Missing p5 Vendor js libs";
+if (!global.p5VendorJs.Redux) throw "Missing p5 Vendor js lib: Redux";
+var Redux = global.p5VendorJs.Redux;
+var ReduxThunk = global.p5VendorJs.ReduxThunk;
+var createStoreWithThunkMiddleware = Redux.applyMiddleware(ReduxThunk)(Redux.createStore); // TODO: to vendor.js
+
+function createTableStoreWithInitialData(initialFilter) {
+	DBG && console.log('INIT: initialFilter', { initialFilter });
+	var initialState = {
+		width: 1000,
+		isLoading: false, sentRequestId: 0, receivedRequestId: 0,
+		isEmpty: true,
+		rows: [],
+	}
+	Object.keys(initialFilter).forEach(function (key) {
+		if ('currSortCol' === key) {
+			initialState.currSortCol = initialFilter[key]
+		} else if ('currSortFlip' === key) {
+			if ('desc' === initialFilter[key]) initialState.currSortFlip = true;
+			else if ('asc' === initialFilter[key]) initialState.currSortFlip = false;
+			else initialState.currSortFlip = Boolean(initialFilter[key]);
+		} else if ('f_' === key.substr(0, 2)) {
+			initialState.filter.set(key.substr(2), initialFilter[key])
+		} else if ('sf_' === key.substr(0, 3)) {
+			initialState.specialFilter.set(key.substr(3), initialFilter[key])
+		}
+	})
+
+	return function tableStore(state, action) {
+		var prevState = state || initialState;
+		DBG && console.warn('DBG:tableStore:' + action.type, action);
+		switch (action.type) {
+			case 'SET_SENT_REQUEST_ID': return Object.assign({}, prevState, {
+				sentRequestId: action.sentRequestId,
+				filterParams: action.filterParams, // needed?
+				isLoading: true,
+			});
+			case 'SET_RESPONSE': return Object.assign({}, prevState, {
+				receivedRequestId: action.response.requestId,
+				rows: action.response.body.rows,
+				isLoading: (prevState.sentRequestId > action.response.requestId) ? true : false,
+			});
+			case 'SET_LOADING': return Object.assign({}, prevState, {
+				isLoading: Boolean(action.isLoading),
+			});
+			case 'SET_DATA': return Object.assign({}, prevState, {
+				rows: action.rows
+			});
+			case 'SET_WIDTH': return Object.assign({}, prevState, {
+				width: action.width
+			});
+			default: return prevState;
+		}
+	}
+}
+function createTableDataStoreActions(fetchDataPromise) {
+	var _fetchDataPromise = fetchDataPromise;
+	var delay = 450;
+	var _filterTimeout = null;
+	var _clearTimer = function () {
+		if (_filterTimeout) {
+			clearTimeout(_filterTimeout);
+		}
+	}
+	var _setTimer = function (timeout) {
+		_filterTimeout = timeout;
+	}
+
+	function setSentRequestId(sentRequestId, filterParams) {
+		return { type: 'SET_SENT_REQUEST_ID', sentRequestId: sentRequestId, filterParams: filterParams };
+	}
+	function setResponse(response) {
+		return { type: 'SET_RESPONSE', response: response };
+	}
+
+	function loadData(namespace, filterParams) {
+		DBG && console.log('DBG:tableStore:loadData #JSTA1', { namespace, filterParams });
+		return function (dispatch, getState) {
+			// dispatch( { type: 'SET_LOADING', isLoading: true } );
+			return new Promise(function (resolve, reject) {
+				var state = getState();
+				DBG && console.log('DBG:tableStore:loadData #JSTA2 Promise', { namespace, filterParams, state });
+				var this__requestId = 1 + state.sentRequestId;
+				dispatch(setSentRequestId(this__requestId));
+
+				// _clearTimer();
+				// _setTimer(
+				// 	setTimeout(function () {
+				// 		// reject("Error");
+				// 		var value = {
+				// 			rows: window._.range( window._.random(3, 21) ).map(function (val, idx) {
+				// 				var pk = idx + 1;
+				// 				return { '@primaryKey': pk, id: pk, label: 'row ' + pk, cell_2: 'cell 2', cell_3: 'cell 3', cell_4: 'cell 4', cell_5: 'cell 5', cell_6: 'cell 6', cell_7: 'cell 7', cell_8: 'cell 8' };
+				// 			}).reverse()
+				// 		};
+				//
+				// 		var response = {
+				// 			requestId: this__requestId,
+				// 			msg: "Pobrano dane",
+				// 			body: {
+				// 				rows: value.rows
+				// 			}
+				// 		};
+				// 		// resolve(response);
+				// 		dispatch(setResponse(response)); // TODO: depend on delay - ajax queue
+				// 	}, delay)
+				// );
+				_fetchDataPromise(namespace, filterParams).then(function (tableData) {
+					DBG && console.log('DBG:tableStore:loadData #JSTA3 resolve', { tableData });
+					var response = {
+						requestId: this__requestId,
+						msg: "Pobrano dane",
+						body: tableData,
+					}
+					dispatch(setResponse(response));
+				}).catch(reject)
+			}).catch(function (err) {
+				DBG && console.warn('DBG:tableStore:loadData #JSTA4 reject', { err });
+				// dispatch(setErrorResponse(err)); // TODO display error
+			})
+		}
+	}
+
+	return {
+		loadData: loadData,
+	}
+}
+
+
+function createTableDataStateObject(initialData, fetchDataPromise) {
+	var _initialData = initialData || {};
+	return {
+		store: createStoreWithThunkMiddleware( createTableStoreWithInitialData( _initialData ) ),
+		actions: createTableDataStoreActions(fetchDataPromise),
+	}
+}
+
+global.createTableDataStateObject = createTableDataStateObject;

+ 388 - 32
SE/se-lib/TableAjax.php.p5UI__TableAjax.js

@@ -1,42 +1,321 @@
 var DBG = DBG || false;
 var DBG1 = true;
 if (!global.p5VendorJs) throw "Missing p5 Vendor js libs";
-if (!global.p5VendorJs.Redux) throw "Missing p5 Vendor js lib: Redux";
+// if (!global.p5VendorJs.Redux) throw "Missing p5 Vendor js lib: Redux";
 var createReactClass = global.p5VendorJs.createReactClass;
 var h = global.p5VendorJs.React.createElement;
 var ReactDOM = global.p5VendorJs.ReactDOM;
-var Redux = global.p5VendorJs.Redux;
-var ReduxThunk = global.p5VendorJs.ReduxThunk;
-var createStoreWithThunkMiddleware = Redux.applyMiddleware(ReduxThunk)(Redux.createStore); // TODO: to vendor.js
+// var Redux = global.p5VendorJs.Redux;
+// var ReduxThunk = global.p5VendorJs.ReduxThunk;
+// var createStoreWithThunkMiddleware = Redux.applyMiddleware(ReduxThunk)(Redux.createStore); // TODO: to vendor.js
 
-var cellFontSize = 12;
-var cellLineHeight = 18;
+var baseCellStyle = {
+	height: 26,
+	padding: 4
+};
+var baseStickyCellStyle = Object.assign({}, baseCellStyle, {
+	position: "absolute", top: "auto", left: "0",
+	'background-color': "#fff"
+});
+var Defaults = {
+	cellFontSize: 12,
+	rowsPerPage: 10,
+	cellLineHeight: 18,
+	baseCellStyle: baseCellStyle,
+	baseStickyCellStyle: baseStickyCellStyle,
+}
+
+function p5Utils__mapToQuery(map, callback) {
+	var mapCallback = ('function' === typeof callback) ? callback : function (key) {
+		return '' + key + '=' + encodeURIComponent(map.get(key));
+	};
+	return Array.from(map.keys()).sort()
+		.map(mapCallback)
+		.join('&')
+}
+function p5Utils__mapToQueryWithKeyPrefix(map, keyPrefix, callback) {
+	var mapCallback = ('function' === typeof callback) ? callback : function (key) {
+		return '' + (keyPrefix || '') + key + '=' + encodeURIComponent(map.get(key));
+	};
+	return p5Utils__mapToQuery(map, mapCallback)
+}
+function p5Utils__objectToQueryWithKeyPrefix(obj, prefix, callback) {
+	if (!obj) return '';
+	var mapCallback = ('function' === typeof callback) ? callback : function (key) {
+		return '' + key + '=' + encodeURIComponent(obj[key]);
+	};
+	return Object.keys(obj).sort()
+		.map(mapCallback)
+		.join('&')
+}
+
+var TableAjax_Feature_FunctionsCell = function (props) {
+	DBG && console.warn('DBG::TableAjax_Feature_FunctionsCell::render', { props: props });
+	return h('div', {
+		style: props.style
+	}, [
+		(props.primaryKey) ? "F(" + props.primaryKey + ")" : null
+	]);
+};
+var TableAjax_Feature_SelectedCell = function (props) {
+	DBG && console.warn('DBG::TableAjax_Feature_SelectedCell::render', { props: props });
+	return h('div', {
+		style: props.style
+	}, [
+		(props.primaryKey) ? h('input', { type: 'checkbox', title: props.primaryKey }) : null
+	]);
+};
+var TableAjax_Feature_PrimaryKeyCell = function (props) {
+	return h('div', {
+		style: props.style
+	}, props.primaryKey);
+};
 
+var TableAjax_Filter_FunctionsCell = function (props) {
+	DBG && console.warn('DBG::TableAjax_Filter_FunctionsCell::render', { props: props });
+	return h('div', {
+		style: props.style
+	}, [
+		"(x)"
+	]);
+};
+var TableAjax_Filter_SelectedCell = function (props) {
+	DBG && console.warn('DBG::TableAjax_Filter_SelectedCell::render', { props: props });
+	return h('div', {
+		style: props.style
+	}, [
+		h('input', { type: 'checkbox' })
+	]);
+};
+var TableAjax_Filter_PrimaryKeyCell = function (props) {
+	return h('div', {
+		style: props.style
+	}, [
+		h('input', {
+			type: "text",
+			placeholder: "%",
+			style: {
+				'box-sizing': "border-box",
+				padding: "0 5px",
+				width: "100%",
+				height: "100%",
+				'border': "0px",
+				'border-bottom': "2px solid transparent",
+				'background-color': "#eee"
+			}
+		})
+	]);
+};
+
+var TableAjax_Label_FunctionsCell = function (props) {
+	DBG && console.warn('DBG::TableAjax_Label_FunctionsCell::render', { props: props });
+	return h('div', {
+		style: props.style
+	}, [
+		"(icons)"
+	]);
+};
+var TableAjax_Label_SelectedCell = function (props) {
+	DBG && console.warn('DBG::TableAjax_Label_SelectedCell::render', { props: props });
+	return h('div', {
+		style: props.style
+	}, [
+		h('input', { type: 'checkbox' })
+	]);
+};
+var TableAjax_Label_PrimaryKeyCell = function (props) {
+	return h('div', {
+		style: props.style
+	}, props.label);
+};
+
+var TableAjax_Tbody = createReactClass({
+	renderDataRow: function (value, rowIdx) {
+		var allColsCount = 13; // TODO: (this.state.cols.length || 0) + 3;
+		var renderRowCell = this.renderRowCell(rowIdx).bind(this);
+		return h('tr', {},
+			[
+				this.renderStickyRowCell(rowIdx)
+			].concat(
+				Array.apply(null, { length: allColsCount }).map(renderRowCell)
+			)
+		);
+	},
+	renderRowCell: function (rowIdx) {
+		var _rowIdx = rowIdx;
+		var _item = this.getItem(rowIdx);
+		return function (value, cellIdx) {
+			var value = (_item) ? Object.values(_item).slice(3)[ cellIdx ] : ''; // TODO: convert item to values Array by fields order - function itemToGridValuesList - defined in parent, passed by prop
+			return h('td', {
+				style: Object.assign({}, Defaults.baseCellStyle, {
+
+				})
+			}, value);
+		}
+	},
+	renderStickyRowCell: function (rowIdx) {
+		var cell1Width = this.props.stickyColWidths[0];
+		var cell2Width = this.props.stickyColWidths[1];
+		var cell3Width = this.props.stickyColWidths[2];
+		var cell1Style = Object.assign({}, Defaults.baseStickyCellStyle, { position: "absolute", top: "auto", left: "0", width: (cell1Width - 1), height: Defaults.baseCellStyle.height - 1 });
+		var cell2Style = Object.assign({}, Defaults.baseStickyCellStyle, { position: "absolute", top: "auto", left: cell1Width, width: (cell2Width - 1), height: Defaults.baseCellStyle.height - 1 });
+		var cell3Style = Object.assign({}, Defaults.baseStickyCellStyle, { position: "absolute", top: "auto", left: cell1Width + cell2Width, width: (cell3Width - 1), height: Defaults.baseCellStyle.height - 1 });
+		var item = this.getItem(rowIdx);
+		var primaryKey = (item) ? item['@primaryKey'] : null;
+		return h('td', {
+			style: Object.assign({}, Defaults.baseCellStyle, {
+				display: "block", position: "absolute", left: 0, top: "auto",
+				width: (cell1Width + cell2Width + cell3Width),
+				padding: 0,
+				'background-color': "#ddd"
+			})
+		}, [
+			h(TableAjax_Feature_FunctionsCell,  { style: cell1Style, primaryKey: primaryKey }),
+			h(TableAjax_Feature_SelectedCell,   { style: cell2Style, primaryKey: primaryKey }),
+			h(TableAjax_Feature_PrimaryKeyCell, { style: cell3Style, primaryKey: primaryKey }),
+		]);
+	},
+	getItem: function (rowIdx) {
+		return (this.props.rows.length > rowIdx) ? this.props.rows[rowIdx] : null; // { primaryKey: rowIdx } // TODO: real data
+	},
+	shouldComponentUpdate: function (nextProps, nextState) {
+		// receivedRequestId
+		var shouldUpdate = false;
+		if (nextProps.receivedRequestId > this.props.receivedRequestId) shouldUpdate = true;
+		if (nextProps.rowsPerPage != this.props.rowsPerPage) shouldUpdate = true;
+		else if (nextProps.stickyColWidths.join(',') != this.props.stickyColWidths.join(',')) shouldUpdate = true;
+		DBG && console.log("DBG:TableAjax_Tbody:shouldComponentUpdate", { shouldUpdate, props: this.props, nextProps, state: this.state, nextState });
+		return shouldUpdate;
+	},
+	render: function () {
+		DBG && console.log("DBG:TableAjax_Tbody:render");
+		return h('tbody', {}, [
+			Array.apply(null, { length: this.props.rowsPerPage }).map(this.renderDataRow)
+		]);
+	}
+});
+
+
+// p5UI__TableAjax.props.dataStore: priv.options.tableDataStore,
+// p5UI__TableAjax.props.dataActions: priv.options.tableDataActions,
+// p5UI__TableAjax.props.filterStore: priv.options.filterStore,
+// p5UI__TableAjax.props.filterActions: priv.options.filterActions,
+// p5UI__TableAjax.props.selectedStore: priv.options.selectedStore,
+// p5UI__TableAjax.props.selectedActions: priv.options.selectedActions,
 var p5UI__TableAjax = createReactClass({
+	// props.namespace
+	// props.width - table max width
+	// @doc: element.scrollLeft = intValue; // set scroll left for dom element
+	// TODO: stickyCols = [ rowFunctions, selectFeature, primaryKey, ...custom fields ]
+	_refContentEl: null,
+	setContentElRef: function (el) {
+		this._refContentEl = el;
+	},
+
+	_getStateFromDataStore: function () {
+		var state = this.props.dataStore.getState();
+		DBG && console.log('DBG::P5UI__TableAjax::_getStateFromDataStore', { state: state });
+		return {
+			width: state.width,
+			rowsPerPage: state.rowsPerPage || Defaults.rowsPerPage,
+			isLoading: state.isLoading,
+			sentRequestId: state.sentRequestId,
+			receivedRequestId: state.receivedRequestId,
+			rows: state.rows
+		};
+	},
+	getItem: function (rowIdx) {
+		return (this.state.rows.length > rowIdx) ? this.state.rows[rowIdx] : null; // { primaryKey: rowIdx } // TODO: real data
+	},
 	getInitialState: function () {
 		var cols = Array.apply(null, { length: 13 }).map(function (undefinedValue, cellIdx) {
 			var label = "Col("+cellIdx+")" + ( cellIdx % 2 ? "<br>Col line 2..." : "" );
 			return label;
 		});
-		return {
+		var baseCellStyle = {
+			height: 26,
+			padding: 4
+		};
+		var baseStickyCellStyle = Object.assign({}, baseCellStyle, {
+			position: "absolute", top: "auto", left: "0",
+			'background-color': "#fff"
+		});
+		return Object.assign({
 			isLoading: false,
+			filter: null,
 			cols: cols,
 			data: [],
 			rowsPerPage: 10,
+			baseCellStyle: baseCellStyle,
+			baseStickyCellStyle: baseStickyCellStyle,
+			stickyColWidths: [
+				50,
+				25,
+				75 // 63
+			]
+		}, this._getStateFromDataStore());
+	},
+	componentDidMount: function () {
+		DBG && console.log('DBG::P5UI__TableAjax::componentDidMount');
+		this._unsubscribeDataStore = this.props.dataStore.subscribe(this._dataStoreUpdated)
+		this._unsubscribeFilterStore = this.props.filterStore.subscribe(this._filterStoreUpdated)
+	},
+	componentWillUnmount: function () {
+		this._unsubscribeDataStore()
+		this._unsubscribeFilterStore()
+	},
+	_dataStoreUpdated: function () {
+		DBG && console.log('DBG::P5UI__TableAjax::_dataStoreUpdated');
+		this.setState(this._getStateFromDataStore())
+	},
+	_filterStoreUpdated: function () {
+		DBG && console.log('DBG::P5UI__TableAjax::_filterStoreUpdated');
+		var curFilterState = this._getStateFromFilterStore();
+		var needFetchData = (
+			!this.state.filter
+			|| curFilterState.filterQuery !== this.state.filter.filterQuery
+			|| curFilterState.specialFilterQuery !== this.state.filter.specialFilterQuery
+			|| curFilterState.currSortCol !== this.state.filter.currSortCol
+			|| curFilterState.currSortFlip !== this.state.filter.currSortFlip
+		);
+		if (needFetchData) {
+			this.setState({ filter: curFilterState })
+			this.props.dataStore.dispatch(this.props.dataActions.loadData(this.props.namespace, curFilterState))
+		}
+	},
+	_getStateFromFilterStore: function () {
+		var curFilterState = this.props.filterStore.getState();
+		return {
+			filterQuery: p5Utils__mapToQueryWithKeyPrefix(curFilterState.filter, 'f_'),
+			specialFilterQuery: p5Utils__mapToQueryWithKeyPrefix(curFilterState.specialFilter, 'sf_'),
+			currSortCol: curFilterState.currSortCol,
+			currSortFlip: curFilterState.currSortFlip
 		};
 	},
+	shouldComponentUpdate: function (nextProps, nextState) {
+		DBG && console.log('DBG::P5UI__TableAjax::shouldComponentUpdate - TODO only when data changed?', { props: this.props, nextProps, state: this.state, nextState });
+		return true;
+		// var dataChanged = true; // TODO compare this.state.rows with nextState.rows array
+		// var getPk = function (item) {
+		// 	return item['@primaryKey'];
+		// }
+		// var listPks = this.state.rows.map(getPk).join(',');
+		// var prevPks = nextState.rows.map(getPk).join(',');
+		// DBG && console.log('DBG::P5UI__TableAjax::shouldComponentUpdate', { state: this.state, nextState, listPks, prevPks });
+		// return (
+		// 	this.state.isLoading !== nextState.isLoading
+		// 	|| this.state.width !== nextState.width
+		// 	|| dataChanged
+		// );
+	},
+
+
 	getTheadCellHeight: function () {
 		var maxLines = this.state.cols.reduce(function (ret, cell) {
 			var label = cell;
 			return Math.max(ret, label.replace('<br>', '###').replace('<br/>', '###').split("###").length);
 		}, 1)
-		return 2 * 2 + maxLines * cellLineHeight;
-	},
-	renderRowCell: function (rowIdx) {
-		var _rowIdx = rowIdx;
-		return function (value, cellIdx) {
-			return h('td', {}, "("+_rowIdx+"/"+cellIdx+")");
-		}
+		return 2 * 2 + maxLines * Defaults.cellLineHeight;
 	},
 	renderTheadColNameRowCell: function (value, cellIdx) {
 		var label = this.state.cols[cellIdx];
@@ -44,8 +323,8 @@ var p5UI__TableAjax = createReactClass({
 			style: {
 				padding: "2px 120px",
 				'white-space': 'nowrap',
-				'line-height': cellLineHeight+"px",
-				'font-size': cellFontSize+"px",
+				'line-height': Defaults.cellLineHeight+"px",
+				'font-size': Defaults.cellFontSize+"px",
 				'border-bottom-width': "1px"
 			}
 		}, label.replace('<br>', '###<br>###').replace('<br/>', '###<br>###').split("###").map(function (txtOrBr) {
@@ -60,55 +339,132 @@ var p5UI__TableAjax = createReactClass({
 				placeholder: "%",
 				style: {
 					'box-sizing': "border-box",
-					padding: "0 5px",
 					width: "100%",
-					height: "21px",
-					'border': "0px",
+					height: "26px",
+					padding: "4px 5px 0 5px",
+					'font-size': "12px",
+					'border': 0,
 					'border-bottom': "2px solid transparent",
 					'background-color': "#eee"
 				}
 			})
 		]);
 	},
-	renderRow: function (value, rowIdx) {
-		var allColsCount = 13; // TODO: (this.state.cols.length || 0) + 3;
-		var renderRowCell = this.renderRowCell(rowIdx);
-		return h('tr', {}, Array.apply(null, { length: allColsCount }).map(renderRowCell));
+	renderStickyTheadNameRowCell: function (item) {
+		var headerCellHeight = this.getTheadCellHeight();
+		var cell1Width = this.state.stickyColWidths[0];
+		var cell2Width = this.state.stickyColWidths[1];
+		var cell3Width = this.state.stickyColWidths[2];
+		var cell1Style = Object.assign({}, this.state.baseStickyCellStyle, { position: "absolute", top: "auto", left: "0", width: (cell1Width - 1), height: headerCellHeight });
+		var cell2Style = Object.assign({}, this.state.baseStickyCellStyle, { position: "absolute", top: "auto", left: cell1Width, width: (cell2Width - 1), height: headerCellHeight });
+		var cell3Style = Object.assign({}, this.state.baseStickyCellStyle, { position: "absolute", top: "auto", left: cell1Width + cell2Width, width: (cell3Width - 1), height: headerCellHeight });
+		var label = 'Nr'; // TODO: get primaryKey cell label
+		return h('td', {
+			style: Object.assign({}, this.state.baseCellStyle, {
+				display: "block", position: "absolute", left: 0, top: "auto",
+				width: (cell1Width + cell2Width + cell3Width),
+				height: headerCellHeight,
+				padding: 0,
+				'background-color': "#ddd"
+			})
+		}, [
+			h(TableAjax_Label_FunctionsCell,  { style: cell1Style }),
+			h(TableAjax_Label_SelectedCell,   { style: cell2Style }),
+			h(TableAjax_Label_PrimaryKeyCell, { style: cell3Style, label: label }),
+		]);
+	},
+	renderStickyTheadFilterRowCell: function (item) {
+		var cell1Width = this.state.stickyColWidths[0];
+		var cell2Width = this.state.stickyColWidths[1];
+		var cell3Width = this.state.stickyColWidths[2];
+		var cell1Style = Object.assign({}, this.state.baseStickyCellStyle, { position: "absolute", top: "auto", left: "0", width: (cell1Width - 1) });
+		var cell2Style = Object.assign({}, this.state.baseStickyCellStyle, { position: "absolute", top: "auto", left: cell1Width, width: (cell2Width - 1) });
+		var cell3Style = Object.assign({}, this.state.baseStickyCellStyle, { position: "absolute", top: "auto", left: cell1Width + cell2Width, width: (cell3Width - 1), padding: "none" });
+		return h('td', {
+			style: Object.assign({}, this.state.baseCellStyle, {
+				display: "block", position: "absolute", left: 0, top: "auto",
+				width: (cell1Width + cell2Width + cell3Width),
+				height: this.state.baseCellStyle.height + 2,
+				padding: 0,
+				'background-color': "#ddd"
+			})
+		}, [
+			h(TableAjax_Filter_FunctionsCell,  { style: cell1Style }),
+			h(TableAjax_Filter_SelectedCell,   { style: cell2Style }),
+			h(TableAjax_Filter_PrimaryKeyCell, { style: cell3Style }),
+		]);
 	},
 	render: function () {
+		DBG && console.log('DBG::P5UI__TableAjax::render', { state: this.state });
 		var baseStyle = { 'border-top': "1px solid red", 'border-bottom': "1px solid red", 'min-height': "100px" } // TODO: DBG
 		var allColsCount = 13; // TODO: (this.state.cols.length || 0) + 3;
+		var widthTotal = this.state.width || 1200;
+		var widthStickyCols = this.state.stickyColWidths.reduce(function (a, b) { return a + b; }, 0);
+		var widthScrollableContent = widthTotal - widthStickyCols - 2;
 
 		return h('div', {
 			className: "p5UI__TableAjax",
 			style: Object.assign(baseStyle, {
+				'background-color': "#ddd",
 			})
 		}, [
 			h('div', { style: { 'background-color': "#f00", color: "#fff", padding: "3px 12px" } }, "namespace: '" + this.props.namespace + "' getTheadCellHeight("+this.getTheadCellHeight()+")"),
 			h('div', {
+				ref: this.setContentElRef,
 				style: {
-					'margin-left': "163px",
-					'min-height': "360px",
+					'margin-left': widthStickyCols + 2,
+					// 'min-height': "300px",
 					'overflow': "scroll visible",
 					'padding-bottom': "1px",
 					'clear': "both",
-					'width': "1187px",
+					'width': widthScrollableContent,
+					'background-color': "#fff",
 				}
 			}, [
 				h('table', {
 					className: "table table-bordered table-condensed",
+					style: {
+						'margin-bottom': 0,
+						'margin-left': "-2px",
+					}
 				}, [
 					h('thead', {}, [
-						h('tr', {}, Array.apply(null, { length: allColsCount }).map(this.renderTheadColNameRowCell)),
-						h('tr', {}, Array.apply(null, { length: allColsCount }).map(this.renderTheadFilterRowCell))
+						h('tr', {}, [ this.renderStickyTheadNameRowCell() ].concat( Array.apply(null, { length: allColsCount }).map(this.renderTheadColNameRowCell) )),
+						h('tr', {}, [ this.renderStickyTheadFilterRowCell() ].concat( Array.apply(null, { length: allColsCount }).map(this.renderTheadFilterRowCell) ))
 					]),
-					h('tbody', {}, [
-						Array.apply(null, { length: this.state.rowsPerPage }).map(this.renderRow)
-					])
+					h(TableAjax_Tbody, {
+						rowsPerPage: this.state.rowsPerPage,
+						receivedRequestId: this.state.receivedRequestId,
+						rows: this.state.rows,
+						stickyColWidths: this.state.stickyColWidths,
+						// store: this.props.dataStore
+					})
 				])
+			]),
+
+
+			h('hr', { style: { border: "5px solid red" } }),
+			h('pre', { style: { padding: "6px 12px", border: "1px solid red", 'background-color': "#eee" } }, [
+				"State: ",
+				(this.state.isLoading) ? " loading... " : null,
+			]),
+			h('div', { style: { padding: "6px 12px", border: "1px solid red", 'background-color': "#eee" } }, [
+				"TEST btns: ",
+				h('button', { onClick: this._testScrollContentTo100Left }, "scroll 100"),
+				h('button', { onClick: this._testScrollContentTo200Left }, "scroll 200"),
+				h('button', { onClick: this._testScrollContentTo0Left }, "scroll 0"),
+				h('button', { onClick: this._testLoadData }, "load data"),
 			])
+
 		]);
-	}
+	},
+
+	_testLoadData: function () { this.props.dataStore.dispatch(this.props.dataActions.loadData(this.props.namespace)); },
+	_testScrollContentTo100Left: function () { this._refContentEl.scrollLeft = 100; },
+	_testScrollContentTo200Left: function () { this._refContentEl.scrollLeft = 200; },
+	_testScrollContentTo0Left: function () { this._refContentEl.scrollLeft = 0; }
+
 });
 
 global.p5VendorJs['p5UI__TableAjax'] = p5UI__TableAjax;
+// export default p5UI__TableAjax

+ 0 - 5
SE/se-lib/TableAjax.php.p5UI__selected.js

@@ -1,13 +1,8 @@
 var DBG = DBG || false;
 var DBG1 = true;
 if (!global.p5VendorJs) throw "Missing p5 Vendor js libs";
-if (!global.p5VendorJs.Redux) throw "Missing p5 Vendor js lib: Redux";
 var createReactClass = global.p5VendorJs.createReactClass;
 var h = global.p5VendorJs.React.createElement;
-var ReactDOM = global.p5VendorJs.ReactDOM;
-var Redux = global.p5VendorJs.Redux;
-var ReduxThunk = global.p5VendorJs.ReduxThunk;
-var createStoreWithThunkMiddleware = Redux.applyMiddleware(ReduxThunk)(Redux.createStore); // TODO: to vendor.js
 var p5UI__FieldCheckboxLoading = global.p5VendorJs['p5UI__FieldCheckboxLoading'];
 
 var P5UI__TableAjaxRowCheckbox = createReactClass({