Jelajahi Sumber

added Relation filters for AntAcl objects

Piotr Labudda 7 tahun lalu
induk
melakukan
19a6f6a41f

+ 2 - 0
SE/se-lib/ACL.php

@@ -444,6 +444,7 @@ class ACL {
 				join CRM_INSTANCE_CONFIG i on ( i.namespace = c.ROOT_OBJECT_NS )
 			where ( c.CHILD_NS = :namespace )
 				and c.A_STATUS = 'NORMAL'
+			order by c.ROOT_OBJECT_NS ASC
 		", [
 			':namespace' => $namespace,
 		]);
@@ -457,6 +458,7 @@ class ACL {
 				join CRM_INSTANCE_CONFIG i on ( i.namespace = c.CHILD_NS )
 			where ( c.ROOT_OBJECT_NS = :namespace )
 				and c.A_STATUS = 'NORMAL'
+			order by c.CHILD_NS ASC
 		", [
 			':namespace' => $namespace,
 		]);

+ 76 - 0
SE/se-lib/AntAclBase.php

@@ -746,4 +746,80 @@ class AntAclBase extends Core_AclBase {
 	public function hasReadGroupField() { return $this->_hasReadGroupField; }
 	public function hasOwnerField() { return $this->_hasOwnerField; }
 
+	function getSpecialFilters() {
+		$fltrs = array();
+
+		$namespace = $this->getNamespace();
+		$availableBackRefs = array_map(function($instance) {
+			$exNs = explode('/', $instance['namespace']);
+			$label = end($exNs);
+			return [
+				'id' => $instance['idInstance'],
+				'namespace' => $instance['namespace'],
+				'label' => $label,
+			];
+		}, ACL::getBackRefList($namespace));
+		// [ 'id' => 41, 'namespace' => "default_db/TEST_PERMS/TestPermsAnt", 'label' => "TestPermsAnt" ],
+		DBG::log($availableBackRefs, 'array', "\$availableBackRefs");
+
+		$availableChildRefs = []; // TODO: ? show relation to child objects
+		$availableChildRefs = array_map(function($instance) {
+			$exNs = explode('/', $instance['namespace']);
+			$label = end($exNs);
+			return [
+				'id' => $instance['idInstance'],
+				'namespace' => $instance['namespace'],
+				'label' => $label,
+			];
+		}, ACL::getRefList($namespace));
+		// usort($availableChildRefs);
+		DBG::log($availableChildRefs, 'array', "\$availableChildRefs");
+
+		if (!empty($availableBackRefs) || !empty($availableChildRefs)) {
+			$fltrs['Relations'] = (object)[
+				'type' => 'RELATIONS',
+				'icon' => 'glyphicon glyphicon-random',
+				'label' => 'Relacje',
+				'availableBackRefs' => $availableBackRefs,
+				'availableChildRefs' => $availableChildRefs,
+			];
+		}
+		return $fltrs;
+	}
+	function parseSpecialFilter($filter, $value) { // @return string | NULL
+		if ('Ref_From_' === substr($filter, 0, strlen('Ref_From_'))) {
+			return $this->parseSpecialFilterRefFrom(substr($filter, strlen('Ref_From_')), $value);
+		}
+		if ('Ref_To_' === substr($filter, 0, strlen('Ref_To_'))) {
+			return $this->parseSpecialFilterRefTo(substr($filter, strlen('Ref_To_')), $value);
+		}
+		throw new Exception("Not Implemented special filter '{$filter}={$value}' ");
+	}
+	function parseSpecialFilterRefFrom($idInstance, $primaryKey) { // @return string | NULL
+		$sqlPkField = $this->getSqlPrimaryKeyField();
+		$parentNamespace = ACL::getInstanceNamespaceById($idInstance);
+		$refTable = ACL::getRefTable($parentNamespace, Api_WfsNs::toTypeName($this->getNamespace()) );
+		$sqlParentPk = DB::getPDO()->quote($primaryKey);
+		return "
+			t.{$sqlPkField} in (
+				select `{$refAlias}`.REMOTE_PRIMARY_KEY
+				from `{$refTable}` `{$refAlias}`
+				where `{$refAlias}`.PRIMARY_KEY = {$sqlParentPk}
+			)
+		";
+	}
+	function parseSpecialFilterRefTo($idInstance, $primaryKey) { // @return string | NULL
+		$sqlPkField = $this->getSqlPrimaryKeyField();
+		$childNamespace = ACL::getInstanceNamespaceById($idInstance);
+		$refTable = ACL::getRefTable($this->getNamespace(), Api_WfsNs::toTypeName($childNamespace) );
+		$sqlChildRefPk = DB::getPDO()->quote($primaryKey);
+		return "
+			t.{$sqlPkField} in (
+				select refTable.PRIMARY_KEY
+				from `{$refTable}` refTable
+				where refTable.REMOTE_PRIMARY_KEY = {$sqlChildRefPk}
+			)
+		";
+	}
+
 }

+ 7 - 9
SE/se-lib/Route/ViewTableAjax.php

@@ -625,15 +625,13 @@ class Route_ViewTableAjax extends RouteBase {
 		try {
 			self::getCsvTheGeomAjax();
 		} catch (Exception $e) {
-			SE_Layout::gora();
-			SE_Layout::menu();
-			SE_Layout::alert('danger', $e->getMessage());
-?>
-<div style="text-align: center;">
-  <a href="<?=$_SERVER['HTTP_REFERER']?>" class="btn btn-primary" style="width: 80px;">Powrót</a>
-</div>
-<?php
-			SE_Layout::dol();
+			UI::gora();
+			UI::menu();
+			UI::alert('danger', $e->getMessage());
+			echo UI::h('div', [ 'style' => "text-align:center" ], [
+				UI::h('a', [ 'href' => $_SERVER['HTTP_REFERER'], 'class' => "btn btn-primary", 'style' => "width:80px" ], "Powrót"),
+			]);
+			UI::dol();
 		}
 	}
 

+ 301 - 10
SE/se-lib/TableAjax.php.TableAjax.js

@@ -575,7 +575,7 @@ global['P5UI__TableAjaxSpecialFilter'] = createReactClass({
 
 	getStateFromStore: function () {
 		var state = this.props.store.getState();
-		DBG && console.log('DBG::P5UI__TableAjaxSpecialFilters::getStateFromStore (name:'+this.props.name+')', { store: state, name: this.props.name, selected: state.specialFilter.get(this.props.name) });
+		DBG && console.log('DBG::P5UI__TableAjaxSpecialFilter::getStateFromStore (name:'+this.props.name+')', { store: state, name: this.props.name, selected: state.specialFilter.get(this.props.name) });
 		return {
 			selected: state.specialFilter.get(this.props.name),
 		};
@@ -584,18 +584,18 @@ global['P5UI__TableAjaxSpecialFilter'] = createReactClass({
 		return this.getStateFromStore();
 	},
 	componentDidMount: function () {
-		DBG && console.log('DBG::P5UI__TableAjaxSpecialFilters::componentDidMount (name:'+this.props.name+')');
+		DBG && console.log('DBG::P5UI__TableAjaxSpecialFilter::componentDidMount (name:'+this.props.name+')');
 		this.unsubscribe = this.props.store.subscribe(this.storeUpdated)
 	},
 	componentWillUnmount: function () {
 		this.unsubscribe()
 	},
 	storeUpdated: function () {
-		DBG && console.log('DBG::P5UI__TableAjaxSpecialFilters::storeUpdated (name:'+this.props.name+')');
+		DBG && console.log('DBG::P5UI__TableAjaxSpecialFilter::storeUpdated (name:'+this.props.name+')');
 		this.setState(this.getStateFromStore())
 	},
 	shouldComponentUpdate: function (nextProps, nextState) {
-		DBG && console.log('DBG::P5UI__TableAjaxSpecialFilters::shouldComponentUpdate (name:'+this.props.name+')', { state: this.state, nextState});
+		DBG && console.log('DBG::P5UI__TableAjaxSpecialFilter::shouldComponentUpdate (name:'+this.props.name+')', { state: this.state, nextState});
 		return (
 			this.state.selected !== nextState.selected
 		);
@@ -624,7 +624,7 @@ global['P5UI__TableAjaxSpecialFilter'] = createReactClass({
 		}, option);
 	},
 	render: function () {
-		DBG && console.log('DBG::P5UI__TableAjaxSpecialFilters::render (name:'+this.props.name+')', { state: this.state, props: this.props });
+		DBG && console.log('DBG::P5UI__TableAjaxSpecialFilter::render (name:'+this.props.name+')', { state: this.state, props: this.props });
 		var label = this.props.label || this.props.name;
 		return h('div', { className: "btn-group", style: { margin: '0 4px 0 0' } },
 			[
@@ -652,6 +652,272 @@ global['P5UI__TableAjaxSpecialFilter'] = createReactClass({
 		);
 	}
 });
+global['P5UI__TableAjaxSpecialFilterRelations'] = createReactClass({
+	// props.store: Redux store with state: { isLoading bool, selected array of strings }
+	// props.actions: Redux store actions
+	// props.name: PropTypes.string.isRequired
+	// props.values: PropTypes.array.isRequired
+	// props.icon: PropTypes.string.isRequired
+	// props.label: PropTypes.string
+	// props.availableBackRefs: PropTypes.array.isRequired // array of objects { id, namespace, label }
+	// props.availableChildRefs: PropTypes.array.isRequired // array of objects { id, namespace, label }
+
+	getStateFromStore: function () {
+		var state = this.props.store.getState();
+		DBG && console.log('DBG::P5UI__TableAjaxSpecialFilterRelations::getStateFromStore (name:'+this.props.name+')', { store: state, name: this.props.name, selected: state.specialFilter.get(this.props.name) });
+		var selected = []; // Array of { idInstance, primaryKey }
+		state.specialFilter.forEach(function (value, key) {
+			if ('Ref_From_' === key.substr(0, 'Ref_From_'.length)) {
+				selected.push({ type: 'From', idInstance: key.substr('Ref_From_'.length), primaryKey: value, filterKey: key })
+			}
+			if ('Ref_To_' === key.substr(0, 'Ref_To_'.length)) {
+				selected.push({ type: 'To', idInstance: key.substr('Ref_To_'.length), primaryKey: value, filterKey: key })
+			}
+		})
+		DBG && console.log('TODO: DBG::P5UI__TableAjaxSpecialFilterRelations::getStateFromStore (name:'+this.props.name+')', { store: state, name: this.props.name, selected });
+		return {
+			selected: selected,
+		};
+	},
+	getInitialState: function () {
+		return this.getStateFromStore();
+	},
+	componentDidMount: function () {
+		DBG && console.log('DBG::P5UI__TableAjaxSpecialFilterRelations::componentDidMount (name:'+this.props.name+')');
+		this.unsubscribe = this.props.store.subscribe(this.storeUpdated)
+	},
+	componentWillUnmount: function () {
+		this.unsubscribe()
+	},
+	storeUpdated: function () {
+		DBG && console.log('DBG::P5UI__TableAjaxSpecialFilterRelations::storeUpdated (name:'+this.props.name+')');
+		this.setState(this.getStateFromStore())
+	},
+	shouldComponentUpdate: function (nextProps, nextState) {
+		DBG && console.log('DBG::P5UI__TableAjaxSpecialFilterRelations::shouldComponentUpdate (name:'+this.props.name+')', { state: this.state, nextState});
+		return (
+			this.state.selected !== nextState.selected
+		);
+	},
+
+	handleRemoveFilter: function (event) {
+		this.props.store.dispatch( this.props.actions.setSpecialFilter(this.props.name, '') );
+	},
+	handleSetFilter: function (value) {
+		this.props.store.dispatch( this.props.actions.setSpecialFilter(this.props.name, value) );
+	},
+	handleCreate: function () {
+		// TODO: save swal reference to variable to avoid open twice?
+		var inputOptions = {};
+		if (this.props.availableBackRefs) this.props.availableBackRefs.forEach(function (instanceInfo) {
+			inputOptions[ 'From_' + instanceInfo.id ] = "z " + instanceInfo.label || instanceInfo.namespace;
+		});
+		if (this.props.availableChildRefs) this.props.availableChildRefs.forEach(function (instanceInfo) {
+			inputOptions[ 'To_' + instanceInfo.id ] = "do " + instanceInfo.label || instanceInfo.namespace;
+		});
+
+		swal({
+			title: "Wybierz rodzaj relacji",
+			input: 'select',
+			inputOptions: inputOptions,
+			inputPlaceholder: "Wybierz",
+			showCancelButton: true,
+			cancelButtonText: "Anuluj",
+			animation: false,
+			inputValidator: function (value) {
+				DBG && console.log('DBG::P5UI__TableAjaxSpecialFilterRelations::inputValidator', value);
+				return new Promise(function (resolve) {
+					if (!value) {
+						resolve('Nie wybrano obiektu');
+					} else {
+						resolve();
+					}
+				});
+			},
+		}).then(this.handleInstanceCreate) // args: { value: ... }
+	},
+	handleInstanceCreate: function (result) {
+		if (!result.value) return; // cancel when result = { dismiss: "cancel" }
+		var idInstance = '';
+		var type = '';
+		var instanceLabel = '';
+		if ('From_' === result.value.substr(0, 'From_'.length)) {
+			type = 'From';
+			idInstance = result.value.substr('From_'.length);
+			if (this.props.availableBackRefs) this.props.availableBackRefs.forEach(function (instanceInfo) {
+				if (idInstance == instanceInfo.id) instanceLabel = instanceInfo.label || instanceInfo.namespace;
+			});
+		}
+		else if ('To_' === result.value.substr(0, 'To_'.length)) {
+			type = 'To';
+			idInstance = result.value.substr('To_'.length);
+			if (this.props.availableChildRefs) this.props.availableChildRefs.forEach(function (instanceInfo) {
+				if (idInstance == instanceInfo.id) instanceLabel = instanceInfo.label || instanceInfo.namespace;
+			});
+		}
+		DBG && console.log('DBG::P5UI__TableAjaxSpecialFilterRelations::handleInstanceCreate (name:'+this.props.name+') selected instance('+idInstance+')', { result: result, state: this.state, props: this.props, availableBackRefs: this.props.availableBackRefs });
+
+		swal({
+			title: "Wybierz obiekt",
+			html: (instanceLabel) ? "typu: " + instanceLabel : "",
+			input: 'text',
+			inputPlaceholder: "Wybierz obiekt",
+			showCancelButton: true,
+			cancelButtonText: "Anuluj",
+			animation: false,
+			inputValidator: function (value) {
+				DBG && console.log('DBG::P5UI__TableAjaxSpecialFilterRelations::inputValidator', value);
+				return new Promise(function (resolve) {
+					if (!value) {
+						resolve('Nie podano obiektu');
+					} else {
+						resolve();
+					}
+				});
+			},
+			preConfirm: function (value) {
+				return {
+					type: type,
+					idInstance: idInstance,
+					primaryKey: value,
+				}
+			}
+		}).then(this.handleInstancePrimaryKeySelected) // args: { value: { idInstance, primaryKey } }
+	},
+	handleInstancePrimaryKeySelected: function (result) {
+		if (!result.value) return; // cancel when result = { dismiss: "cancel" }
+		var type = result.value.type;
+		var idInstance = result.value.idInstance;
+		var primaryKey = result.value.primaryKey;
+		DBG && console.log('DBG::P5UI__TableAjaxSpecialFilterRelations::handleInstancePrimaryKeySelected (name:'+this.props.name+') selected featureID('+idInstance+'.'+primaryKey+')', { result: result, state: this.state, props: this.props, availableBackRefs: this.props.availableBackRefs });
+		this.props.store.dispatch( this.props.actions.setSpecialFilter('Ref_' + type + '_' + idInstance, primaryKey) );
+	},
+
+	renderButton: function (buttonKey) {
+		var option = buttonKey;
+		var props = this.props.values[option];
+		var value = props.value || '';
+		var state = this.props.store.getState();
+		DBG && console.warn('DBG:renderButton state', { state, props, value });
+		var active = (value === this.state.selected);
+		return h('button', {
+			className: "btn btn-xs btn-default" + ( active ? " active" : "" ),
+			onClick: function (event) {
+				DBG && console.warn('DBG:renderButton click', { value });
+				this.handleSetFilter(value);
+			}.bind(this),
+		}, option);
+	},
+	renderLabel: function () {
+		var label = this.props.label || this.props.name;
+		return h('button', {
+			className: "btn btn-xs btn-default",
+			title: label,
+			style: {
+				color: '#31708f',
+				backgroundColor: '#d9edf7',
+			}
+		}, [
+			h('i', { className: this.props.icon }),
+			" Filtr relacji: ",
+		]);
+	},
+	renderListSelected: function () { // @return array of react nodes
+		DBG && console.log('DBG::P5UI__TableAjaxSpecialFilterRelations::this.props.availableBackRefs (name:'+this.props.name+')', { state: this.state, props: this.props, availableBackRefs: this.props.availableBackRefs });
+		return this.state.selected.map(this.renderSelected);
+	},
+	renderSelected: function (selected) { // @selected: { type: 'From' | 'To', filterKey, idInstance, primaryKey },  @return react node
+		var filterKey = selected.filterKey;
+		var type = selected.type; // From | To - backRef | childRef
+		var idInstance = selected.idInstance;
+		var primaryKey = selected.primaryKey;
+		var namespace = idInstance;
+		var label = "" + idInstance + "." + primaryKey + "";
+		if ('From' === type) {
+			var foundInstanceInfo = this.props.availableBackRefs.filter(function (instanceInfo) {
+				return ( parseInt(idInstance) === parseInt(instanceInfo.id) );
+			});
+			if (foundInstanceInfo.length > 0) label = foundInstanceInfo[0].label + "." + primaryKey;
+			if (foundInstanceInfo.length > 0) namespace = foundInstanceInfo[0].namespace;
+		} else if ('To' === type) {
+			var foundInstanceInfo = this.props.availableChildRefs.filter(function (instanceInfo) {
+				return ( parseInt(idInstance) === parseInt(instanceInfo.id) );
+			});
+			if (foundInstanceInfo.length > 0) label = foundInstanceInfo[0].label + "." + primaryKey;
+			if (foundInstanceInfo.length > 0) namespace = foundInstanceInfo[0].namespace;
+		}
+		return h(P5UI__TableAjaxSpecialFilterRelationFrom, {
+			filterKey: filterKey,
+			label: label,
+			namespace: namespace,
+			primaryKey: primaryKey,
+			store: this.props.store,
+			actions: this.props.actions,
+		});
+	},
+	renderBackRef: function () { // @return react node - button with dropdown to select primaryKey from parent instance + remove button if selected
+		return null;
+	},
+	renderCreateButton: function () {
+		return h('button', {
+			className: "btn btn-xs btn-default",
+			title: "Dodaj nowy filtr relacji",
+			onClick: this.handleCreate,
+		}, [
+			h('i', { className: 'glyphicon glyphicon-plus' })
+		]);
+	},
+	render: function () {
+		DBG && console.log('DBG::P5UI__TableAjaxSpecialFilterRelations::render (name:'+this.props.name+')', { state: this.state, props: this.props });
+		return h('div', { className: "btn-group", style: { margin: '0 4px 0 0' } },
+			[
+				this.renderLabel(),
+			].concat(
+				this.renderListSelected()
+			).concat([
+				this.renderCreateButton()
+			])
+			// 	Object.keys(this.props.values).map(this.renderButton)
+			// ).concat(
+				// [
+				// 	h('button', {
+				// 		className: "btn btn-xs btn-default",
+				// 		title: "Kasuj filtr",
+				// 		disabled: !this.state.selected,
+				// 		onClick: this.handleRemoveFilter,
+				// 		style: { color: !this.state.selected ? '#bbb' : '#f00' }
+				// 	}, [
+				// 		h('i', { className: "glyphicon glyphicon-remove" })
+				// 	])
+				// ]
+		);
+	}
+});
+global['P5UI__TableAjaxSpecialFilterRelationFrom'] = createReactClass({
+	handleRemoveFilter: function (event) {
+		this.props.store.dispatch( this.props.actions.setSpecialFilter(this.props.filterKey, '') );
+	},
+
+	render: function () {
+		DBG && console.log('DBG::P5UI__TableAjaxSpecialFilterRelationFrom::render (filterKey:'+this.props.filterKey+')', { state: this.state, props: this.props });
+		return h('button', {
+			className: "btn btn-xs btn-default active",
+			title: "Powiązane z " + this.props.namespace + " " + this.props.primaryKey,
+			// onClick: this.handleUpdate, // TODO: paste param
+		}, [
+			this.props.label,
+			h('i', {
+				className: 'glyphicon glyphicon-remove',
+				style: {
+					color: '#f00',
+					opacity: 0.5,
+					marginLeft: "3px",
+				},
+				onClick: this.handleRemoveFilter,
+			})
+		]);
+	}
+});
 global['P5UI__TableAjaxSpecialFilters'] = createReactClass({
 	// props.store: Redux store with state: { isLoading bool, selected array of strings }
 	// props.actions: Redux store actions
@@ -683,6 +949,19 @@ global['P5UI__TableAjaxSpecialFilters'] = createReactClass({
 	// },
 	renderSpecialFilter: function (key) {
 		var props = this.props.specialFilters[key];
+		DBG && console.log('DBG::P5UI__TableAjaxSpecialFilters::renderSpecialFilter (ns:'+this.props.namespace+')', { key: key, props, state: this.state });
+		if ('RELATIONS' === props.type) {
+			DBG && console.warn('DBG::P5UI__TableAjaxSpecialFilters::renderSpecialFilter TODO render RELATIONS filter (ns:'+this.props.namespace+')', { props, state: this.state });
+			return h(P5UI__TableAjaxSpecialFilterRelations, {
+				store: this.props.store,
+				actions: this.props.actions,
+				name: key,
+				icon: props.icon,
+				label: props.label,
+				availableBackRefs: props.availableBackRefs,
+				availableChildRefs: props.availableChildRefs,
+			});
+		}
 		return h(P5UI__TableAjaxSpecialFilter, {
 			store: this.props.store,
 			actions: this.props.actions,
@@ -2484,6 +2763,21 @@ var TableAjax = function() {
 		node.attr('href', exportUrl);
 	};
 
+	priv.showFailFetchDataMsg = function () {
+		var clearAllFiltersBtn = jQuery('<button class="btn btn-xs btn-link" style="color:red">usunąć wszystkie filtry</button>')
+		clearAllFiltersBtn.on('click', function () {
+			priv.options.filterStore.dispatch( priv.options.filterActions.clearAllFilters() )
+			jQuery(this).parent().remove();
+		})
+		var msgNode = jQuery('<div class="alert alert-danger" style="clear:both; max-width:600px; margin: 10px auto">' +
+			'Wystąpił błąd podczas pobierania danych ' +
+			'<a href="javascript:window.location.reload()" class="btn btn-xs btn-link">spróbuj ponownie</a>.' + '<br>' +
+			'Jeśli problem się powtarza, spróbuj ' +
+		'</div>')
+		msgNode.append(clearAllFiltersBtn);
+		jQuery(_uiNodeCont).prepend(msgNode);
+	}
+
 	/*
 	 calls the webservice(if defined).
 	 used only inside priv.init
@@ -2514,12 +2808,10 @@ var TableAjax = function() {
 		}).then(function (data) {
 			if (priv.options.debug || DBG) console.log('loadDataAjax:fetch:update: request finished response data:', data);
 			if ('success' == data.type) {
-				// p5UI__notifyAjaxCallback(data);
 				return data;
-			} else if ('error' == data.type) {
-				p5UI__notifyAjaxCallback(data);
 			} else {
 				p5UI__notifyAjaxCallback(data);
+				priv.showFailFetchDataMsg();
 			}
 			return null;
 		}).then(function (data) {
@@ -3590,10 +3882,9 @@ var TableAjax = function() {
 				state.filters = data.filters || {};
 				priv.setState(state);
 				_uiNode$Table.parent().parent().removeClass('AjaxTable-loading');
-			} else if ('error' == data.type) {
-				p5UI__notifyAjaxCallback(data);
 			} else {
 				p5UI__notifyAjaxCallback(data);
+				priv.showFailFetchDataMsg();
 			}
 		}).catch(function (e) {
 			console.log('loadDataAjax:fetch: ERR:', e);